From 8a7bf865d3db6d23223268c934660abc5b2948e3 Mon Sep 17 00:00:00 2001 From: Thomas Heartman <thomas@getunleash.io> Date: Tue, 8 Oct 2024 08:46:14 +0200 Subject: [PATCH] fix: handle project fetching error (#8375) Work in progress --- .../ContentGridNoProjects.tsx | 73 +++--- .../personalDashboard/MyProjects.tsx | 217 +++++++++++------- .../personalDashboard/PersonalDashboard.tsx | 17 +- .../personalDashboard/ProjectDetailsError.tsx | 54 +++++ 4 files changed, 235 insertions(+), 126 deletions(-) create mode 100644 frontend/src/component/personalDashboard/ProjectDetailsError.tsx diff --git a/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx b/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx index 4a1f68c605..95cf9b6fbf 100644 --- a/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx +++ b/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx @@ -66,6 +66,44 @@ type Props = { admins: PersonalDashboardSchemaAdminsItem[]; }; +export const AdminListRendered: React.FC<Pick<Props, 'admins'>> = ({ + admins, +}) => { + return ( + <BoxMainContent> + {admins.length ? ( + <> + <p> + Your Unleash administrator + {admins.length > 1 ? 's are' : ' is'}: + </p> + <AdminList> + {admins.map((admin) => { + return ( + <AdminListItem key={admin.id}> + <UserAvatar + sx={{ + margin: 0, + }} + user={admin} + /> + <Typography> + {admin.name || + admin.username || + admin.email} + </Typography> + </AdminListItem> + ); + })} + </AdminList> + </> + ) : ( + <p>You have no Unleash administrators to contact.</p> + )} + </BoxMainContent> + ); +}; + export const ContentGridNoProjects: React.FC<Props> = ({ owners, admins }) => { return ( <ContentGridContainer> @@ -98,40 +136,7 @@ export const ContentGridNoProjects: React.FC<Props> = ({ owners, admins }) => { <NeutralCircleContainer>1</NeutralCircleContainer> Contact Unleash admin </TitleContainer> - <BoxMainContent> - {admins.length ? ( - <> - <p> - Your Unleash administrator - {admins.length > 1 ? 's are' : ' is'}: - </p> - <AdminList> - {admins.map((admin) => { - return ( - <AdminListItem key={admin.id}> - <UserAvatar - sx={{ - margin: 0, - }} - user={admin} - /> - <Typography> - {admin.name || - admin.username || - admin.email} - </Typography> - </AdminListItem> - ); - })} - </AdminList> - </> - ) : ( - <p> - You have no Unleash administrators to - contact. - </p> - )} - </BoxMainContent> + <AdminListRendered admins={admins} /> </GridContent> </GridItem> <GridItem gridArea='box2'> diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index 4b664221a4..469329b408 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -14,10 +14,11 @@ import { ProjectSetupComplete } from './ProjectSetupComplete'; import { ConnectSDK, CreateFlag, ExistingFlag } from './ConnectSDK'; import { LatestProjectEvents } from './LatestProjectEvents'; import { RoleAndOwnerInfo } from './RoleAndOwnerInfo'; -import { useEffect, useRef, type FC } from 'react'; +import { forwardRef, useEffect, useRef, type FC } from 'react'; import { StyledCardTitle } from './PersonalDashboard'; import type { PersonalDashboardProjectDetailsSchema, + PersonalDashboardSchemaAdminsItem, PersonalDashboardSchemaProjectsItem, } from '../../openapi'; import { @@ -29,6 +30,7 @@ import { GridItem, SpacedGridItem, } from './Grid'; +import { ContactAdmins, DataError } from './ProjectDetailsError'; const ActiveProjectDetails: FC<{ project: PersonalDashboardSchemaProjectsItem; @@ -108,98 +110,141 @@ const ProjectListItem: FC<{ ); }; -export const MyProjects: FC<{ - projects: PersonalDashboardSchemaProjectsItem[]; - personalDashboardProjectDetails?: PersonalDashboardProjectDetailsSchema; - activeProject: string; - setActiveProject: (project: string) => void; -}> = ({ - projects, - personalDashboardProjectDetails, - setActiveProject, - activeProject, -}) => { - const activeProjectStage = - personalDashboardProjectDetails?.onboardingStatus.status ?? 'loading'; - const setupIncomplete = - activeProjectStage === 'onboarding-started' || - activeProjectStage === 'first-flag-created'; +export const MyProjects = forwardRef< + HTMLDivElement, + { + projects: PersonalDashboardSchemaProjectsItem[]; + personalDashboardProjectDetails?: PersonalDashboardProjectDetailsSchema; + activeProject: string; + setActiveProject: (project: string) => void; + admins: PersonalDashboardSchemaAdminsItem[]; + } +>( + ( + { + projects, + personalDashboardProjectDetails, + setActiveProject, + activeProject, + admins, + }, + ref, + ) => { + const activeProjectStage = + personalDashboardProjectDetails?.onboardingStatus.status ?? + 'loading'; + const setupIncomplete = + activeProjectStage === 'onboarding-started' || + activeProjectStage === 'first-flag-created'; - return ( - <ContentGridContainer> - <ProjectGrid> - <GridItem gridArea='title'> - <Typography variant='h3'>My projects</Typography> - </GridItem> - <GridItem - gridArea='onboarding' - sx={{ - display: 'flex', - justifyContent: 'flex-end', - }} - > - {setupIncomplete ? ( - <Badge color='warning'>Setup incomplete</Badge> - ) : null} - </GridItem> - <SpacedGridItem gridArea='projects'> - <List - disablePadding={true} - sx={{ maxHeight: '400px', overflow: 'auto' }} + const error = personalDashboardProjectDetails === undefined; + + const box1Content = () => { + if (error) { + return <DataError project={activeProject} />; + } + + if ( + activeProjectStage === 'onboarded' && + personalDashboardProjectDetails + ) { + return ( + <ProjectSetupComplete + project={activeProject} + insights={personalDashboardProjectDetails.insights} + /> + ); + } else if ( + activeProjectStage === 'onboarding-started' || + activeProjectStage === 'loading' + ) { + return <CreateFlag project={activeProject} />; + } else if (activeProjectStage === 'first-flag-created') { + return <ExistingFlag project={activeProject} />; + } + }; + + const box2Content = () => { + if (error) { + return <ContactAdmins admins={admins} />; + } + + if ( + activeProjectStage === 'onboarded' && + personalDashboardProjectDetails + ) { + return ( + <LatestProjectEvents + latestEvents={ + personalDashboardProjectDetails.latestEvents + } + /> + ); + } + + if (setupIncomplete || activeProjectStage === 'loading') { + return <ConnectSDK project={activeProject} />; + } + }; + + return ( + <ContentGridContainer ref={ref}> + <ProjectGrid> + <GridItem gridArea='title'> + <Typography variant='h3'>My projects</Typography> + </GridItem> + <GridItem + gridArea='onboarding' + sx={{ + display: 'flex', + justifyContent: 'flex-end', + }} > - {projects.map((project) => { - return ( + {setupIncomplete ? ( + <Badge color='warning'>Setup incomplete</Badge> + ) : null} + {error ? ( + <Badge color='error'>Setup state unknown</Badge> + ) : null} + </GridItem> + <SpacedGridItem gridArea='projects'> + <List + disablePadding={true} + sx={{ maxHeight: '400px', overflow: 'auto' }} + > + {projects.map((project) => ( <ProjectListItem key={project.id} project={project} selected={project.id === activeProject} onClick={() => setActiveProject(project.id)} /> - ); - })} - </List> - </SpacedGridItem> - <SpacedGridItem gridArea='box1'> - {activeProjectStage === 'onboarded' && - personalDashboardProjectDetails ? ( - <ProjectSetupComplete - project={activeProject} - insights={personalDashboardProjectDetails.insights} - /> - ) : null} - {activeProjectStage === 'onboarding-started' || - activeProjectStage === 'loading' ? ( - <CreateFlag project={activeProject} /> - ) : null} - {activeProjectStage === 'first-flag-created' ? ( - <ExistingFlag project={activeProject} /> - ) : null} - </SpacedGridItem> - <SpacedGridItem gridArea='box2'> - {activeProjectStage === 'onboarded' && - personalDashboardProjectDetails ? ( - <LatestProjectEvents - latestEvents={ - personalDashboardProjectDetails.latestEvents + ))} + </List> + </SpacedGridItem> + <SpacedGridItem gridArea='box1'> + {box1Content()} + </SpacedGridItem> + <SpacedGridItem gridArea='box2'> + {box2Content()} + </SpacedGridItem> + <EmptyGridItem /> + <GridItem gridArea='owners'> + <RoleAndOwnerInfo + roles={ + personalDashboardProjectDetails?.roles.map( + (role) => role.name, + ) ?? [] + } + owners={ + personalDashboardProjectDetails?.owners ?? [ + { ownerType: 'user', name: '?' }, + ] } /> - ) : null} - {setupIncomplete || activeProjectStage === 'loading' ? ( - <ConnectSDK project={activeProject} /> - ) : null} - </SpacedGridItem> - <EmptyGridItem /> - <GridItem gridArea='owners'> - {personalDashboardProjectDetails ? ( - <RoleAndOwnerInfo - roles={personalDashboardProjectDetails.roles.map( - (role) => role.name, - )} - owners={personalDashboardProjectDetails.owners} - /> - ) : null} - </GridItem> - </ProjectGrid> - </ContentGridContainer> - ); -}; + </GridItem> + </ProjectGrid> + </ContentGridContainer> + ); + }, +); diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index a8bc0cf41b..7f6e713a5b 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -55,7 +55,6 @@ export const StyledCardTitle = styled('div')<{ lines?: number }>( wordBreak: 'break-word', }), ); - const FlagListItem: FC<{ flag: { name: string; project: string; type: string }; selected: boolean; @@ -167,7 +166,6 @@ const useDashboardState = ( setActiveProject, }; }; - export const PersonalDashboard = () => { const { user } = useAuthUser(); @@ -188,8 +186,11 @@ export const PersonalDashboard = () => { 'open' | 'closed' >('welcome-dialog:v1', 'open'); - const { personalDashboardProjectDetails, loading: loadingDetails } = - usePersonalDashboardProjectDetails(activeProject); + const { + personalDashboardProjectDetails, + loading: loadingDetails, + error: detailsError, + } = usePersonalDashboardProjectDetails(activeProject); const activeProjectStage = personalDashboardProjectDetails?.onboardingStatus.status ?? 'loading'; @@ -199,10 +200,12 @@ export const PersonalDashboard = () => { const noProjects = projects.length === 0; - const projectStageRef = useLoading(activeProjectStage === 'loading'); + const projectStageRef = useLoading( + !detailsError && activeProjectStage === 'loading', + ); return ( - <div ref={projectStageRef}> + <div> <Typography component='h2' variant='h2'> Welcome {name} </Typography> @@ -235,6 +238,8 @@ export const PersonalDashboard = () => { /> ) : ( <MyProjects + admins={personalDashboard?.admins ?? []} + ref={projectStageRef} projects={projects} activeProject={activeProject || ''} setActiveProject={setActiveProject} diff --git a/frontend/src/component/personalDashboard/ProjectDetailsError.tsx b/frontend/src/component/personalDashboard/ProjectDetailsError.tsx new file mode 100644 index 0000000000..37a62743e8 --- /dev/null +++ b/frontend/src/component/personalDashboard/ProjectDetailsError.tsx @@ -0,0 +1,54 @@ +import { styled } from '@mui/material'; +import type { PersonalDashboardSchemaAdminsItem } from 'openapi'; +import type { FC } from 'react'; +import { AdminListRendered } from './ContentGridNoProjects'; + +const TitleContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(2), + alignItems: 'center', + fontSize: theme.spacing(1.75), + fontWeight: 'bold', +})); + +const ActionBox = styled('div')(({ theme }) => ({ + flexBasis: '50%', + padding: theme.spacing(4, 2), + display: 'flex', + gap: theme.spacing(3), + flexDirection: 'column', +})); + +export const DataError: FC<{ project: string }> = ({ project }) => { + return ( + <ActionBox data-loading> + <TitleContainer> + Couldn't fetch data for project "{project}". + </TitleContainer> + + <p> + The API request to get data for this project returned with an + error. + </p> + <p> + This may be due to an intermittent error or it may be due to + issues with the project's id ("{project}"). You can try + reloading to see if that helps. + </p> + </ActionBox> + ); +}; + +export const ContactAdmins: FC<{ + admins: PersonalDashboardSchemaAdminsItem[]; +}> = ({ admins }) => { + return ( + <ActionBox> + <TitleContainer> + Consider contacting one of your Unleash admins for help. + </TitleContainer> + <AdminListRendered admins={admins} /> + </ActionBox> + ); +};