From 6655b2d961a710b11b54376a426b1d4f56d4f11f Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 27 Sep 2024 10:41:25 +0200 Subject: [PATCH] feat: create page for when you have no projects (#8285) This adds a front end fallback screen for when you have no projects. ![image](https://github.com/user-attachments/assets/1e6e0a63-968a-43cf-84ee-9a67d9f0ca91) --- .../AvatarGroupFromOwners.tsx | 35 ++++ .../ContentGridNoProjects.tsx | 136 ++++++++++++++ .../personalDashboard/PersonalDashboard.tsx | 175 ++++++++++-------- .../personalDashboard/RoleAndOwnerInfo.tsx | 27 +-- .../ProjectOwners/ProjectOwners.tsx | 55 ++---- 5 files changed, 284 insertions(+), 144 deletions(-) create mode 100644 frontend/src/component/common/AvatarGroupFromOwners/AvatarGroupFromOwners.tsx create mode 100644 frontend/src/component/personalDashboard/ContentGridNoProjects.tsx diff --git a/frontend/src/component/common/AvatarGroupFromOwners/AvatarGroupFromOwners.tsx b/frontend/src/component/common/AvatarGroupFromOwners/AvatarGroupFromOwners.tsx new file mode 100644 index 0000000000..258def196f --- /dev/null +++ b/frontend/src/component/common/AvatarGroupFromOwners/AvatarGroupFromOwners.tsx @@ -0,0 +1,35 @@ +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import type { ProjectSchemaOwners } from 'openapi'; +import type { UserAvatar } from '../UserAvatar/UserAvatar'; +import { AvatarGroup } from '../AvatarGroup/AvatarGroup'; + +type Props = { + users: ProjectSchemaOwners; + avatarLimit?: number; + AvatarComponent?: typeof UserAvatar; + className?: string; +}; +export const AvatarGroupFromOwners: React.FC = ({ users, ...props }) => { + const { uiConfig } = useUiConfig(); + + const mapOwners = (owner: ProjectSchemaOwners[number]) => { + 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 mappedOwners = users.map(mapOwners); + return ; +}; diff --git a/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx b/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx new file mode 100644 index 0000000000..c358fd0d33 --- /dev/null +++ b/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx @@ -0,0 +1,136 @@ +import { Grid, Typography, styled } from '@mui/material'; +import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners'; +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; +import type { ProjectSchemaOwners } from 'openapi'; +import { Link } from 'react-router-dom'; + +const ContentGrid = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.background.paper, + borderRadius: `${theme.shape.borderRadiusLarge}px`, +})); + +const SpacedGridItem = styled(Grid)(({ theme }) => ({ + padding: theme.spacing(4), + border: `0.5px solid ${theme.palette.divider}`, +})); + +const TitleContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(2), + alignItems: 'center', + fontSize: theme.spacing(1.75), + fontWeight: 'bold', +})); + +const NeutralCircleContainer = styled('span')(({ theme }) => ({ + width: '28px', + height: '28px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.palette.neutral.border, + borderRadius: '50%', +})); + +const GridContent = styled('div')(({ theme }) => ({ + flexBasis: '50%', + padding: theme.spacing(4, 2), + display: 'flex', + gap: theme.spacing(3), + flexDirection: 'column', +})); + +const BoxMainContent = styled('article')(({ theme }) => ({ + display: 'flex', + flexFlow: 'column', + gap: theme.spacing(2), +})); + +const AdminList = styled('ul')(({ theme }) => ({ + padding: 0, + 'li + li': { + marginTop: theme.spacing(2), + }, +})); + +const AdminListItem = styled('li')(({ theme }) => ({ + display: 'flex', + flexFlow: 'row', + gap: theme.spacing(2), +})); + +type Props = { + owners: ProjectSchemaOwners; + admins: { name: string; imageUrl?: string }[]; +}; + +export const ContentGridNoProjects: React.FC = ({ owners, admins }) => { + return ( + + + My projects + + + Potential next steps + + + + + You don't currently have access to any projects in the + system. + + + To get started, you can{' '} + + create your own project + + . Alternatively, you can review the available projects + in the system and ask the owner for access. + + + + + + + 1 + Contact Unleash admin + + +

+ Your Unleash administrator + {admins.length > 1 ? 's are' : ' is'}: +

+ + {admins.map((admin) => ( + + + {admin.name} + + ))} + +
+
+
+ + + + 2 + Ask a project owner to add you to their project + + +

Project owners in Unleash:

+ +
+
+
+ + +
+ ); +}; diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index 874ef67ca1..6be71d281c 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -28,6 +28,7 @@ import type { } from '../../openapi'; import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure'; import { RoleAndOwnerInfo } from './RoleAndOwnerInfo'; +import { ContentGridNoProjects } from './ContentGridNoProjects'; const ScreenExplanation = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), @@ -197,6 +198,8 @@ export const PersonalDashboard = () => { 'seen' | 'not_seen' >('welcome-dialog:v1', 'not_seen'); + const noProjects = projects.length === 0; + return (
@@ -207,88 +210,102 @@ export const PersonalDashboard = () => { most of Unleash Your resources - - - My projects - - - Setup incomplete - - - + ) : ( + + + My projects + + - {projects.map((project) => { - return ( - - - setActiveProject(project.name) - } + Setup incomplete + + + + {projects.map((project) => { + return ( + - - - - {project.name} - - - + setActiveProject(project.name) + } + > + + + + {project.name} + + + + + + {project.name === activeProject ? ( + - - - {project.name === activeProject ? ( - - ) : null} - - - ); - })} - - - - {onboardingCompleted ? ( - - ) : activeProject ? ( - - ) : null} - - - {activeProject ? ( - - ) : null} - - - - {activeProject ? ( - - ) : null} - - + ) : null} + + + ); + })} + + + + {onboardingCompleted ? ( + + ) : activeProject ? ( + + ) : null} + + + {activeProject ? ( + + ) : null} + + + + {activeProject ? ( + + ) : null} + + + )} My feature flags diff --git a/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx b/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx index fedfe86bdf..f9d112cdc8 100644 --- a/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx +++ b/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx @@ -1,7 +1,6 @@ import { styled } from '@mui/material'; -import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup'; import { Badge } from 'component/common/Badge/Badge'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners'; import type { ProjectSchemaOwners } from 'openapi'; type Props = { @@ -22,29 +21,7 @@ const InfoSection = styled('div')(({ theme }) => ({ gap: theme.spacing(1), })); -const mapOwners = - (unleashUrl?: string) => (owner: ProjectSchemaOwners[number]) => { - 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: `${unleashUrl}/logo-unleash.png`, - }; - }; - export const RoleAndOwnerInfo = ({ roles, owners }: Props) => { - const { uiConfig } = useUiConfig(); - const mappedOwners = owners.map(mapOwners(uiConfig.unleashUrl)); return ( @@ -57,7 +34,7 @@ export const RoleAndOwnerInfo = ({ roles, owners }: Props) => { Project owner{owners.length > 1 ? 's' : ''} - + ); diff --git a/frontend/src/component/project/ProjectCard/ProjectCardFooter/ProjectOwners/ProjectOwners.tsx b/frontend/src/component/project/ProjectCard/ProjectCardFooter/ProjectOwners/ProjectOwners.tsx index d119ad3020..ba1405348c 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCardFooter/ProjectOwners/ProjectOwners.tsx +++ b/frontend/src/component/project/ProjectCard/ProjectCardFooter/ProjectOwners/ProjectOwners.tsx @@ -1,46 +1,14 @@ 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'; +import { AvatarComponent } from 'component/common/AvatarGroup/AvatarGroup'; +import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners'; 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, lineHeight: 1, @@ -87,15 +55,22 @@ const StyledAvatarComponent = styled(AvatarComponent)(({ theme }) => ({ cursor: 'default', })); -export const ProjectOwners: FC = ({ owners = [] }) => { - const ownersMap = useOwnersMap(); - const users = owners.map(ownersMap); +const getOwnerName = (owner?: ProjectSchemaOwners[number]) => { + switch (owner?.ownerType) { + case 'user': + case 'group': + return owner.name; + default: + return 'System'; + } +}; +export const ProjectOwners: FC = ({ owners = [] }) => { return ( - @@ -106,7 +81,7 @@ export const ProjectOwners: FC = ({ owners = [] }) => { Owner - {users[0]?.name} + {getOwnerName(owners[0])} }