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>
+    );
+};