mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
refactor: front end code pt II (#8444)
This PR continues the refactoring of the front end code for dashboards. The main points are: - Extracts the `ActionBox` component that we used in a lot of places. There were some minor differences between the various incarnations, so this also better aligns them. - Extract other components (`AskOwnerToAddYouToTheirProject`, `YourAdmins`) - Move the `NeutralCircleContainer` into `SharedComponents` - Delete the separate no content grid (this is now handled in projects instead) - extract my projects grid contents into a single function so that it's easier to understand what content you get for what states Here's all the states side by side: 
This commit is contained in:
parent
f5a2a18ffc
commit
e4cfb29adc
35
frontend/src/component/personalDashboard/ActionBox.tsx
Normal file
35
frontend/src/component/personalDashboard/ActionBox.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
|
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
|
const Container = styled('article')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(4, 2),
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(3),
|
||||||
|
flexDirection: 'column',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TitleContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: theme.spacing(1.75),
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string | ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionBox: FC<PropsWithChildren<Props>> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{title ? <TitleContainer>{title}</TitleContainer> : null}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { ActionBox } from './ActionBox';
|
||||||
|
import { NeutralCircleContainer } from './SharedComponents';
|
||||||
|
import type { PersonalDashboardSchemaProjectOwnersItem } from 'openapi';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners';
|
||||||
|
|
||||||
|
export const AskOwnerToAddYouToTheirProject: FC<{
|
||||||
|
owners: PersonalDashboardSchemaProjectOwnersItem[];
|
||||||
|
}> = ({ owners }) => {
|
||||||
|
return (
|
||||||
|
<ActionBox
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<NeutralCircleContainer>2</NeutralCircleContainer>
|
||||||
|
Ask a project owner to add you to their project
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{owners.length ? (
|
||||||
|
<>
|
||||||
|
<p>Project owners in Unleash:</p>
|
||||||
|
<AvatarGroupFromOwners users={owners} avatarLimit={9} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>There are no project owners in Unleash to ask for access.</p>
|
||||||
|
)}
|
||||||
|
</ActionBox>
|
||||||
|
);
|
||||||
|
};
|
@ -1,25 +1,9 @@
|
|||||||
import { Button, styled, Typography } from '@mui/material';
|
import { Button, styled, Typography } from '@mui/material';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { ActionBox } from './ActionBox';
|
||||||
const TitleContainer = styled('div')(({ theme }) => ({
|
import { Link } from 'react-router-dom';
|
||||||
display: 'flex',
|
import { NeutralCircleContainer } from './SharedComponents';
|
||||||
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 MainCircleContainer = styled(NeutralCircleContainer)(({ theme }) => ({
|
const MainCircleContainer = styled(NeutralCircleContainer)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.primary.main,
|
backgroundColor: theme.palette.primary.main,
|
||||||
@ -37,22 +21,18 @@ const SuccessContainer = styled('div')(({ theme }) => ({
|
|||||||
padding: theme.spacing(2, 2, 2, 2),
|
padding: theme.spacing(2, 2, 2, 2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ActionBox = styled('div')(({ theme }) => ({
|
|
||||||
flexBasis: '50%',
|
|
||||||
padding: theme.spacing(4, 2),
|
|
||||||
display: 'flex',
|
|
||||||
gap: theme.spacing(3),
|
|
||||||
flexDirection: 'column',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const CreateFlag: FC<{ project: string }> = ({ project }) => {
|
export const CreateFlag: FC<{ project: string }> = ({ project }) => {
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
return (
|
return (
|
||||||
<ActionBox data-loading>
|
<ActionBox
|
||||||
<TitleContainer>
|
data-loading
|
||||||
<NeutralCircleContainer>1</NeutralCircleContainer>
|
title={
|
||||||
Create a feature flag
|
<>
|
||||||
</TitleContainer>
|
<NeutralCircleContainer>1</NeutralCircleContainer>
|
||||||
|
Create a feature flag
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>The project currently holds no feature flags.</p>
|
<p>The project currently holds no feature flags.</p>
|
||||||
<p>Create one to get started.</p>
|
<p>Create one to get started.</p>
|
||||||
@ -78,11 +58,14 @@ export const CreateFlag: FC<{ project: string }> = ({ project }) => {
|
|||||||
|
|
||||||
export const ExistingFlag: FC<{ project: string }> = ({ project }) => {
|
export const ExistingFlag: FC<{ project: string }> = ({ project }) => {
|
||||||
return (
|
return (
|
||||||
<ActionBox>
|
<ActionBox
|
||||||
<TitleContainer>
|
title={
|
||||||
<MainCircleContainer>✓</MainCircleContainer>
|
<>
|
||||||
Create a feature flag
|
<MainCircleContainer>1</MainCircleContainer>
|
||||||
</TitleContainer>
|
Create a feature flag
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SuccessContainer>
|
<SuccessContainer>
|
||||||
<Typography fontWeight='bold' variant='body2'>
|
<Typography fontWeight='bold' variant='body2'>
|
||||||
You have created your first flag
|
You have created your first flag
|
||||||
@ -92,7 +75,11 @@ export const ExistingFlag: FC<{ project: string }> = ({ project }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</SuccessContainer>
|
</SuccessContainer>
|
||||||
<div>
|
<div>
|
||||||
<Button href={`projects/${project}`} variant='contained'>
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={`/projects/${project}`}
|
||||||
|
variant='contained'
|
||||||
|
>
|
||||||
Go to project
|
Go to project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -102,11 +89,15 @@ export const ExistingFlag: FC<{ project: string }> = ({ project }) => {
|
|||||||
|
|
||||||
export const ConnectSDK: FC<{ project: string }> = ({ project }) => {
|
export const ConnectSDK: FC<{ project: string }> = ({ project }) => {
|
||||||
return (
|
return (
|
||||||
<ActionBox data-loading>
|
<ActionBox
|
||||||
<TitleContainer>
|
data-loading
|
||||||
<NeutralCircleContainer>2</NeutralCircleContainer>
|
title={
|
||||||
Connect an SDK
|
<>
|
||||||
</TitleContainer>
|
<NeutralCircleContainer>2</NeutralCircleContainer>
|
||||||
|
Connect an SDK
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>Your project is not yet connected to any SDK.</p>
|
<p>Your project is not yet connected to any SDK.</p>
|
||||||
<p>
|
<p>
|
||||||
@ -115,7 +106,11 @@ export const ConnectSDK: FC<{ project: string }> = ({ project }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button href={`projects/${project}`} variant='contained'>
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={`/projects/${project}`}
|
||||||
|
variant='contained'
|
||||||
|
>
|
||||||
Go to project
|
Go to project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,165 +0,0 @@
|
|||||||
import { Typography, styled } from '@mui/material';
|
|
||||||
import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners';
|
|
||||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
|
||||||
import type { PersonalDashboardSchemaAdminsItem } from 'openapi/models/personalDashboardSchemaAdminsItem';
|
|
||||||
import type { PersonalDashboardSchemaProjectOwnersItem } from 'openapi/models/personalDashboardSchemaProjectOwnersItem';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
ContentGridContainer,
|
|
||||||
EmptyGridItem,
|
|
||||||
ProjectGrid,
|
|
||||||
GridItem,
|
|
||||||
} from './SharedComponents';
|
|
||||||
|
|
||||||
const PaddedEmptyGridItem = styled(EmptyGridItem)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(4),
|
|
||||||
}));
|
|
||||||
|
|
||||||
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: PersonalDashboardSchemaProjectOwnersItem[];
|
|
||||||
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>
|
|
||||||
<ProjectGrid>
|
|
||||||
<GridItem gridArea='projects'>
|
|
||||||
<GridContent>
|
|
||||||
<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>
|
|
||||||
</GridContent>
|
|
||||||
</GridItem>
|
|
||||||
<GridItem gridArea='box1'>
|
|
||||||
<GridContent>
|
|
||||||
<TitleContainer>
|
|
||||||
<NeutralCircleContainer>1</NeutralCircleContainer>
|
|
||||||
Contact Unleash admin
|
|
||||||
</TitleContainer>
|
|
||||||
<AdminListRendered admins={admins} />
|
|
||||||
</GridContent>
|
|
||||||
</GridItem>
|
|
||||||
<GridItem gridArea='box2'>
|
|
||||||
<GridContent>
|
|
||||||
<TitleContainer>
|
|
||||||
<NeutralCircleContainer>2</NeutralCircleContainer>
|
|
||||||
Ask a project owner to add you to their project
|
|
||||||
</TitleContainer>
|
|
||||||
<BoxMainContent>
|
|
||||||
{owners.length ? (
|
|
||||||
<>
|
|
||||||
<p>Project owners in Unleash:</p>
|
|
||||||
<AvatarGroupFromOwners
|
|
||||||
users={owners}
|
|
||||||
avatarLimit={9}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
There are no project owners in Unleash to
|
|
||||||
ask for access.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</BoxMainContent>
|
|
||||||
</GridContent>
|
|
||||||
</GridItem>
|
|
||||||
<EmptyGridItem />
|
|
||||||
<PaddedEmptyGridItem gridArea='owners' />
|
|
||||||
</ProjectGrid>
|
|
||||||
</ContentGridContainer>
|
|
||||||
);
|
|
||||||
};
|
|
@ -5,6 +5,7 @@ import { UserAvatar } from '../common/UserAvatar/UserAvatar';
|
|||||||
import { Typography, styled } from '@mui/material';
|
import { Typography, styled } from '@mui/material';
|
||||||
import { formatDateYMDHM } from 'utils/formatDate';
|
import { formatDateYMDHM } from 'utils/formatDate';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { ActionBox } from './ActionBox';
|
||||||
|
|
||||||
const Events = styled('ul')(({ theme }) => ({
|
const Events = styled('ul')(({ theme }) => ({
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@ -33,13 +34,6 @@ const TitleContainer = styled('div')(({ theme }) => ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ActionBox = styled('article')(({ theme }) => ({
|
|
||||||
padding: theme.spacing(0, 2),
|
|
||||||
display: 'flex',
|
|
||||||
gap: theme.spacing(3),
|
|
||||||
flexDirection: 'column',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Timestamp = styled('time')(({ theme }) => ({
|
const Timestamp = styled('time')(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.typography.fontSize,
|
fontSize: theme.typography.fontSize,
|
||||||
@ -55,8 +49,8 @@ export const LatestProjectEvents: FC<{
|
|||||||
}> = ({ latestEvents }) => {
|
}> = ({ latestEvents }) => {
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
return (
|
return (
|
||||||
<ActionBox>
|
<ActionBox
|
||||||
<TitleContainer>
|
title={
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
@ -65,7 +59,8 @@ export const LatestProjectEvents: FC<{
|
|||||||
>
|
>
|
||||||
Latest events
|
Latest events
|
||||||
</Typography>
|
</Typography>
|
||||||
</TitleContainer>
|
}
|
||||||
|
>
|
||||||
<Events>
|
<Events>
|
||||||
{latestEvents.map((event) => {
|
{latestEvents.map((event) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
Typography,
|
Typography,
|
||||||
@ -12,10 +11,11 @@ import { ProjectSetupComplete } from './ProjectSetupComplete';
|
|||||||
import { ConnectSDK, CreateFlag, ExistingFlag } from './ConnectSDK';
|
import { ConnectSDK, CreateFlag, ExistingFlag } from './ConnectSDK';
|
||||||
import { LatestProjectEvents } from './LatestProjectEvents';
|
import { LatestProjectEvents } from './LatestProjectEvents';
|
||||||
import { RoleAndOwnerInfo } from './RoleAndOwnerInfo';
|
import { RoleAndOwnerInfo } from './RoleAndOwnerInfo';
|
||||||
import { forwardRef, useEffect, useRef, type FC } from 'react';
|
import { type ReactNode, forwardRef, useEffect, useRef, type FC } from 'react';
|
||||||
import type {
|
import type {
|
||||||
PersonalDashboardProjectDetailsSchema,
|
PersonalDashboardProjectDetailsSchema,
|
||||||
PersonalDashboardSchemaAdminsItem,
|
PersonalDashboardSchemaAdminsItem,
|
||||||
|
PersonalDashboardSchemaProjectOwnersItem,
|
||||||
PersonalDashboardSchemaProjectsItem,
|
PersonalDashboardSchemaProjectsItem,
|
||||||
} from '../../openapi';
|
} from '../../openapi';
|
||||||
import {
|
import {
|
||||||
@ -31,6 +31,10 @@ import {
|
|||||||
} from './SharedComponents';
|
} from './SharedComponents';
|
||||||
import { ContactAdmins, DataError } from './ProjectDetailsError';
|
import { ContactAdmins, DataError } from './ProjectDetailsError';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { ActionBox } from './ActionBox';
|
||||||
|
import { NoProjectsContactAdmin } from './NoProjectsContactAdmin';
|
||||||
|
import { AskOwnerToAddYouToTheirProject } from './AskOwnerToAddYouToTheirProject';
|
||||||
|
|
||||||
const ActiveProjectDetails: FC<{
|
const ActiveProjectDetails: FC<{
|
||||||
project: PersonalDashboardSchemaProjectsItem;
|
project: PersonalDashboardSchemaProjectsItem;
|
||||||
@ -98,7 +102,7 @@ const ProjectListItem: FC<{
|
|||||||
<StyledCardTitle>{project.name}</StyledCardTitle>
|
<StyledCardTitle>{project.name}</StyledCardTitle>
|
||||||
<IconButton
|
<IconButton
|
||||||
component={Link}
|
component={Link}
|
||||||
href={`projects/${project.id}`}
|
to={`/projects/${project.id}`}
|
||||||
size='small'
|
size='small'
|
||||||
sx={{ ml: 'auto' }}
|
sx={{ ml: 'auto' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -118,6 +122,8 @@ const ProjectListItem: FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MyProjectsState = 'no projects' | 'projects' | 'projects with error';
|
||||||
|
|
||||||
export const MyProjects = forwardRef<
|
export const MyProjects = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{
|
{
|
||||||
@ -126,6 +132,7 @@ export const MyProjects = forwardRef<
|
|||||||
activeProject: string;
|
activeProject: string;
|
||||||
setActiveProject: (project: string) => void;
|
setActiveProject: (project: string) => void;
|
||||||
admins: PersonalDashboardSchemaAdminsItem[];
|
admins: PersonalDashboardSchemaAdminsItem[];
|
||||||
|
owners: PersonalDashboardSchemaProjectOwnersItem[];
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
@ -135,9 +142,16 @@ export const MyProjects = forwardRef<
|
|||||||
setActiveProject,
|
setActiveProject,
|
||||||
activeProject,
|
activeProject,
|
||||||
admins,
|
admins,
|
||||||
|
owners,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
const state: MyProjectsState = projects.length
|
||||||
|
? personalDashboardProjectDetails
|
||||||
|
? 'projects'
|
||||||
|
: 'projects with error'
|
||||||
|
: 'no projects';
|
||||||
|
|
||||||
const activeProjectStage =
|
const activeProjectStage =
|
||||||
personalDashboardProjectDetails?.onboardingStatus.status ??
|
personalDashboardProjectDetails?.onboardingStatus.status ??
|
||||||
'loading';
|
'loading';
|
||||||
@ -145,77 +159,132 @@ export const MyProjects = forwardRef<
|
|||||||
activeProjectStage === 'onboarding-started' ||
|
activeProjectStage === 'onboarding-started' ||
|
||||||
activeProjectStage === 'first-flag-created';
|
activeProjectStage === 'first-flag-created';
|
||||||
|
|
||||||
const error = personalDashboardProjectDetails === undefined;
|
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} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
const box1Content = () => {
|
case 'projects with error':
|
||||||
if (error) {
|
return {
|
||||||
return <DataError project={activeProject} />;
|
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} />,
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
case 'projects': {
|
||||||
activeProjectStage === 'onboarded' &&
|
const box1 = (() => {
|
||||||
personalDashboardProjectDetails
|
if (
|
||||||
) {
|
activeProjectStage === 'onboarded' &&
|
||||||
return (
|
personalDashboardProjectDetails
|
||||||
<ProjectSetupComplete
|
) {
|
||||||
project={activeProject}
|
return (
|
||||||
insights={personalDashboardProjectDetails.insights}
|
<ProjectSetupComplete
|
||||||
/>
|
project={activeProject}
|
||||||
);
|
insights={
|
||||||
} else if (
|
personalDashboardProjectDetails.insights
|
||||||
activeProjectStage === 'onboarding-started' ||
|
}
|
||||||
activeProjectStage === 'loading'
|
/>
|
||||||
) {
|
);
|
||||||
return <CreateFlag project={activeProject} />;
|
} else if (
|
||||||
} else if (activeProjectStage === 'first-flag-created') {
|
activeProjectStage === 'onboarding-started' ||
|
||||||
return <ExistingFlag project={activeProject} />;
|
activeProjectStage === 'loading'
|
||||||
}
|
) {
|
||||||
};
|
return <CreateFlag project={activeProject} />;
|
||||||
|
} else if (
|
||||||
const box2Content = () => {
|
activeProjectStage === 'first-flag-created'
|
||||||
if (error) {
|
) {
|
||||||
return <ContactAdmins admins={admins} />;
|
return <ExistingFlag project={activeProject} />;
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
activeProjectStage === 'onboarded' &&
|
|
||||||
personalDashboardProjectDetails
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<LatestProjectEvents
|
|
||||||
latestEvents={
|
|
||||||
personalDashboardProjectDetails.latestEvents
|
|
||||||
}
|
}
|
||||||
/>
|
})();
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setupIncomplete || activeProjectStage === 'loading') {
|
const box2 = (() => {
|
||||||
return <ConnectSDK project={activeProject} />;
|
if (
|
||||||
|
activeProjectStage === 'onboarded' &&
|
||||||
|
personalDashboardProjectDetails
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<LatestProjectEvents
|
||||||
|
latestEvents={
|
||||||
|
personalDashboardProjectDetails.latestEvents
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
setupIncomplete ||
|
||||||
|
activeProjectStage === 'loading'
|
||||||
|
) {
|
||||||
|
return <ConnectSDK project={activeProject} />;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: (
|
||||||
|
<StyledList>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<ProjectListItem
|
||||||
|
key={project.id}
|
||||||
|
project={project}
|
||||||
|
selected={project.id === activeProject}
|
||||||
|
onClick={() =>
|
||||||
|
setActiveProject(project.id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledList>
|
||||||
|
),
|
||||||
|
box1,
|
||||||
|
box2,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { list, box1, box2 } = getGridContents();
|
||||||
return (
|
return (
|
||||||
<ContentGridContainer ref={ref}>
|
<ContentGridContainer ref={ref}>
|
||||||
<ProjectGrid>
|
<ProjectGrid>
|
||||||
<SpacedGridItem gridArea='projects'>
|
<SpacedGridItem gridArea='projects'>{list}</SpacedGridItem>
|
||||||
<StyledList>
|
<SpacedGridItem gridArea='box1'>{box1}</SpacedGridItem>
|
||||||
{projects.map((project) => (
|
<SpacedGridItem gridArea='box2'>{box2}</SpacedGridItem>
|
||||||
<ProjectListItem
|
|
||||||
key={project.id}
|
|
||||||
project={project}
|
|
||||||
selected={project.id === activeProject}
|
|
||||||
onClick={() => setActiveProject(project.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StyledList>
|
|
||||||
</SpacedGridItem>
|
|
||||||
<SpacedGridItem gridArea='box1'>
|
|
||||||
{box1Content()}
|
|
||||||
</SpacedGridItem>
|
|
||||||
<SpacedGridItem gridArea='box2'>
|
|
||||||
{box2Content()}
|
|
||||||
</SpacedGridItem>
|
|
||||||
<EmptyGridItem />
|
<EmptyGridItem />
|
||||||
<GridItem gridArea='owners'>
|
<GridItem gridArea='owners'>
|
||||||
<RoleAndOwnerInfo
|
<RoleAndOwnerInfo
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { ActionBox } from './ActionBox';
|
||||||
|
import { YourAdmins } from './YourAdmins';
|
||||||
|
import { NeutralCircleContainer } from './SharedComponents';
|
||||||
|
import type { PersonalDashboardSchemaAdminsItem } from 'openapi';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
export const NoProjectsContactAdmin: FC<{
|
||||||
|
admins: PersonalDashboardSchemaAdminsItem[];
|
||||||
|
}> = ({ admins }) => {
|
||||||
|
return (
|
||||||
|
<ActionBox
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<NeutralCircleContainer>1</NeutralCircleContainer>
|
||||||
|
Contact Unleash admin
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<YourAdmins admins={admins} />
|
||||||
|
</ActionBox>
|
||||||
|
);
|
||||||
|
};
|
@ -13,7 +13,6 @@ import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/use
|
|||||||
import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails';
|
import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails';
|
||||||
import useLoading from '../../hooks/useLoading';
|
import useLoading from '../../hooks/useLoading';
|
||||||
import { MyProjects } from './MyProjects';
|
import { MyProjects } from './MyProjects';
|
||||||
import { ContentGridNoProjects } from './ContentGridNoProjects';
|
|
||||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
|
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
|
||||||
@ -165,23 +164,17 @@ export const PersonalDashboard = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</StyledAccordionSummary>
|
</StyledAccordionSummary>
|
||||||
<StyledAccordionDetails>
|
<StyledAccordionDetails>
|
||||||
{noProjects && personalDashboard ? (
|
<MyProjects
|
||||||
<ContentGridNoProjects
|
owners={personalDashboard?.projectOwners ?? []}
|
||||||
owners={personalDashboard.projectOwners}
|
admins={personalDashboard?.admins ?? []}
|
||||||
admins={personalDashboard.admins}
|
ref={projectStageRef}
|
||||||
/>
|
projects={projects}
|
||||||
) : (
|
activeProject={activeProject || ''}
|
||||||
<MyProjects
|
setActiveProject={setActiveProject}
|
||||||
admins={personalDashboard?.admins ?? []}
|
personalDashboardProjectDetails={
|
||||||
ref={projectStageRef}
|
personalDashboardProjectDetails
|
||||||
projects={projects}
|
}
|
||||||
activeProject={activeProject || ''}
|
/>
|
||||||
setActiveProject={setActiveProject}
|
|
||||||
personalDashboardProjectDetails={
|
|
||||||
personalDashboardProjectDetails
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledAccordionDetails>
|
</StyledAccordionDetails>
|
||||||
</SectionAccordion>
|
</SectionAccordion>
|
||||||
|
|
||||||
|
@ -1,32 +1,14 @@
|
|||||||
import { styled } from '@mui/material';
|
|
||||||
import type { PersonalDashboardSchemaAdminsItem } from 'openapi';
|
import type { PersonalDashboardSchemaAdminsItem } from 'openapi';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { AdminListRendered } from './ContentGridNoProjects';
|
import { YourAdmins } from './YourAdmins';
|
||||||
|
import { ActionBox } from './ActionBox';
|
||||||
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 }) => {
|
export const DataError: FC<{ project: string }> = ({ project }) => {
|
||||||
return (
|
return (
|
||||||
<ActionBox data-loading>
|
<ActionBox
|
||||||
<TitleContainer>
|
data-loading
|
||||||
Couldn't fetch data for project "{project}".
|
title={`Couldn't fetch data for project "${project}".`}
|
||||||
</TitleContainer>
|
>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The API request to get data for this project returned with an
|
The API request to get data for this project returned with an
|
||||||
error.
|
error.
|
||||||
@ -44,11 +26,8 @@ export const ContactAdmins: FC<{
|
|||||||
admins: PersonalDashboardSchemaAdminsItem[];
|
admins: PersonalDashboardSchemaAdminsItem[];
|
||||||
}> = ({ admins }) => {
|
}> = ({ admins }) => {
|
||||||
return (
|
return (
|
||||||
<ActionBox>
|
<ActionBox title='Consider contacting one of your Unleash admins for help.'>
|
||||||
<TitleContainer>
|
<YourAdmins admins={admins} />
|
||||||
Consider contacting one of your Unleash admins for help.
|
|
||||||
</TitleContainer>
|
|
||||||
<AdminListRendered admins={admins} />
|
|
||||||
</ActionBox>
|
</ActionBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,20 +3,7 @@ import type { FC } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
||||||
import type { PersonalDashboardProjectDetailsSchemaInsights } from '../../openapi';
|
import type { PersonalDashboardProjectDetailsSchemaInsights } from '../../openapi';
|
||||||
|
import { ActionBox } from './ActionBox';
|
||||||
const TitleContainer = styled('div')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
alignItems: 'center',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ActionBox = styled('article')(({ theme }) => ({
|
|
||||||
padding: theme.spacing(0, 2),
|
|
||||||
display: 'flex',
|
|
||||||
gap: theme.spacing(3),
|
|
||||||
flexDirection: 'column',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const PercentageScore = styled('span')(({ theme }) => ({
|
const PercentageScore = styled('span')(({ theme }) => ({
|
||||||
fontWeight: theme.typography.fontWeightBold,
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
@ -145,14 +132,16 @@ export const ProjectSetupComplete: FC<{
|
|||||||
const projectHealthTrend = determineProjectHealthTrend(insights);
|
const projectHealthTrend = determineProjectHealthTrend(insights);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionBox>
|
<ActionBox
|
||||||
<TitleContainer>
|
title={
|
||||||
<Lightbulb color='primary' />
|
<>
|
||||||
<Typography sx={{ fontWeight: 'bold' }} component='h4'>
|
<Lightbulb color='primary' />
|
||||||
Project health
|
<Typography sx={{ fontWeight: 'bold' }} component='h4'>
|
||||||
</Typography>
|
Project health
|
||||||
</TitleContainer>
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ProjectHealthMessage
|
<ProjectHealthMessage
|
||||||
trend={projectHealthTrend}
|
trend={projectHealthTrend}
|
||||||
insights={insights}
|
insights={insights}
|
||||||
|
@ -130,3 +130,13 @@ export const StyledCardTitle = styled('div')<{ lines?: number }>(
|
|||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const NeutralCircleContainer = styled('span')(({ theme }) => ({
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: theme.palette.neutral.border,
|
||||||
|
borderRadius: '50%',
|
||||||
|
}));
|
||||||
|
60
frontend/src/component/personalDashboard/YourAdmins.tsx
Normal file
60
frontend/src/component/personalDashboard/YourAdmins.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||||
|
import { Typography, styled } from '@mui/material';
|
||||||
|
import type { PersonalDashboardSchemaAdminsItem } from 'openapi';
|
||||||
|
|
||||||
|
const StyledList = styled('ul')(({ theme }) => ({
|
||||||
|
padding: 0,
|
||||||
|
'li + li': {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledListItem = styled('li')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'row',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Wrapper = styled('article')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const YourAdmins: React.FC<{
|
||||||
|
admins: PersonalDashboardSchemaAdminsItem[];
|
||||||
|
}> = ({ admins }) => {
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
{admins.length ? (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Your Unleash administrator
|
||||||
|
{admins.length > 1 ? 's are' : ' is'}:
|
||||||
|
</p>
|
||||||
|
<StyledList>
|
||||||
|
{admins.map((admin) => {
|
||||||
|
return (
|
||||||
|
<StyledListItem key={admin.id}>
|
||||||
|
<UserAvatar
|
||||||
|
sx={{
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
user={admin}
|
||||||
|
/>
|
||||||
|
<Typography>
|
||||||
|
{admin.name ||
|
||||||
|
admin.username ||
|
||||||
|
admin.email}
|
||||||
|
</Typography>
|
||||||
|
</StyledListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledList>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>You have no Unleash administrators to contact.</p>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user