diff --git a/frontend/src/component/personalDashboard/ActionBox.tsx b/frontend/src/component/personalDashboard/ActionBox.tsx new file mode 100644 index 0000000000..eb9bb2be45 --- /dev/null +++ b/frontend/src/component/personalDashboard/ActionBox.tsx @@ -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> = ({ + title, + children, +}) => { + return ( + + {title ? {title} : null} + + {children} + + ); +}; diff --git a/frontend/src/component/personalDashboard/AskOwnerToAddYouToTheirProject.tsx b/frontend/src/component/personalDashboard/AskOwnerToAddYouToTheirProject.tsx new file mode 100644 index 0000000000..501ea855cf --- /dev/null +++ b/frontend/src/component/personalDashboard/AskOwnerToAddYouToTheirProject.tsx @@ -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 ( + + 2 + Ask a project owner to add you to their project + + } + > + {owners.length ? ( + <> +

Project owners in Unleash:

+ + + ) : ( +

There are no project owners in Unleash to ask for access.

+ )} +
+ ); +}; diff --git a/frontend/src/component/personalDashboard/ConnectSDK.tsx b/frontend/src/component/personalDashboard/ConnectSDK.tsx index 99e0149e9e..6729fafd0a 100644 --- a/frontend/src/component/personalDashboard/ConnectSDK.tsx +++ b/frontend/src/component/personalDashboard/ConnectSDK.tsx @@ -1,25 +1,9 @@ import { Button, styled, Typography } from '@mui/material'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import type { FC } from 'react'; - -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%', -})); +import { ActionBox } from './ActionBox'; +import { Link } from 'react-router-dom'; +import { NeutralCircleContainer } from './SharedComponents'; const MainCircleContainer = styled(NeutralCircleContainer)(({ theme }) => ({ backgroundColor: theme.palette.primary.main, @@ -37,22 +21,18 @@ const SuccessContainer = styled('div')(({ theme }) => ({ 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 }) => { const { trackEvent } = usePlausibleTracker(); return ( - - - 1 - Create a feature flag - + + 1 + Create a feature flag + + } + >

The project currently holds no feature flags.

Create one to get started.

@@ -78,11 +58,14 @@ export const CreateFlag: FC<{ project: string }> = ({ project }) => { export const ExistingFlag: FC<{ project: string }> = ({ project }) => { return ( - - - - Create a feature flag - + + 1 + Create a feature flag + + } + > You have created your first flag @@ -92,7 +75,11 @@ export const ExistingFlag: FC<{ project: string }> = ({ project }) => {
-
@@ -102,11 +89,15 @@ export const ExistingFlag: FC<{ project: string }> = ({ project }) => { export const ConnectSDK: FC<{ project: string }> = ({ project }) => { return ( - - - 2 - Connect an SDK - + + 2 + Connect an SDK + + } + >

Your project is not yet connected to any SDK.

@@ -115,7 +106,11 @@ export const ConnectSDK: FC<{ project: string }> = ({ project }) => {

-
diff --git a/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx b/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx deleted file mode 100644 index 68041240dc..0000000000 --- a/frontend/src/component/personalDashboard/ContentGridNoProjects.tsx +++ /dev/null @@ -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> = ({ - admins, -}) => { - return ( - - {admins.length ? ( - <> -

- Your Unleash administrator - {admins.length > 1 ? 's are' : ' is'}: -

- - {admins.map((admin) => { - return ( - - - - {admin.name || - admin.username || - admin.email} - - - ); - })} - - - ) : ( -

You have no Unleash administrators to contact.

- )} -
- ); -}; - -export const ContentGridNoProjects: React.FC = ({ owners, admins }) => { - return ( - - - - - - 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. - - - - - - - 1 - Contact Unleash admin - - - - - - - - 2 - Ask a project owner to add you to their project - - - {owners.length ? ( - <> -

Project owners in Unleash:

- - - ) : ( -

- There are no project owners in Unleash to - ask for access. -

- )} -
-
-
- - -
-
- ); -}; diff --git a/frontend/src/component/personalDashboard/LatestProjectEvents.tsx b/frontend/src/component/personalDashboard/LatestProjectEvents.tsx index fdb04de313..3f91aa2352 100644 --- a/frontend/src/component/personalDashboard/LatestProjectEvents.tsx +++ b/frontend/src/component/personalDashboard/LatestProjectEvents.tsx @@ -5,6 +5,7 @@ import { UserAvatar } from '../common/UserAvatar/UserAvatar'; import { Typography, styled } from '@mui/material'; import { formatDateYMDHM } from 'utils/formatDate'; import { useLocationSettings } from 'hooks/useLocationSettings'; +import { ActionBox } from './ActionBox'; const Events = styled('ul')(({ theme }) => ({ padding: 0, @@ -33,13 +34,6 @@ const TitleContainer = styled('div')(({ theme }) => ({ 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 }) => ({ color: theme.palette.text.secondary, fontSize: theme.typography.fontSize, @@ -55,8 +49,8 @@ export const LatestProjectEvents: FC<{ }> = ({ latestEvents }) => { const { locationSettings } = useLocationSettings(); return ( - - + Latest events - + } + > {latestEvents.map((event) => { return ( diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index 3a2d20c1be..d509a93c37 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -1,7 +1,6 @@ import { Box, IconButton, - Link, ListItem, ListItemButton, Typography, @@ -12,10 +11,11 @@ import { ProjectSetupComplete } from './ProjectSetupComplete'; import { ConnectSDK, CreateFlag, ExistingFlag } from './ConnectSDK'; import { LatestProjectEvents } from './LatestProjectEvents'; import { RoleAndOwnerInfo } from './RoleAndOwnerInfo'; -import { forwardRef, useEffect, useRef, type FC } from 'react'; +import { type ReactNode, forwardRef, useEffect, useRef, type FC } from 'react'; import type { PersonalDashboardProjectDetailsSchema, PersonalDashboardSchemaAdminsItem, + PersonalDashboardSchemaProjectOwnersItem, PersonalDashboardSchemaProjectsItem, } from '../../openapi'; import { @@ -31,6 +31,10 @@ import { } from './SharedComponents'; 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'; const ActiveProjectDetails: FC<{ project: PersonalDashboardSchemaProjectsItem; @@ -98,7 +102,7 @@ const ProjectListItem: FC<{ {project.name} { @@ -118,6 +122,8 @@ const ProjectListItem: FC<{ ); }; +type MyProjectsState = 'no projects' | 'projects' | 'projects with error'; + export const MyProjects = forwardRef< HTMLDivElement, { @@ -126,6 +132,7 @@ export const MyProjects = forwardRef< activeProject: string; setActiveProject: (project: string) => void; admins: PersonalDashboardSchemaAdminsItem[]; + owners: PersonalDashboardSchemaProjectOwnersItem[]; } >( ( @@ -135,9 +142,16 @@ export const MyProjects = forwardRef< setActiveProject, activeProject, admins, + owners, }, ref, ) => { + const state: MyProjectsState = projects.length + ? personalDashboardProjectDetails + ? 'projects' + : 'projects with error' + : 'no projects'; + const activeProjectStage = personalDashboardProjectDetails?.onboardingStatus.status ?? 'loading'; @@ -145,77 +159,132 @@ export const MyProjects = forwardRef< activeProjectStage === 'onboarding-started' || activeProjectStage === 'first-flag-created'; - const error = personalDashboardProjectDetails === undefined; + 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 box1Content = () => { - if (error) { - return ; - } + case 'projects with error': + return { + list: ( + + {projects.map((project) => ( + + setActiveProject(project.id) + } + /> + ))} + + ), + box1: , + box2: , + }; - if ( - activeProjectStage === 'onboarded' && - personalDashboardProjectDetails - ) { - return ( - - ); - } else if ( - activeProjectStage === 'onboarding-started' || - activeProjectStage === 'loading' - ) { - return ; - } else if (activeProjectStage === 'first-flag-created') { - return ; - } - }; - - const box2Content = () => { - if (error) { - return ; - } - - if ( - activeProjectStage === 'onboarded' && - personalDashboardProjectDetails - ) { - return ( - { + if ( + activeProjectStage === 'onboarded' && + personalDashboardProjectDetails + ) { + return ( + + ); + } else if ( + activeProjectStage === 'onboarding-started' || + activeProjectStage === 'loading' + ) { + return ; + } else if ( + activeProjectStage === 'first-flag-created' + ) { + return ; } - /> - ); - } + })(); - if (setupIncomplete || activeProjectStage === 'loading') { - return ; + const box2 = (() => { + if ( + activeProjectStage === 'onboarded' && + personalDashboardProjectDetails + ) { + return ( + + ); + } else if ( + setupIncomplete || + activeProjectStage === 'loading' + ) { + return ; + } + })(); + + return { + list: ( + + {projects.map((project) => ( + + setActiveProject(project.id) + } + /> + ))} + + ), + box1, + box2, + }; + } } }; + const { list, box1, box2 } = getGridContents(); return ( - - - {projects.map((project) => ( - setActiveProject(project.id)} - /> - ))} - - - - {box1Content()} - - - {box2Content()} - + {list} + {box1} + {box2} = ({ admins }) => { + return ( + + 1 + Contact Unleash admin + + } + > + + + ); +}; diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index e61a05c3de..75a4046e69 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -13,7 +13,6 @@ import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/use import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails'; import useLoading from '../../hooks/useLoading'; import { MyProjects } from './MyProjects'; -import { ContentGridNoProjects } from './ContentGridNoProjects'; import ExpandMore from '@mui/icons-material/ExpandMore'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi'; @@ -165,23 +164,17 @@ export const PersonalDashboard = () => { - {noProjects && personalDashboard ? ( - - ) : ( - - )} + diff --git a/frontend/src/component/personalDashboard/ProjectDetailsError.tsx b/frontend/src/component/personalDashboard/ProjectDetailsError.tsx index 37a62743e8..22a3da5728 100644 --- a/frontend/src/component/personalDashboard/ProjectDetailsError.tsx +++ b/frontend/src/component/personalDashboard/ProjectDetailsError.tsx @@ -1,32 +1,14 @@ -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', -})); +import { YourAdmins } from './YourAdmins'; +import { ActionBox } from './ActionBox'; export const DataError: FC<{ project: string }> = ({ project }) => { return ( - - - Couldn't fetch data for project "{project}". - - +

The API request to get data for this project returned with an error. @@ -44,11 +26,8 @@ export const ContactAdmins: FC<{ admins: PersonalDashboardSchemaAdminsItem[]; }> = ({ admins }) => { return ( - - - Consider contacting one of your Unleash admins for help. - - + + ); }; diff --git a/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx b/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx index 58ab79f34b..db8be8b7ee 100644 --- a/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx +++ b/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx @@ -3,20 +3,7 @@ import type { FC } from 'react'; import { Link } from 'react-router-dom'; import Lightbulb from '@mui/icons-material/LightbulbOutlined'; import type { PersonalDashboardProjectDetailsSchemaInsights } from '../../openapi'; - -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', -})); +import { ActionBox } from './ActionBox'; const PercentageScore = styled('span')(({ theme }) => ({ fontWeight: theme.typography.fontWeightBold, @@ -145,14 +132,16 @@ export const ProjectSetupComplete: FC<{ const projectHealthTrend = determineProjectHealthTrend(insights); return ( - - - - - Project health - - - + + + + Project health + + + } + > ( 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%', +})); diff --git a/frontend/src/component/personalDashboard/YourAdmins.tsx b/frontend/src/component/personalDashboard/YourAdmins.tsx new file mode 100644 index 0000000000..d7371a8cf9 --- /dev/null +++ b/frontend/src/component/personalDashboard/YourAdmins.tsx @@ -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 ( + + {admins.length ? ( + <> +

+ Your Unleash administrator + {admins.length > 1 ? 's are' : ' is'}: +

+ + {admins.map((admin) => { + return ( + + + + {admin.name || + admin.username || + admin.email} + + + ); + })} + + + ) : ( +

You have no Unleash administrators to contact.

+ )} + + ); +};