mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
feat: new project card (#7992)
This commit is contained in:
parent
3188f991f4
commit
8923e28d83
@ -2,15 +2,10 @@ import type { FC } from 'react';
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';
|
import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
import { styled } from '@mui/material';
|
|
||||||
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
|
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
|
||||||
|
|
||||||
type FavoriteActionProps = { id: string; isFavorite?: boolean };
|
type FavoriteActionProps = { id: string; isFavorite?: boolean };
|
||||||
|
|
||||||
const StyledFavoriteIconButton = styled(FavoriteIconButton)(({ theme }) => ({
|
|
||||||
margin: theme.spacing(1, 2, 0, 0),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const FavoriteAction: FC<FavoriteActionProps> = ({ id, isFavorite }) => {
|
export const FavoriteAction: FC<FavoriteActionProps> = ({ id, isFavorite }) => {
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
const { favorite, unfavorite } = useFavoriteProjectsApi();
|
const { favorite, unfavorite } = useFavoriteProjectsApi();
|
||||||
@ -31,7 +26,7 @@ export const FavoriteAction: FC<FavoriteActionProps> = ({ id, isFavorite }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFavoriteIconButton
|
<FavoriteIconButton
|
||||||
onClick={onFavorite}
|
onClick={onFavorite}
|
||||||
isFavorite={Boolean(isFavorite)}
|
isFavorite={Boolean(isFavorite)}
|
||||||
size='medium'
|
size='medium'
|
@ -12,21 +12,10 @@ import {
|
|||||||
} from './ProjectCard.styles';
|
} from './ProjectCard.styles';
|
||||||
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
|
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
|
||||||
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge';
|
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge';
|
||||||
import type { ProjectSchemaOwners } from 'openapi';
|
|
||||||
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
||||||
import { FavoriteAction } from './ProjectCardFooter/FavoriteAction/FavoriteAction';
|
import { FavoriteAction } from './FavoriteAction/FavoriteAction';
|
||||||
|
import type { IProjectCard } from 'interfaces/project';
|
||||||
interface IProjectCardProps {
|
import { Box } from '@mui/material';
|
||||||
name: string;
|
|
||||||
featureCount: number;
|
|
||||||
health: number;
|
|
||||||
memberCount?: number;
|
|
||||||
id: string;
|
|
||||||
onHover: () => void;
|
|
||||||
favorite?: boolean;
|
|
||||||
mode: string;
|
|
||||||
owners?: ProjectSchemaOwners;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProjectCard = ({
|
export const ProjectCard = ({
|
||||||
name,
|
name,
|
||||||
@ -38,7 +27,7 @@ export const ProjectCard = ({
|
|||||||
mode,
|
mode,
|
||||||
favorite = false,
|
favorite = false,
|
||||||
owners,
|
owners,
|
||||||
}: IProjectCardProps) => (
|
}: IProjectCard) => (
|
||||||
<StyledProjectCard onMouseEnter={onHover}>
|
<StyledProjectCard onMouseEnter={onHover}>
|
||||||
<StyledProjectCardBody>
|
<StyledProjectCardBody>
|
||||||
<StyledDivHeader>
|
<StyledDivHeader>
|
||||||
@ -79,7 +68,9 @@ export const ProjectCard = ({
|
|||||||
</StyledDivInfo>
|
</StyledDivInfo>
|
||||||
</StyledProjectCardBody>
|
</StyledProjectCardBody>
|
||||||
<ProjectCardFooter id={id} owners={owners}>
|
<ProjectCardFooter id={id} owners={owners}>
|
||||||
<FavoriteAction id={id} isFavorite={favorite} />
|
<Box sx={(theme) => ({ margin: theme.spacing(1, 2, 0, 0) })}>
|
||||||
|
<FavoriteAction id={id} isFavorite={favorite} />
|
||||||
|
</Box>
|
||||||
</ProjectCardFooter>
|
</ProjectCardFooter>
|
||||||
</StyledProjectCard>
|
</StyledProjectCard>
|
||||||
);
|
);
|
||||||
|
@ -34,7 +34,6 @@ export type ProjectArchiveCardProps = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
archivedAt?: string;
|
archivedAt?: string;
|
||||||
archivedFeaturesCount?: number;
|
|
||||||
onRevive: () => void;
|
onRevive: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
@ -45,7 +44,6 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
archivedAt,
|
archivedAt,
|
||||||
archivedFeaturesCount,
|
|
||||||
onRevive,
|
onRevive,
|
||||||
onDelete,
|
onDelete,
|
||||||
mode,
|
mode,
|
||||||
|
@ -41,20 +41,22 @@ export const StyledDivHeader = styled('div')(({ theme }) => ({
|
|||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledCardTitle = styled('h3')(({ theme }) => ({
|
export const StyledCardTitle = styled('h3')<{ lines?: number }>(
|
||||||
fontWeight: theme.typography.fontWeightRegular,
|
({ theme, lines = 2 }) => ({
|
||||||
fontSize: theme.typography.body1.fontSize,
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
lineClamp: '2',
|
fontSize: theme.typography.body1.fontSize,
|
||||||
WebkitLineClamp: 2,
|
lineClamp: `${lines}`,
|
||||||
lineHeight: '1.2',
|
WebkitLineClamp: lines,
|
||||||
display: '-webkit-box',
|
lineHeight: '1.2',
|
||||||
boxOrient: 'vertical',
|
display: '-webkit-box',
|
||||||
textOverflow: 'ellipsis',
|
boxOrient: 'vertical',
|
||||||
overflow: 'hidden',
|
textOverflow: 'ellipsis',
|
||||||
alignItems: 'flex-start',
|
overflow: 'hidden',
|
||||||
WebkitBoxOrient: 'vertical',
|
alignItems: 'flex-start',
|
||||||
wordBreak: 'break-word',
|
WebkitBoxOrient: 'vertical',
|
||||||
}));
|
wordBreak: 'break-word',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const StyledBox = styled(Box)(() => ({
|
export const StyledBox = styled(Box)(() => ({
|
||||||
...flexRow,
|
...flexRow,
|
||||||
|
@ -1,32 +1,37 @@
|
|||||||
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
StyledProjectCard,
|
StyledProjectCard,
|
||||||
StyledDivHeader,
|
StyledDivHeader,
|
||||||
StyledBox,
|
|
||||||
StyledCardTitle,
|
StyledCardTitle,
|
||||||
StyledDivInfo,
|
|
||||||
StyledParagraphInfo,
|
|
||||||
StyledProjectCardBody,
|
StyledProjectCardBody,
|
||||||
StyledIconBox,
|
StyledIconBox,
|
||||||
} from './ProjectCard.styles';
|
} from './ProjectCard.styles';
|
||||||
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
|
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
|
||||||
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge';
|
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge';
|
||||||
import type { ProjectSchemaOwners } from 'openapi';
|
|
||||||
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
||||||
import { FavoriteAction } from './ProjectCardFooter/FavoriteAction/FavoriteAction';
|
import { FavoriteAction } from './FavoriteAction/FavoriteAction';
|
||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
import { flexColumn } from 'themes/themeStyles';
|
||||||
|
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||||
|
import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen';
|
||||||
|
import type { IProjectCard } from 'interfaces/project';
|
||||||
|
|
||||||
interface IProjectCardProps {
|
const StyledUpdated = styled('span')(({ theme }) => ({
|
||||||
name: string;
|
color: theme.palette.text.secondary,
|
||||||
featureCount: number;
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
health: number;
|
}));
|
||||||
memberCount?: number;
|
|
||||||
id: string;
|
const StyledCount = styled('strong')(({ theme }) => ({
|
||||||
onHover: () => void;
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
favorite?: boolean;
|
}));
|
||||||
mode: string;
|
|
||||||
owners?: ProjectSchemaOwners;
|
const StyledInfo = styled('div')(({ theme }) => ({
|
||||||
}
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
}));
|
||||||
|
|
||||||
export const ProjectCard = ({
|
export const ProjectCard = ({
|
||||||
name,
|
name,
|
||||||
@ -38,48 +43,50 @@ export const ProjectCard = ({
|
|||||||
mode,
|
mode,
|
||||||
favorite = false,
|
favorite = false,
|
||||||
owners,
|
owners,
|
||||||
}: IProjectCardProps) => (
|
lastUpdatedAt,
|
||||||
|
lastReportedFlagUsage,
|
||||||
|
}: IProjectCard) => (
|
||||||
<StyledProjectCard onMouseEnter={onHover}>
|
<StyledProjectCard onMouseEnter={onHover}>
|
||||||
<StyledProjectCardBody>
|
<StyledProjectCardBody>
|
||||||
<StyledDivHeader>
|
<StyledDivHeader>
|
||||||
<StyledIconBox>
|
<StyledIconBox>
|
||||||
<ProjectIcon />
|
<ProjectIcon />
|
||||||
</StyledIconBox>
|
</StyledIconBox>
|
||||||
<StyledBox data-loading>
|
<Box
|
||||||
<StyledCardTitle>{name}</StyledCardTitle>
|
data-loading
|
||||||
</StyledBox>
|
sx={(theme) => ({
|
||||||
|
...flexColumn,
|
||||||
|
margin: theme.spacing(1, 'auto', 1, 0),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<StyledCardTitle lines={1} sx={{ margin: 0 }}>
|
||||||
|
{name}
|
||||||
|
</StyledCardTitle>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(lastUpdatedAt)}
|
||||||
|
show={
|
||||||
|
<StyledUpdated>
|
||||||
|
Updated <TimeAgo date={lastUpdatedAt} />
|
||||||
|
</StyledUpdated>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
<ProjectModeBadge mode={mode} />
|
<ProjectModeBadge mode={mode} />
|
||||||
|
<FavoriteAction id={id} isFavorite={favorite} />
|
||||||
</StyledDivHeader>
|
</StyledDivHeader>
|
||||||
<StyledDivInfo>
|
<StyledInfo>
|
||||||
<div>
|
<div>
|
||||||
<StyledParagraphInfo data-loading>
|
<div>
|
||||||
{featureCount}
|
<StyledCount>{featureCount}</StyledCount> flag
|
||||||
</StyledParagraphInfo>
|
{featureCount === 1 ? '' : 's'}
|
||||||
<p data-loading>{featureCount === 1 ? 'flag' : 'flags'}</p>
|
</div>
|
||||||
|
<div>
|
||||||
|
<StyledCount>{health}%</StyledCount> health
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ConditionallyRender
|
<ProjectLastSeen date={lastReportedFlagUsage} />
|
||||||
condition={id !== DEFAULT_PROJECT_ID}
|
</StyledInfo>
|
||||||
show={
|
|
||||||
<div>
|
|
||||||
<StyledParagraphInfo data-loading>
|
|
||||||
{memberCount}
|
|
||||||
</StyledParagraphInfo>
|
|
||||||
<p data-loading>
|
|
||||||
{memberCount === 1 ? 'member' : 'members'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<StyledParagraphInfo data-loading>
|
|
||||||
{health}%
|
|
||||||
</StyledParagraphInfo>
|
|
||||||
<p data-loading>healthy</p>
|
|
||||||
</div>
|
|
||||||
</StyledDivInfo>
|
|
||||||
</StyledProjectCardBody>
|
</StyledProjectCardBody>
|
||||||
<ProjectCardFooter id={id} owners={owners}>
|
<ProjectCardFooter id={id} owners={owners} memberCount={memberCount} />
|
||||||
<FavoriteAction id={id} isFavorite={favorite} />
|
|
||||||
</ProjectCardFooter>
|
|
||||||
</StyledProjectCard>
|
</StyledProjectCard>
|
||||||
);
|
);
|
||||||
|
@ -3,15 +3,21 @@ import type { FC } from 'react';
|
|||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
import {
|
import {
|
||||||
type IProjectOwnersProps,
|
type IProjectOwnersProps,
|
||||||
ProjectOwners,
|
ProjectOwners as LegacyProjectOwners,
|
||||||
} from '../ProjectOwners/ProjectOwners';
|
} from '../LegacyProjectOwners/LegacyProjectOwners';
|
||||||
|
import { ProjectOwners } from './ProjectOwners/ProjectOwners';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ProjectMembers } from './ProjectMembers/ProjectMembers';
|
||||||
|
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
||||||
|
|
||||||
interface IProjectCardFooterProps {
|
interface IProjectCardFooterProps {
|
||||||
id: string;
|
id?: string;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
owners: IProjectOwnersProps['owners'];
|
owners: IProjectOwnersProps['owners'];
|
||||||
|
memberCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledFooter = styled(Box)<{ disabled: boolean }>(
|
const StyledFooter = styled(Box)<{ disabled: boolean }>(
|
||||||
@ -28,14 +34,29 @@ const StyledFooter = styled(Box)<{ disabled: boolean }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
||||||
|
id,
|
||||||
children,
|
children,
|
||||||
owners,
|
owners,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
memberCount,
|
||||||
}) => {
|
}) => {
|
||||||
|
const projectListImprovementsEnabled = useUiFlag('projectListImprovements');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFooter disabled={disabled}>
|
<StyledFooter disabled={disabled}>
|
||||||
<ProjectOwners owners={owners} />
|
<ConditionallyRender
|
||||||
{children}
|
condition={Boolean(projectListImprovementsEnabled)}
|
||||||
|
show={<ProjectOwners owners={owners} />}
|
||||||
|
elseShow={<LegacyProjectOwners owners={owners} />}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
Boolean(projectListImprovementsEnabled) &&
|
||||||
|
id !== DEFAULT_PROJECT_ID
|
||||||
|
}
|
||||||
|
show={<ProjectMembers count={memberCount} members={[]} />}
|
||||||
|
elseShow={children}
|
||||||
|
/>
|
||||||
</StyledFooter>
|
</StyledFooter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup';
|
||||||
|
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||||
|
import { flexColumn } from 'themes/themeStyles';
|
||||||
|
|
||||||
|
type ProjectMembersProps = {
|
||||||
|
count?: number;
|
||||||
|
members: Array<{
|
||||||
|
imageUrl?: string;
|
||||||
|
email?: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
...flexColumn,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
padding: theme.spacing(0, 2, 0, 1),
|
||||||
|
minWidth: 95,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDescription = styled('span')(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||||
|
width: theme.spacing(2),
|
||||||
|
height: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const AvatarComponent = ({ ...props }) => (
|
||||||
|
<StyledAvatar {...props} disableTooltip />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ProjectMembers: FC<ProjectMembersProps> = ({
|
||||||
|
count = 0,
|
||||||
|
members,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledDescription data-loading>
|
||||||
|
{count} member
|
||||||
|
{count === 1 ? '' : 's'}
|
||||||
|
</StyledDescription>
|
||||||
|
<AvatarGroup
|
||||||
|
users={members}
|
||||||
|
avatarLimit={4}
|
||||||
|
AvatarComponent={AvatarComponent}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,113 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import type { ProjectSchema, ProjectSchemaOwners } from 'openapi';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import {
|
||||||
|
AvatarGroup,
|
||||||
|
AvatarComponent,
|
||||||
|
} from 'component/common/AvatarGroup/AvatarGroup';
|
||||||
|
|
||||||
|
export interface IProjectOwnersProps {
|
||||||
|
owners?: ProjectSchema['owners'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const useOwnersMap = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
owner: ProjectSchemaOwners[0],
|
||||||
|
): {
|
||||||
|
name: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
email?: string;
|
||||||
|
} => {
|
||||||
|
if (owner.ownerType === 'user') {
|
||||||
|
return {
|
||||||
|
name: owner.name,
|
||||||
|
imageUrl: owner.imageUrl || undefined,
|
||||||
|
email: owner.email || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (owner.ownerType === 'group') {
|
||||||
|
return {
|
||||||
|
name: owner.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: 'System',
|
||||||
|
imageUrl: `${uiConfig.unleashUrl}/logo-unleash.png`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledUserName = styled('span')(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
alignSelf: 'end',
|
||||||
|
lineHeight: 1,
|
||||||
|
lineClamp: `1`,
|
||||||
|
WebkitLineClamp: 1,
|
||||||
|
display: '-webkit-box',
|
||||||
|
boxOrient: 'vertical',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledOwnerName = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(0.25),
|
||||||
|
margin: theme.spacing(0, 0, 0, 1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledHeader = styled('span')(({ theme }) => ({
|
||||||
|
lineHeight: 1,
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledWrapper = styled('div')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(1.5, 0, 1.5, 2),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAvatarComponent = styled(AvatarComponent)(({ theme }) => ({
|
||||||
|
cursor: 'default',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
|
||||||
|
const ownersMap = useOwnersMap();
|
||||||
|
const users = owners.map(ownersMap);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper data-testid='test'>
|
||||||
|
<StyledContainer>
|
||||||
|
<AvatarGroup
|
||||||
|
users={users}
|
||||||
|
avatarLimit={4}
|
||||||
|
AvatarComponent={StyledAvatarComponent}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={owners.length === 1}
|
||||||
|
show={
|
||||||
|
<StyledOwnerName>
|
||||||
|
<StyledHeader>Owner</StyledHeader>
|
||||||
|
<StyledUserName>{users[0]?.name}</StyledUserName>
|
||||||
|
</StyledOwnerName>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,80 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { useLastSeenColors } from 'component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors';
|
||||||
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
|
import { ReactComponent as UsageLine } from 'assets/icons/usage-line.svg';
|
||||||
|
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
|
||||||
|
import { StyledIconWrapper } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||||
|
import { flexRow } from 'themes/themeStyles';
|
||||||
|
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||||
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
|
|
||||||
|
type ProjectLastSeenProps = {
|
||||||
|
date?: Date | number | string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
|
...flexRow,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
width: '50%',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
cursor: 'default',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledIcon = styled(StyledIconWrapper)<{ background: string }>(
|
||||||
|
({ background }) => ({
|
||||||
|
background,
|
||||||
|
margin: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const Title = () => (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
component='span'
|
||||||
|
sx={(theme) => ({ fontSize: theme.fontSizes.smallBody })}
|
||||||
|
>
|
||||||
|
Last usage reported
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Across all flags in your project this is the last time usage metrics
|
||||||
|
where reported from connected applications.
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ProjectLastSeen: FC<ProjectLastSeenProps> = ({ date }) => {
|
||||||
|
const getColor = useLastSeenColors();
|
||||||
|
const { text, background } = getColor(date);
|
||||||
|
|
||||||
|
if (!date) {
|
||||||
|
return (
|
||||||
|
<HtmlTooltip title={<Title />} arrow>
|
||||||
|
<StyledContainer
|
||||||
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
|
>
|
||||||
|
<StyledIcon background={background}>
|
||||||
|
<UsageLine stroke={text} />
|
||||||
|
</StyledIcon>{' '}
|
||||||
|
<div>No activity</div>
|
||||||
|
</StyledContainer>
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HtmlTooltip title={<Title />} arrow>
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledIcon background={background}>
|
||||||
|
<UsageRate stroke={text} />
|
||||||
|
</StyledIcon>{' '}
|
||||||
|
<TimeAgo date={date} refresh={false} />
|
||||||
|
</StyledContainer>
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
};
|
@ -1,15 +1,39 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
import ProtectedProjectIcon from '@mui/icons-material/LockOutlined';
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
|
import PrivateProjectIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
interface IProjectModeBadgeProps {
|
interface IProjectModeBadgeProps {
|
||||||
mode?: 'private' | 'protected' | 'public' | string;
|
mode?: 'private' | 'protected' | 'public' | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledIcon = styled('div')(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontSize: theme.spacing(2.25),
|
||||||
|
paddingTop: theme.spacing(0.75),
|
||||||
|
}));
|
||||||
|
|
||||||
export const ProjectModeBadge: FC<IProjectModeBadgeProps> = ({ mode }) => {
|
export const ProjectModeBadge: FC<IProjectModeBadgeProps> = ({ mode }) => {
|
||||||
|
const projectListImprovementsEnabled = useUiFlag('projectListImprovements');
|
||||||
|
|
||||||
if (mode === 'private') {
|
if (mode === 'private') {
|
||||||
|
if (projectListImprovementsEnabled) {
|
||||||
|
return (
|
||||||
|
<HtmlTooltip
|
||||||
|
title="This project's collaboration mode is set to private. The project and associated feature flags can only be seen by members of the project."
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<StyledIcon>
|
||||||
|
<PrivateProjectIcon fontSize='inherit' />
|
||||||
|
</StyledIcon>
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<HtmlTooltip
|
<HtmlTooltip
|
||||||
title="This project's collaboration mode is set to private. The project and associated feature flags can only be seen by members of the project."
|
title="This project's collaboration mode is set to private. The project and associated feature flags can only be seen by members of the project."
|
||||||
@ -21,6 +45,18 @@ export const ProjectModeBadge: FC<IProjectModeBadgeProps> = ({ mode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'protected') {
|
if (mode === 'protected') {
|
||||||
|
if (projectListImprovementsEnabled) {
|
||||||
|
return (
|
||||||
|
<HtmlTooltip
|
||||||
|
title="This project's collaboration mode is set to protected. Only admins and project members can submit change requests."
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<StyledIcon>
|
||||||
|
<ProtectedProjectIcon fontSize='inherit' />
|
||||||
|
</StyledIcon>
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<HtmlTooltip
|
<HtmlTooltip
|
||||||
title="This project's collaboration mode is set to protected. Only admins and project members can submit change requests."
|
title="This project's collaboration mode is set to protected. Only admins and project members can submit change requests."
|
||||||
|
@ -2,11 +2,13 @@ import type { ComponentType } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { ProjectCard as LegacyProjectCard } from '../ProjectCard/LegacyProjectCard';
|
import { ProjectCard as LegacyProjectCard } from '../ProjectCard/LegacyProjectCard';
|
||||||
|
import { ProjectCard as NewProjectCard } from '../ProjectCard/ProjectCard';
|
||||||
|
|
||||||
import type { IProjectCard } from 'interfaces/project';
|
import type { IProjectCard } from 'interfaces/project';
|
||||||
import loadingData from './loadingData';
|
import loadingData from './loadingData';
|
||||||
import { TablePlaceholder } from 'component/common/Table';
|
import { TablePlaceholder } from 'component/common/Table';
|
||||||
import { styled, Typography } from '@mui/material';
|
import { styled, Typography } from '@mui/material';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const StyledGridContainer = styled('div')(({ theme }) => ({
|
const StyledGridContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -24,25 +26,30 @@ const StyledCardLink = styled(Link)(({ theme }) => ({
|
|||||||
pointer: 'cursor',
|
pointer: 'cursor',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type ProjectGroupProps<T extends { id: string } = IProjectCard> = {
|
type ProjectGroupProps = {
|
||||||
sectionTitle?: string;
|
sectionTitle?: string;
|
||||||
projects: T[];
|
projects: IProjectCard[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
ProjectCardComponent?: ComponentType<T & any>;
|
ProjectCardComponent?: ComponentType<IProjectCard & any>;
|
||||||
link?: boolean;
|
link?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectGroup = <T extends { id: string }>({
|
export const ProjectGroup = ({
|
||||||
sectionTitle,
|
sectionTitle,
|
||||||
projects,
|
projects,
|
||||||
loading,
|
loading,
|
||||||
searchValue,
|
searchValue,
|
||||||
placeholder = 'No projects available.',
|
placeholder = 'No projects available.',
|
||||||
ProjectCardComponent = LegacyProjectCard,
|
ProjectCardComponent,
|
||||||
link = true,
|
link = true,
|
||||||
}: ProjectGroupProps<T>) => {
|
}: ProjectGroupProps) => {
|
||||||
|
const projectListImprovementsEnabled = useUiFlag('projectListImprovements');
|
||||||
|
const ProjectCard =
|
||||||
|
ProjectCardComponent ??
|
||||||
|
(projectListImprovementsEnabled ? NewProjectCard : LegacyProjectCard);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -82,9 +89,9 @@ export const ProjectGroup = <T extends { id: string }>({
|
|||||||
<>
|
<>
|
||||||
{loadingData.map(
|
{loadingData.map(
|
||||||
(project: IProjectCard) => (
|
(project: IProjectCard) => (
|
||||||
<LegacyProjectCard
|
<ProjectCard
|
||||||
data-loading
|
data-loading
|
||||||
onHover={() => {}}
|
createdAt={project.createdAt}
|
||||||
key={project.id}
|
key={project.id}
|
||||||
name={project.name}
|
name={project.name}
|
||||||
id={project.id}
|
id={project.id}
|
||||||
@ -99,21 +106,17 @@ export const ProjectGroup = <T extends { id: string }>({
|
|||||||
)}
|
)}
|
||||||
elseShow={() => (
|
elseShow={() => (
|
||||||
<>
|
<>
|
||||||
{projects.map((project: T) =>
|
{projects.map((project) =>
|
||||||
link ? (
|
link ? (
|
||||||
<StyledCardLink
|
<StyledCardLink
|
||||||
key={project.id}
|
key={project.id}
|
||||||
to={`/projects/${project.id}`}
|
to={`/projects/${project.id}`}
|
||||||
>
|
>
|
||||||
<ProjectCardComponent
|
<ProjectCard {...project} />
|
||||||
onHover={() => {}}
|
|
||||||
{...project}
|
|
||||||
/>
|
|
||||||
</StyledCardLink>
|
</StyledCardLink>
|
||||||
) : (
|
) : (
|
||||||
<ProjectCardComponent
|
<ProjectCard
|
||||||
key={project.id}
|
key={project.id}
|
||||||
onHover={() => {}}
|
|
||||||
{...project}
|
{...project}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -7,13 +7,16 @@ export interface IProjectCard {
|
|||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
health: number;
|
health?: number;
|
||||||
description: string;
|
description?: string;
|
||||||
featureCount: number;
|
featureCount?: number;
|
||||||
mode: string;
|
mode?: string;
|
||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
|
onHover?: () => void;
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
owners?: ProjectSchema['owners'];
|
owners?: ProjectSchema['owners'];
|
||||||
|
lastUpdatedAt?: Date;
|
||||||
|
lastReportedFlagUsage?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeatureNamingType = {
|
export type FeatureNamingType = {
|
||||||
|
Loading…
Reference in New Issue
Block a user