From 3bc9c9ae8ac70dca90362016108379aaaabd7cbd Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 21 Oct 2024 12:10:07 +0200 Subject: [PATCH 1/5] fix: handle loading states for project details for a single project --- .../personalDashboard/MyProjects.tsx | 24 +++++++++++++++---- .../personalDashboard/PersonalDashboard.tsx | 11 +++------ .../personalDashboard/ProjectDetailsError.tsx | 5 +--- .../personalDashboard/RoleAndOwnerInfo.tsx | 2 +- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index d509a93c37c3..06e9fd898c88 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -122,7 +122,10 @@ const ProjectListItem: FC<{ ); }; -type MyProjectsState = 'no projects' | 'projects' | 'projects with error'; +type MyProjectsState = + | 'no projects' + | 'projects' + | 'projects with error or loading'; export const MyProjects = forwardRef< HTMLDivElement, @@ -149,7 +152,7 @@ export const MyProjects = forwardRef< const state: MyProjectsState = projects.length ? personalDashboardProjectDetails ? 'projects' - : 'projects with error' + : 'projects with error or loading' : 'no projects'; const activeProjectStage = @@ -190,7 +193,7 @@ export const MyProjects = forwardRef< ), }; - case 'projects with error': + case 'projects with error or loading': return { list: ( @@ -206,8 +209,19 @@ export const MyProjects = forwardRef< ))} ), - box1: , - box2: , + box1: ( +
+ +
+ ), + box2: ( +
+ +
+ ), }; case 'projects': { diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index bba512e01923..a90bd95987e2 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -130,15 +130,10 @@ export const PersonalDashboard = () => { splash?.personalDashboardKeyConcepts ? 'closed' : 'open', ); - const { personalDashboardProjectDetails, error: detailsError } = + const { personalDashboardProjectDetails, loading: detailsLoading } = usePersonalDashboardProjectDetails(activeProject); - const activeProjectStage = - personalDashboardProjectDetails?.onboardingStatus.status ?? 'loading'; - - const projectStageRef = useLoading( - !detailsError && activeProjectStage === 'loading', - ); + const loadingProjectDetailsRef = useLoading(detailsLoading); return ( @@ -192,7 +187,7 @@ export const PersonalDashboard = () => { = ({ project }) => { return ( - +

The API request to get data for this project returned with an error. diff --git a/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx b/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx index fce80ebf17ed..f0b62053e608 100644 --- a/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx +++ b/frontend/src/component/personalDashboard/RoleAndOwnerInfo.tsx @@ -51,7 +51,7 @@ export const RoleAndOwnerInfo = ({ roles, owners }: Props) => { const firstRoles = roles.slice(0, 3); const extraRoles = roles.slice(3); return ( - + {roles.length > 0 ? ( <> From fac7b476cf3b00fcfbbeb2e789ea35f3f7ccc544 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 21 Oct 2024 13:50:35 +0200 Subject: [PATCH 2/5] fix: switch to remotedata --- .../personalDashboard/MyProjects.tsx | 235 ++++++++---------- .../personalDashboard/PersonalDashboard.tsx | 11 +- .../component/personalDashboard/RemoteData.ts | 29 +++ 3 files changed, 134 insertions(+), 141 deletions(-) create mode 100644 frontend/src/component/personalDashboard/RemoteData.ts diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index 06e9fd898c88..ec8b73cd22d2 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -1,9 +1,11 @@ +import type { RemoteData } from './RemoteData'; import { Box, IconButton, ListItem, ListItemButton, Typography, + styled, } from '@mui/material'; import { ProjectIcon } from '../common/ProjectIcon/ProjectIcon'; import LinkIcon from '@mui/icons-material/ArrowForward'; @@ -33,8 +35,7 @@ import { ContactAdmins, DataError } from './ProjectDetailsError'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { Link } from 'react-router-dom'; import { ActionBox } from './ActionBox'; -import { NoProjectsContactAdmin } from './NoProjectsContactAdmin'; -import { AskOwnerToAddYouToTheirProject } from './AskOwnerToAddYouToTheirProject'; +import useLoading from 'hooks/useLoading'; const ActiveProjectDetails: FC<{ project: PersonalDashboardSchemaProjectsItem; @@ -69,6 +70,10 @@ const ActiveProjectDetails: FC<{ ); }; +const SkeletonDiv = styled('div')({ + height: '80%', +}); + const ProjectListItem: FC<{ project: PersonalDashboardSchemaProjectsItem; selected: boolean; @@ -122,16 +127,11 @@ const ProjectListItem: FC<{ ); }; -type MyProjectsState = - | 'no projects' - | 'projects' - | 'projects with error or loading'; - export const MyProjects = forwardRef< HTMLDivElement, { projects: PersonalDashboardSchemaProjectsItem[]; - personalDashboardProjectDetails?: PersonalDashboardProjectDetailsSchema; + personalDashboardProjectDetails: RemoteData; activeProject: string; setActiveProject: (project: string) => void; admins: PersonalDashboardSchemaAdminsItem[]; @@ -147,149 +147,103 @@ export const MyProjects = forwardRef< admins, owners, }, - ref, + // ref, ) => { - const state: MyProjectsState = projects.length - ? personalDashboardProjectDetails - ? 'projects' - : 'projects with error or loading' - : 'no projects'; + const ref = useLoading( + personalDashboardProjectDetails.state === 'loading', + ); - const activeProjectStage = - personalDashboardProjectDetails?.onboardingStatus.status ?? - 'loading'; - const setupIncomplete = - activeProjectStage === 'onboarding-started' || - activeProjectStage === 'first-flag-created'; + // const state: MyProjectsState = projects.length + // ? personalDashboardProjectDetails + // ? 'projects' + // : 'projects with error or loading' + // : 'no projects'; const getGridContents = (): { list: ReactNode; box1: ReactNode; box2: ReactNode; } => { - switch (state) { - case 'no projects': - return { - list: ( - - - 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. - - - ), - box1: , - box2: ( - - ), - }; + const list = projects.length ? ( + + {projects.map((project) => ( + setActiveProject(project.id)} + /> + ))} + + ) : ( + + + 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. + + + ); - case 'projects with error or loading': - return { - list: ( - - {projects.map((project) => ( - - setActiveProject(project.id) - } - /> - ))} - - ), - box1: ( -

- -
- ), - box2: ( -
- -
- ), - }; + const [box1, box2] = (() => { + switch (personalDashboardProjectDetails.state) { + case 'success': { + const activeProjectStage = + personalDashboardProjectDetails.data + .onboardingStatus.status ?? 'loading'; + const setupIncomplete = + activeProjectStage === 'onboarding-started' || + activeProjectStage === 'first-flag-created'; - case 'projects': { - const box1 = (() => { - if ( - activeProjectStage === 'onboarded' && - personalDashboardProjectDetails - ) { - return ( + if (activeProjectStage === 'onboarded') { + return [ - ); - } else if ( - activeProjectStage === 'onboarding-started' || - activeProjectStage === 'loading' - ) { - return ; - } else if ( - activeProjectStage === 'first-flag-created' - ) { - return ; - } - })(); - - const box2 = (() => { - if ( - activeProjectStage === 'onboarded' && - personalDashboardProjectDetails - ) { - return ( + />, - ); - } else if ( - setupIncomplete || - activeProjectStage === 'loading' - ) { - return ; + />, + ]; + } else if (setupIncomplete) { + return [ + , + , + ]; + } else { + return [ + , + , + ]; } - })(); - - return { - list: ( - - {projects.map((project) => ( - - setActiveProject(project.id) - } - /> - ))} - - ), - box1, - box2, - }; + } + case 'error': + return [ + , + , + ]; + default: // loading + return [ + , + , + ]; } - } + })(); + + return { list, box1, box2 }; }; const { list, box1, box2 } = getGridContents(); @@ -303,14 +257,19 @@ export const MyProjects = forwardRef< role.name, - ) ?? [] + personalDashboardProjectDetails.state === + 'success' + ? personalDashboardProjectDetails.data.roles.map( + (role) => role.name, + ) + : [] } owners={ - personalDashboardProjectDetails?.owners ?? [ - { ownerType: 'user', name: '?' }, - ] + personalDashboardProjectDetails.state === + 'success' + ? personalDashboardProjectDetails.data + .owners + : [{ ownerType: 'user', name: '?' }] } /> diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index a90bd95987e2..6de3aa0f19d7 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -20,6 +20,7 @@ import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash'; import { useDashboardState } from './useDashboardState'; import { MyFlags } from './MyFlags'; import { usePageTitle } from 'hooks/usePageTitle'; +import { fromPersonalDashboardProjectDetailsOutput } from './RemoteData'; const WelcomeSection = styled('div')(({ theme }) => ({ display: 'flex', @@ -130,10 +131,14 @@ export const PersonalDashboard = () => { splash?.personalDashboardKeyConcepts ? 'closed' : 'open', ); - const { personalDashboardProjectDetails, loading: detailsLoading } = - usePersonalDashboardProjectDetails(activeProject); + const personalDashboardProjectDetails = + fromPersonalDashboardProjectDetailsOutput( + usePersonalDashboardProjectDetails(activeProject), + ); - const loadingProjectDetailsRef = useLoading(detailsLoading); + const loadingProjectDetailsRef = useLoading( + personalDashboardProjectDetails.state === 'loading', + ); return ( diff --git a/frontend/src/component/personalDashboard/RemoteData.ts b/frontend/src/component/personalDashboard/RemoteData.ts new file mode 100644 index 000000000000..1f79b0c778b5 --- /dev/null +++ b/frontend/src/component/personalDashboard/RemoteData.ts @@ -0,0 +1,29 @@ +import type { IPersonalDashboardProjectDetailsOutput } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails'; +import type { PersonalDashboardProjectDetailsSchema } from 'openapi'; + +type RemoteData = { refetch: () => void } & ( + | { state: 'error'; error: Error } + | { state: 'loading' } + | { state: 'success'; data: T } +); + +export const fromPersonalDashboardProjectDetailsOutput = ({ + personalDashboardProjectDetails, + error, +}: IPersonalDashboardProjectDetailsOutput): RemoteData => { + const converted = error + ? { + state: 'error', + error, + } + : personalDashboardProjectDetails + ? { + state: 'success', + data: personalDashboardProjectDetails, + } + : { + state: 'loading' as const, + }; + + return converted as RemoteData; +}; From ff57eae047a4a8e396b0ea1a3f98dfcaa69225eb Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 21 Oct 2024 13:54:29 +0200 Subject: [PATCH 3/5] fix: shift ref down a step --- .../personalDashboard/MyProjects.tsx | 265 ++++++++---------- .../personalDashboard/PersonalDashboard.tsx | 6 - 2 files changed, 123 insertions(+), 148 deletions(-) diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index ec8b73cd22d2..1959c1fa6ea0 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -13,7 +13,7 @@ import { ProjectSetupComplete } from './ProjectSetupComplete'; import { ConnectSDK, CreateFlag, ExistingFlag } from './ConnectSDK'; import { LatestProjectEvents } from './LatestProjectEvents'; import { RoleAndOwnerInfo } from './RoleAndOwnerInfo'; -import { type ReactNode, forwardRef, useEffect, useRef, type FC } from 'react'; +import { type ReactNode, useEffect, useRef, type FC } from 'react'; import type { PersonalDashboardProjectDetailsSchema, PersonalDashboardSchemaAdminsItem, @@ -127,154 +127,135 @@ const ProjectListItem: FC<{ ); }; -export const MyProjects = forwardRef< - HTMLDivElement, - { - projects: PersonalDashboardSchemaProjectsItem[]; - personalDashboardProjectDetails: RemoteData; - activeProject: string; - setActiveProject: (project: string) => void; - admins: PersonalDashboardSchemaAdminsItem[]; - owners: PersonalDashboardSchemaProjectOwnersItem[]; - } ->( - ( - { - projects, - personalDashboardProjectDetails, - setActiveProject, - activeProject, - admins, - owners, - }, - // ref, - ) => { - const ref = useLoading( - personalDashboardProjectDetails.state === 'loading', - ); - - // const state: MyProjectsState = projects.length - // ? personalDashboardProjectDetails - // ? 'projects' - // : 'projects with error or loading' - // : 'no projects'; +export const MyProjects: React.FC<{ + projects: PersonalDashboardSchemaProjectsItem[]; + personalDashboardProjectDetails: RemoteData; + activeProject: string; + setActiveProject: (project: string) => void; + admins: PersonalDashboardSchemaAdminsItem[]; + owners: PersonalDashboardSchemaProjectOwnersItem[]; +}> = ({ + projects, + personalDashboardProjectDetails, + setActiveProject, + activeProject, + admins, + owners, +}) => { + const ref = useLoading(personalDashboardProjectDetails.state === 'loading'); - const getGridContents = (): { - list: ReactNode; - box1: ReactNode; - box2: ReactNode; - } => { - const list = projects.length ? ( - - {projects.map((project) => ( - setActiveProject(project.id)} - /> - ))} - - ) : ( - - - 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. - - - ); + const getGridContents = (): { + list: ReactNode; + box1: ReactNode; + box2: ReactNode; + } => { + const list = projects.length ? ( + + {projects.map((project) => ( + setActiveProject(project.id)} + /> + ))} + + ) : ( + + + 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. + + + ); - const [box1, box2] = (() => { - switch (personalDashboardProjectDetails.state) { - case 'success': { - const activeProjectStage = - personalDashboardProjectDetails.data - .onboardingStatus.status ?? 'loading'; - const setupIncomplete = - activeProjectStage === 'onboarding-started' || - activeProjectStage === 'first-flag-created'; + const [box1, box2] = (() => { + switch (personalDashboardProjectDetails.state) { + case 'success': { + const activeProjectStage = + personalDashboardProjectDetails.data.onboardingStatus + .status ?? 'loading'; + const setupIncomplete = + activeProjectStage === 'onboarding-started' || + activeProjectStage === 'first-flag-created'; - if (activeProjectStage === 'onboarded') { - return [ - , - , - ]; - } else if (setupIncomplete) { - return [ - , - , - ]; - } else { - return [ - , - , - ]; - } - } - case 'error': + if (activeProjectStage === 'onboarded') { + return [ + , + , + ]; + } else if (setupIncomplete) { return [ - , - , + , + , ]; - default: // loading + } else { return [ - , - , + , + , ]; + } } - })(); + case 'error': + return [ + , + , + ]; + default: // loading + return [ + , + , + ]; + } + })(); - return { list, box1, box2 }; - }; + return { list, box1, box2 }; + }; - const { list, box1, box2 } = getGridContents(); - return ( - - - {list} - {box1} - {box2} - - - role.name, - ) - : [] - } - owners={ - personalDashboardProjectDetails.state === - 'success' - ? personalDashboardProjectDetails.data - .owners - : [{ ownerType: 'user', name: '?' }] - } - /> - - - - ); - }, -); + const { list, box1, box2 } = getGridContents(); + return ( + + + {list} + {box1} + {box2} + + + role.name, + ) + : [] + } + owners={ + personalDashboardProjectDetails.state === 'success' + ? personalDashboardProjectDetails.data.owners + : [{ ownerType: 'user', name: '?' }] + } + /> + + + + ); +}; diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index 6de3aa0f19d7..34ca3e8ab346 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -11,7 +11,6 @@ import { WelcomeDialog } from './WelcomeDialog'; import { useLocalStorageState } from 'hooks/useLocalStorageState'; import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard'; import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails'; -import useLoading from '../../hooks/useLoading'; import { MyProjects } from './MyProjects'; import ExpandMore from '@mui/icons-material/ExpandMore'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; @@ -136,10 +135,6 @@ export const PersonalDashboard = () => { usePersonalDashboardProjectDetails(activeProject), ); - const loadingProjectDetailsRef = useLoading( - personalDashboardProjectDetails.state === 'loading', - ); - return ( @@ -192,7 +187,6 @@ export const PersonalDashboard = () => { Date: Mon, 21 Oct 2024 14:02:17 +0200 Subject: [PATCH 4/5] fix: don't forget cases where you have no projects --- .../personalDashboard/MyProjects.tsx | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index 1959c1fa6ea0..402e12e1ac0d 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -16,6 +16,7 @@ import { RoleAndOwnerInfo } from './RoleAndOwnerInfo'; import { type ReactNode, useEffect, useRef, type FC } from 'react'; import type { PersonalDashboardProjectDetailsSchema, + PersonalDashboardProjectDetailsSchemaRolesItem, PersonalDashboardSchemaAdminsItem, PersonalDashboardSchemaProjectOwnersItem, PersonalDashboardSchemaProjectsItem, @@ -36,6 +37,8 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { Link } from 'react-router-dom'; import { ActionBox } from './ActionBox'; import useLoading from 'hooks/useLoading'; +import { NoProjectsContactAdmin } from './NoProjectsContactAdmin'; +import { AskOwnerToAddYouToTheirProject } from './AskOwnerToAddYouToTheirProject'; const ActiveProjectDetails: FC<{ project: PersonalDashboardSchemaProjectsItem; @@ -149,7 +152,30 @@ export const MyProjects: React.FC<{ box1: ReactNode; box2: ReactNode; } => { - const list = projects.length ? ( + if (projects.length === 0) { + return { + list: ( + + + 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. + + + ), + box1: , + box2: , + }; + } + + const list = ( {projects.map((project) => ( ))} - ) : ( - - - 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. - - ); const [box1, box2] = (() => { @@ -244,7 +255,9 @@ export const MyProjects: React.FC<{ roles={ personalDashboardProjectDetails.state === 'success' ? personalDashboardProjectDetails.data.roles.map( - (role) => role.name, + ( + role: PersonalDashboardProjectDetailsSchemaRolesItem, + ) => role.name, ) : [] } From 972179a3fd3118ca95dfe32d0c03bc49ff9606fd Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 21 Oct 2024 14:11:35 +0200 Subject: [PATCH 5/5] fix: rejig remote data --- frontend/src/component/personalDashboard/RemoteData.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/component/personalDashboard/RemoteData.ts b/frontend/src/component/personalDashboard/RemoteData.ts index 1f79b0c778b5..19edc74dfd5d 100644 --- a/frontend/src/component/personalDashboard/RemoteData.ts +++ b/frontend/src/component/personalDashboard/RemoteData.ts @@ -1,11 +1,10 @@ import type { IPersonalDashboardProjectDetailsOutput } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails'; import type { PersonalDashboardProjectDetailsSchema } from 'openapi'; -type RemoteData = { refetch: () => void } & ( +export type RemoteData = | { state: 'error'; error: Error } | { state: 'loading' } - | { state: 'success'; data: T } -); + | { state: 'success'; data: T }; export const fromPersonalDashboardProjectDetailsOutput = ({ personalDashboardProjectDetails,