mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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 { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi'; | ||||
| import useProjects from 'hooks/api/getters/useProjects/useProjects'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton'; | ||||
| 
 | ||||
| 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 }) => { | ||||
|     const { setToastApiError } = useToast(); | ||||
|     const { favorite, unfavorite } = useFavoriteProjectsApi(); | ||||
| @ -31,7 +26,7 @@ export const FavoriteAction: FC<FavoriteActionProps> = ({ id, isFavorite }) => { | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledFavoriteIconButton | ||||
|         <FavoriteIconButton | ||||
|             onClick={onFavorite} | ||||
|             isFavorite={Boolean(isFavorite)} | ||||
|             size='medium' | ||||
| @ -12,21 +12,10 @@ import { | ||||
| } from './ProjectCard.styles'; | ||||
| import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter'; | ||||
| import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge'; | ||||
| import type { ProjectSchemaOwners } from 'openapi'; | ||||
| import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon'; | ||||
| import { FavoriteAction } from './ProjectCardFooter/FavoriteAction/FavoriteAction'; | ||||
| 
 | ||||
| interface IProjectCardProps { | ||||
|     name: string; | ||||
|     featureCount: number; | ||||
|     health: number; | ||||
|     memberCount?: number; | ||||
|     id: string; | ||||
|     onHover: () => void; | ||||
|     favorite?: boolean; | ||||
|     mode: string; | ||||
|     owners?: ProjectSchemaOwners; | ||||
| } | ||||
| import { FavoriteAction } from './FavoriteAction/FavoriteAction'; | ||||
| import type { IProjectCard } from 'interfaces/project'; | ||||
| import { Box } from '@mui/material'; | ||||
| 
 | ||||
| export const ProjectCard = ({ | ||||
|     name, | ||||
| @ -38,7 +27,7 @@ export const ProjectCard = ({ | ||||
|     mode, | ||||
|     favorite = false, | ||||
|     owners, | ||||
| }: IProjectCardProps) => ( | ||||
| }: IProjectCard) => ( | ||||
|     <StyledProjectCard onMouseEnter={onHover}> | ||||
|         <StyledProjectCardBody> | ||||
|             <StyledDivHeader> | ||||
| @ -79,7 +68,9 @@ export const ProjectCard = ({ | ||||
|             </StyledDivInfo> | ||||
|         </StyledProjectCardBody> | ||||
|         <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> | ||||
|     </StyledProjectCard> | ||||
| ); | ||||
|  | ||||
| @ -34,7 +34,6 @@ export type ProjectArchiveCardProps = { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     archivedAt?: string; | ||||
|     archivedFeaturesCount?: number; | ||||
|     onRevive: () => void; | ||||
|     onDelete: () => void; | ||||
|     mode?: string; | ||||
| @ -45,7 +44,6 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({ | ||||
|     id, | ||||
|     name, | ||||
|     archivedAt, | ||||
|     archivedFeaturesCount, | ||||
|     onRevive, | ||||
|     onDelete, | ||||
|     mode, | ||||
|  | ||||
| @ -41,20 +41,22 @@ export const StyledDivHeader = styled('div')(({ theme }) => ({ | ||||
|     gap: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| export const StyledCardTitle = styled('h3')(({ theme }) => ({ | ||||
|     fontWeight: theme.typography.fontWeightRegular, | ||||
|     fontSize: theme.typography.body1.fontSize, | ||||
|     lineClamp: '2', | ||||
|     WebkitLineClamp: 2, | ||||
|     lineHeight: '1.2', | ||||
|     display: '-webkit-box', | ||||
|     boxOrient: 'vertical', | ||||
|     textOverflow: 'ellipsis', | ||||
|     overflow: 'hidden', | ||||
|     alignItems: 'flex-start', | ||||
|     WebkitBoxOrient: 'vertical', | ||||
|     wordBreak: 'break-word', | ||||
| })); | ||||
| export const StyledCardTitle = styled('h3')<{ lines?: number }>( | ||||
|     ({ theme, lines = 2 }) => ({ | ||||
|         fontWeight: theme.typography.fontWeightRegular, | ||||
|         fontSize: theme.typography.body1.fontSize, | ||||
|         lineClamp: `${lines}`, | ||||
|         WebkitLineClamp: lines, | ||||
|         lineHeight: '1.2', | ||||
|         display: '-webkit-box', | ||||
|         boxOrient: 'vertical', | ||||
|         textOverflow: 'ellipsis', | ||||
|         overflow: 'hidden', | ||||
|         alignItems: 'flex-start', | ||||
|         WebkitBoxOrient: 'vertical', | ||||
|         wordBreak: 'break-word', | ||||
|     }), | ||||
| ); | ||||
| 
 | ||||
| export const StyledBox = styled(Box)(() => ({ | ||||
|     ...flexRow, | ||||
|  | ||||
| @ -1,32 +1,37 @@ | ||||
| import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { | ||||
|     StyledProjectCard, | ||||
|     StyledDivHeader, | ||||
|     StyledBox, | ||||
|     StyledCardTitle, | ||||
|     StyledDivInfo, | ||||
|     StyledParagraphInfo, | ||||
|     StyledProjectCardBody, | ||||
|     StyledIconBox, | ||||
| } from './ProjectCard.styles'; | ||||
| import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter'; | ||||
| import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge'; | ||||
| import type { ProjectSchemaOwners } from 'openapi'; | ||||
| 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 { | ||||
|     name: string; | ||||
|     featureCount: number; | ||||
|     health: number; | ||||
|     memberCount?: number; | ||||
|     id: string; | ||||
|     onHover: () => void; | ||||
|     favorite?: boolean; | ||||
|     mode: string; | ||||
|     owners?: ProjectSchemaOwners; | ||||
| } | ||||
| const StyledUpdated = styled('span')(({ theme }) => ({ | ||||
|     color: theme.palette.text.secondary, | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
| })); | ||||
| 
 | ||||
| const StyledCount = styled('strong')(({ theme }) => ({ | ||||
|     fontWeight: theme.typography.fontWeightMedium, | ||||
| })); | ||||
| 
 | ||||
| const StyledInfo = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     marginTop: theme.spacing(1), | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
|     alignItems: 'flex-end', | ||||
| })); | ||||
| 
 | ||||
| export const ProjectCard = ({ | ||||
|     name, | ||||
| @ -38,48 +43,50 @@ export const ProjectCard = ({ | ||||
|     mode, | ||||
|     favorite = false, | ||||
|     owners, | ||||
| }: IProjectCardProps) => ( | ||||
|     lastUpdatedAt, | ||||
|     lastReportedFlagUsage, | ||||
| }: IProjectCard) => ( | ||||
|     <StyledProjectCard onMouseEnter={onHover}> | ||||
|         <StyledProjectCardBody> | ||||
|             <StyledDivHeader> | ||||
|                 <StyledIconBox> | ||||
|                     <ProjectIcon /> | ||||
|                 </StyledIconBox> | ||||
|                 <StyledBox data-loading> | ||||
|                     <StyledCardTitle>{name}</StyledCardTitle> | ||||
|                 </StyledBox> | ||||
|                 <Box | ||||
|                     data-loading | ||||
|                     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} /> | ||||
|                 <FavoriteAction id={id} isFavorite={favorite} /> | ||||
|             </StyledDivHeader> | ||||
|             <StyledDivInfo> | ||||
|             <StyledInfo> | ||||
|                 <div> | ||||
|                     <StyledParagraphInfo data-loading> | ||||
|                         {featureCount} | ||||
|                     </StyledParagraphInfo> | ||||
|                     <p data-loading>{featureCount === 1 ? 'flag' : 'flags'}</p> | ||||
|                     <div> | ||||
|                         <StyledCount>{featureCount}</StyledCount> flag | ||||
|                         {featureCount === 1 ? '' : 's'} | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <StyledCount>{health}%</StyledCount> health | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <ConditionallyRender | ||||
|                     condition={id !== DEFAULT_PROJECT_ID} | ||||
|                     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> | ||||
|                 <ProjectLastSeen date={lastReportedFlagUsage} /> | ||||
|             </StyledInfo> | ||||
|         </StyledProjectCardBody> | ||||
|         <ProjectCardFooter id={id} owners={owners}> | ||||
|             <FavoriteAction id={id} isFavorite={favorite} /> | ||||
|         </ProjectCardFooter> | ||||
|         <ProjectCardFooter id={id} owners={owners} memberCount={memberCount} /> | ||||
|     </StyledProjectCard> | ||||
| ); | ||||
|  | ||||
| @ -3,15 +3,21 @@ import type { FC } from 'react'; | ||||
| import { Box, styled } from '@mui/material'; | ||||
| import { | ||||
|     type IProjectOwnersProps, | ||||
|     ProjectOwners, | ||||
| } from '../ProjectOwners/ProjectOwners'; | ||||
|     ProjectOwners as LegacyProjectOwners, | ||||
| } 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 { | ||||
|     id: string; | ||||
|     id?: string; | ||||
|     isFavorite?: boolean; | ||||
|     children?: React.ReactNode; | ||||
|     disabled?: boolean; | ||||
|     owners: IProjectOwnersProps['owners']; | ||||
|     memberCount?: number; | ||||
| } | ||||
| 
 | ||||
| const StyledFooter = styled(Box)<{ disabled: boolean }>( | ||||
| @ -28,14 +34,29 @@ const StyledFooter = styled(Box)<{ disabled: boolean }>( | ||||
| ); | ||||
| 
 | ||||
| export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({ | ||||
|     id, | ||||
|     children, | ||||
|     owners, | ||||
|     disabled = false, | ||||
|     memberCount, | ||||
| }) => { | ||||
|     const projectListImprovementsEnabled = useUiFlag('projectListImprovements'); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledFooter disabled={disabled}> | ||||
|             <ProjectOwners owners={owners} /> | ||||
|             {children} | ||||
|             <ConditionallyRender | ||||
|                 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> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -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 LockIcon from '@mui/icons-material/Lock'; | ||||
| import ProtectedProjectIcon from '@mui/icons-material/LockOutlined'; | ||||
| import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; | ||||
| import PrivateProjectIcon from '@mui/icons-material/VisibilityOffOutlined'; | ||||
| import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { styled } from '@mui/material'; | ||||
| 
 | ||||
| interface IProjectModeBadgeProps { | ||||
|     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 }) => { | ||||
|     const projectListImprovementsEnabled = useUiFlag('projectListImprovements'); | ||||
| 
 | ||||
|     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 ( | ||||
|             <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." | ||||
| @ -21,6 +45,18 @@ export const ProjectModeBadge: FC<IProjectModeBadgeProps> = ({ mode }) => { | ||||
|     } | ||||
| 
 | ||||
|     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 ( | ||||
|             <HtmlTooltip | ||||
|                 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { ProjectCard as LegacyProjectCard } from '../ProjectCard/LegacyProjectCard'; | ||||
| import { ProjectCard as NewProjectCard } from '../ProjectCard/ProjectCard'; | ||||
| 
 | ||||
| import type { IProjectCard } from 'interfaces/project'; | ||||
| import loadingData from './loadingData'; | ||||
| import { TablePlaceholder } from 'component/common/Table'; | ||||
| import { styled, Typography } from '@mui/material'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| 
 | ||||
| const StyledGridContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'grid', | ||||
| @ -24,25 +26,30 @@ const StyledCardLink = styled(Link)(({ theme }) => ({ | ||||
|     pointer: 'cursor', | ||||
| })); | ||||
| 
 | ||||
| type ProjectGroupProps<T extends { id: string } = IProjectCard> = { | ||||
| type ProjectGroupProps = { | ||||
|     sectionTitle?: string; | ||||
|     projects: T[]; | ||||
|     projects: IProjectCard[]; | ||||
|     loading: boolean; | ||||
|     searchValue: string; | ||||
|     placeholder?: string; | ||||
|     ProjectCardComponent?: ComponentType<T & any>; | ||||
|     ProjectCardComponent?: ComponentType<IProjectCard & any>; | ||||
|     link?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export const ProjectGroup = <T extends { id: string }>({ | ||||
| export const ProjectGroup = ({ | ||||
|     sectionTitle, | ||||
|     projects, | ||||
|     loading, | ||||
|     searchValue, | ||||
|     placeholder = 'No projects available.', | ||||
|     ProjectCardComponent = LegacyProjectCard, | ||||
|     ProjectCardComponent, | ||||
|     link = true, | ||||
| }: ProjectGroupProps<T>) => { | ||||
| }: ProjectGroupProps) => { | ||||
|     const projectListImprovementsEnabled = useUiFlag('projectListImprovements'); | ||||
|     const ProjectCard = | ||||
|         ProjectCardComponent ?? | ||||
|         (projectListImprovementsEnabled ? NewProjectCard : LegacyProjectCard); | ||||
| 
 | ||||
|     return ( | ||||
|         <article> | ||||
|             <ConditionallyRender | ||||
| @ -82,9 +89,9 @@ export const ProjectGroup = <T extends { id: string }>({ | ||||
|                                 <> | ||||
|                                     {loadingData.map( | ||||
|                                         (project: IProjectCard) => ( | ||||
|                                             <LegacyProjectCard | ||||
|                                             <ProjectCard | ||||
|                                                 data-loading | ||||
|                                                 onHover={() => {}} | ||||
|                                                 createdAt={project.createdAt} | ||||
|                                                 key={project.id} | ||||
|                                                 name={project.name} | ||||
|                                                 id={project.id} | ||||
| @ -99,21 +106,17 @@ export const ProjectGroup = <T extends { id: string }>({ | ||||
|                             )} | ||||
|                             elseShow={() => ( | ||||
|                                 <> | ||||
|                                     {projects.map((project: T) => | ||||
|                                     {projects.map((project) => | ||||
|                                         link ? ( | ||||
|                                             <StyledCardLink | ||||
|                                                 key={project.id} | ||||
|                                                 to={`/projects/${project.id}`} | ||||
|                                             > | ||||
|                                                 <ProjectCardComponent | ||||
|                                                     onHover={() => {}} | ||||
|                                                     {...project} | ||||
|                                                 /> | ||||
|                                                 <ProjectCard {...project} /> | ||||
|                                             </StyledCardLink> | ||||
|                                         ) : ( | ||||
|                                             <ProjectCardComponent | ||||
|                                             <ProjectCard | ||||
|                                                 key={project.id} | ||||
|                                                 onHover={() => {}} | ||||
|                                                 {...project} | ||||
|                                             /> | ||||
|                                         ), | ||||
|  | ||||
| @ -7,13 +7,16 @@ export interface IProjectCard { | ||||
|     name: string; | ||||
|     id: string; | ||||
|     createdAt: string; | ||||
|     health: number; | ||||
|     description: string; | ||||
|     featureCount: number; | ||||
|     mode: string; | ||||
|     health?: number; | ||||
|     description?: string; | ||||
|     featureCount?: number; | ||||
|     mode?: string; | ||||
|     memberCount?: number; | ||||
|     onHover?: () => void; | ||||
|     favorite?: boolean; | ||||
|     owners?: ProjectSchema['owners']; | ||||
|     lastUpdatedAt?: Date; | ||||
|     lastReportedFlagUsage?: Date; | ||||
| } | ||||
| 
 | ||||
| export type FeatureNamingType = { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user