Skip to content

Commit

Permalink
fix: handle loading states for project details for a single project (#…
Browse files Browse the repository at this point in the history
…8492)

This PR updates the use of references on the project details page to
handle the loading state for a single project.

Now, if a project is loading, it'll show skeleton loaders for the
relevant boxes:


![image](https://github.com/user-attachments/assets/a156cc88-e4bf-421a-8afe-2b46e26d5544)

I've also updated the state type we use for this to be more accurate. Shamelessly
stolen from Elm.

```ts
type RemoteData<T> = 
  | { state: 'error', error: Error } 
  | { state: 'loading' } 
  | { state: 'success', data: T } 
```

After refactoring: 

![image](https://github.com/user-attachments/assets/03d655de-1ab8-4289-9f0c-d158ede8e116)
  • Loading branch information
thomasheartman authored Oct 21, 2024
1 parent 9fecc02 commit a8206f5
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 193 deletions.
321 changes: 144 additions & 177 deletions frontend/src/component/personalDashboard/MyProjects.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
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';
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,
PersonalDashboardProjectDetailsSchemaRolesItem,
PersonalDashboardSchemaAdminsItem,
PersonalDashboardSchemaProjectOwnersItem,
PersonalDashboardSchemaProjectsItem,
Expand All @@ -33,6 +36,7 @@ import { ContactAdmins, DataError } from './ProjectDetailsError';
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';

Expand Down Expand Up @@ -69,6 +73,10 @@ const ActiveProjectDetails: FC<{
);
};

const SkeletonDiv = styled('div')({
height: '80%',
});

const ProjectListItem: FC<{
project: PersonalDashboardSchemaProjectsItem;
selected: boolean;
Expand Down Expand Up @@ -122,186 +130,145 @@ const ProjectListItem: FC<{
);
};

type MyProjectsState = 'no projects' | 'projects' | 'projects with error';

export const MyProjects = forwardRef<
HTMLDivElement,
{
projects: PersonalDashboardSchemaProjectsItem[];
personalDashboardProjectDetails?: PersonalDashboardProjectDetailsSchema;
activeProject: string;
setActiveProject: (project: string) => void;
admins: PersonalDashboardSchemaAdminsItem[];
owners: PersonalDashboardSchemaProjectOwnersItem[];
}
>(
(
{
projects,
personalDashboardProjectDetails,
setActiveProject,
activeProject,
admins,
owners,
},
ref,
) => {
const state: MyProjectsState = projects.length
? personalDashboardProjectDetails
? 'projects'
: 'projects with error'
: 'no projects';

const activeProjectStage =
personalDashboardProjectDetails?.onboardingStatus.status ??
'loading';
const setupIncomplete =
activeProjectStage === 'onboarding-started' ||
activeProjectStage === 'first-flag-created';

const getGridContents = (): {
list: ReactNode;
box1: ReactNode;
box2: ReactNode;
} => {
switch (state) {
case 'no projects':
return {
list: (
<ActionBox>
<Typography>
You don't currently have access to any
projects in the system.
</Typography>
<Typography>
To get started, you can{' '}
<Link to='/projects?create=true'>
create your own project
</Link>
. Alternatively, you can review the
available projects in the system and ask the
owner for access.
</Typography>
</ActionBox>
),
box1: <NoProjectsContactAdmin admins={admins} />,
box2: (
<AskOwnerToAddYouToTheirProject owners={owners} />
),
};
export const MyProjects: React.FC<{
projects: PersonalDashboardSchemaProjectsItem[];
personalDashboardProjectDetails: RemoteData<PersonalDashboardProjectDetailsSchema>;
activeProject: string;
setActiveProject: (project: string) => void;
admins: PersonalDashboardSchemaAdminsItem[];
owners: PersonalDashboardSchemaProjectOwnersItem[];
}> = ({
projects,
personalDashboardProjectDetails,
setActiveProject,
activeProject,
admins,
owners,
}) => {
const ref = useLoading(personalDashboardProjectDetails.state === 'loading');

case 'projects with error':
return {
list: (
<StyledList>
{projects.map((project) => (
<ProjectListItem
key={project.id}
project={project}
selected={project.id === activeProject}
onClick={() =>
setActiveProject(project.id)
}
/>
))}
</StyledList>
),
box1: <DataError project={activeProject} />,
box2: <ContactAdmins admins={admins} />,
};
const getGridContents = (): {
list: ReactNode;
box1: ReactNode;
box2: ReactNode;
} => {
if (projects.length === 0) {
return {
list: (
<ActionBox>
<Typography>
You don't currently have access to any projects in
the system.
</Typography>
<Typography>
To get started, you can{' '}
<Link to='/projects?create=true'>
create your own project
</Link>
. Alternatively, you can review the available
projects in the system and ask the owner for access.
</Typography>
</ActionBox>
),
box1: <NoProjectsContactAdmin admins={admins} />,
box2: <AskOwnerToAddYouToTheirProject owners={owners} />,
};
}

case 'projects': {
const box1 = (() => {
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 list = (
<StyledList>
{projects.map((project) => (
<ProjectListItem
key={project.id}
project={project}
selected={project.id === activeProject}
onClick={() => setActiveProject(project.id)}
/>
))}
</StyledList>
);

const box2 = (() => {
if (
activeProjectStage === 'onboarded' &&
personalDashboardProjectDetails
) {
return (
<LatestProjectEvents
latestEvents={
personalDashboardProjectDetails.latestEvents
}
/>
);
} else if (
setupIncomplete ||
activeProjectStage === 'loading'
) {
return <ConnectSDK project={activeProject} />;
}
})();
const [box1, box2] = (() => {
switch (personalDashboardProjectDetails.state) {
case 'success': {
const activeProjectStage =
personalDashboardProjectDetails.data.onboardingStatus
.status ?? 'loading';
const setupIncomplete =
activeProjectStage === 'onboarding-started' ||
activeProjectStage === 'first-flag-created';

return {
list: (
<StyledList>
{projects.map((project) => (
<ProjectListItem
key={project.id}
project={project}
selected={project.id === activeProject}
onClick={() =>
setActiveProject(project.id)
}
/>
))}
</StyledList>
),
box1,
box2,
};
if (activeProjectStage === 'onboarded') {
return [
<ProjectSetupComplete
project={activeProject}
insights={
personalDashboardProjectDetails.data
.insights
}
/>,
<LatestProjectEvents
latestEvents={
personalDashboardProjectDetails.data
.latestEvents
}
/>,
];
} else if (setupIncomplete) {
return [
<CreateFlag project={activeProject} />,
<ConnectSDK project={activeProject} />,
];
} else {
return [
<ExistingFlag project={activeProject} />,
<ConnectSDK project={activeProject} />,
];
}
}
case 'error':
return [
<DataError project={activeProject} />,
<ContactAdmins admins={admins} />,
];
default: // loading
return [
<SkeletonDiv data-loading />,
<SkeletonDiv data-loading />,
];
}
};
})();

const { list, box1, box2 } = getGridContents();
return (
<ContentGridContainer ref={ref}>
<ProjectGrid>
<SpacedGridItem gridArea='projects'>{list}</SpacedGridItem>
<SpacedGridItem gridArea='box1'>{box1}</SpacedGridItem>
<SpacedGridItem gridArea='box2'>{box2}</SpacedGridItem>
<EmptyGridItem />
<GridItem gridArea='owners'>
<RoleAndOwnerInfo
roles={
personalDashboardProjectDetails?.roles.map(
(role) => role.name,
) ?? []
}
owners={
personalDashboardProjectDetails?.owners ?? [
{ ownerType: 'user', name: '?' },
]
}
/>
</GridItem>
</ProjectGrid>
</ContentGridContainer>
);
},
);
return { list, box1, box2 };
};

const { list, box1, box2 } = getGridContents();
return (
<ContentGridContainer ref={ref}>
<ProjectGrid>
<SpacedGridItem gridArea='projects'>{list}</SpacedGridItem>
<SpacedGridItem gridArea='box1'>{box1}</SpacedGridItem>
<SpacedGridItem gridArea='box2'>{box2}</SpacedGridItem>
<EmptyGridItem />
<GridItem gridArea='owners'>
<RoleAndOwnerInfo
roles={
personalDashboardProjectDetails.state === 'success'
? personalDashboardProjectDetails.data.roles.map(
(
role: PersonalDashboardProjectDetailsSchemaRolesItem,
) => role.name,
)
: []
}
owners={
personalDashboardProjectDetails.state === 'success'
? personalDashboardProjectDetails.data.owners
: [{ ownerType: 'user', name: '?' }]
}
/>
</GridItem>
</ProjectGrid>
</ContentGridContainer>
);
};
Loading

0 comments on commit a8206f5

Please sign in to comment.