diff --git a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx index 5ebdafc6d5..2510467f70 100644 --- a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx @@ -74,6 +74,11 @@ const TooltipContainer: FC<{ ); }; +const LineBox = styled(Box)({ + display: 'grid', + placeItems: 'center', +}); + export const FeatureEnvironmentSeen = ({ featureLastSeen, environments, @@ -91,9 +96,9 @@ export const FeatureEnvironmentSeen = ({ tooltip='No usage reported from connected applications' > - + - + ); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx index e02f419221..5b53229e52 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx @@ -12,7 +12,8 @@ export const FlagExposure: FC<{ project: string; flagName: string; onArchive: () => void; -}> = ({ project, flagName, onArchive }) => { + className?: string; +}> = ({ project, flagName, onArchive, className }) => { const { feature, refetchFeature } = useFeature(project, flagName); const lastSeenEnvironments: ILastSeenEnvironments[] = feature.environments?.map((env) => ({ @@ -27,7 +28,7 @@ export const FlagExposure: FC<{ useState(false); return ( - + = ({ owners, admins }) => { return ( - - My projects - - - Potential next steps - diff --git a/frontend/src/component/personalDashboard/FlagMetricsChart.tsx b/frontend/src/component/personalDashboard/FlagMetricsChart.tsx index 0bf4d30ec0..3e2b98eac9 100644 --- a/frontend/src/component/personalDashboard/FlagMetricsChart.tsx +++ b/frontend/src/component/personalDashboard/FlagMetricsChart.tsx @@ -23,6 +23,7 @@ import { createPlaceholderBarChartOptions, } from './createChartOptions'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; +import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure'; const defaultYes = [0, 14, 28, 21, 33, 31, 31, 22, 26, 37, 31, 14, 21, 14, 0]; @@ -140,7 +141,7 @@ const EnvironmentSelect: FC<{ ); }; -const MetricsSelectors = styled(Box)(({ theme }) => ({ +const ExposureAndSelectors = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'flex-end', gap: theme.spacing(2), @@ -154,9 +155,14 @@ const ChartContainer = styled('div')(({ theme }) => ({ alignItems: 'center', })); +const StyledExposure = styled(FlagExposure)({ + alignItems: 'center', +}); + export const FlagMetricsChart: FC<{ flag: { name: string; project: string }; -}> = ({ flag }) => { + onArchive: () => void; +}> = ({ flag, onArchive }) => { const [hoursBack, setHoursBack] = useState(48); const { environment, setEnvironment, activeEnvironments } = @@ -168,7 +174,12 @@ export const FlagMetricsChart: FC<{ return ( - + + {environment ? ( - + {noData ? ( diff --git a/frontend/src/component/personalDashboard/Grid.tsx b/frontend/src/component/personalDashboard/Grid.tsx index 2038a36d91..bce36d2e05 100644 --- a/frontend/src/component/personalDashboard/Grid.tsx +++ b/frontend/src/component/personalDashboard/Grid.tsx @@ -10,7 +10,6 @@ const ContentGrid = styled('article')(({ theme }) => { backgroundColor: theme.palette.divider, borderRadius: `${theme.shape.borderRadiusLarge}px`, overflow: 'hidden', - border: `0.5px solid ${theme.palette.divider}`, gap: `1px`, display: 'flex', flexFlow: 'column nowrap', @@ -41,7 +40,6 @@ export const ProjectGrid = styled(ContentGrid)( gridTemplateColumns: '1fr 1fr 1fr', display: 'grid', gridTemplateAreas: ` - "header header header" "projects box1 box2" ". owners owners" `, @@ -53,7 +51,6 @@ export const FlagGrid = styled(ContentGrid)( gridTemplateColumns: '1fr 1fr 1fr', display: 'grid', gridTemplateAreas: ` - "title lifecycle lifecycle" "flags chart chart" `, }), diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index 32ac8cb7cb..451fa1b147 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -189,9 +189,6 @@ export const MyProjects = forwardRef< return ( - - My projects - { }); }; -// @ts-ignore +// @ts-expect-error The return type here isn't correct, but it's not +// an issue for the tests. We just need to override it because it's +// not implemented in jsdom. HTMLCanvasElement.prototype.getContext = () => {}; //scrollIntoView is not implemented in jsdom @@ -134,7 +136,7 @@ test('Render personal dashboard for a long running project', async () => { await screen.findByText('projectName'); await screen.findByText('10'); // members await screen.findByText('100'); // features - await screen.findByText('80%'); // health + await screen.findAllByText('80%'); // health await screen.findByText('Project health'); await screen.findByText('70%'); // avg health past window diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index d0f4747e14..d5ba350b8d 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -1,5 +1,8 @@ import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; import { + Accordion, + AccordionDetails, + AccordionSummary, Button, IconButton, Link, @@ -19,7 +22,6 @@ import type { PersonalDashboardSchemaFlagsItem, PersonalDashboardSchemaProjectsItem, } from '../../openapi'; -import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure'; import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails'; import useLoading from '../../hooks/useLoading'; import { MyProjects } from './MyProjects'; @@ -28,16 +30,10 @@ import { FlagGrid, ListItemBox, listItemStyle, - GridItem, SpacedGridItem, } from './Grid'; import { ContentGridNoProjects } from './ContentGridNoProjects'; - -const ScreenExplanation = styled('div')(({ theme }) => ({ - marginBottom: theme.spacing(4), - display: 'flex', - alignItems: 'center', -})); +import ExpandMore from '@mui/icons-material/ExpandMore'; export const StyledCardTitle = styled('div')<{ lines?: number }>( ({ theme, lines = 2 }) => ({ @@ -102,6 +98,7 @@ const FlagListItem: FC<{ ); }; +// todo: move into own file const useDashboardState = ( projects: PersonalDashboardSchemaProjectsItem[], flags: PersonalDashboardSchemaFlagsItem[], @@ -109,11 +106,15 @@ const useDashboardState = ( type State = { activeProject: string | undefined; activeFlag: PersonalDashboardSchemaFlagsItem | undefined; + expandProjects: boolean; + expandFlags: boolean; }; - const defaultState = { + const defaultState: State = { activeProject: undefined, activeFlag: undefined, + expandProjects: true, + expandFlags: true, }; const [state, setState] = useLocalStorageState( @@ -121,11 +122,20 @@ const useDashboardState = ( defaultState, ); + const updateState = (newState: Partial) => + setState({ ...defaultState, ...state, ...newState }); + useEffect(() => { + const updates: Partial = {}; const setDefaultFlag = flags.length && (!state.activeFlag || !flags.some((flag) => flag.name === state.activeFlag?.name)); + + if (setDefaultFlag) { + updates.activeFlag = flags[0]; + } + const setDefaultProject = projects.length && (!state.activeProject || @@ -133,37 +143,48 @@ const useDashboardState = ( (project) => project.id === state.activeProject, )); - if (setDefaultFlag || setDefaultProject) { - setState({ - activeFlag: setDefaultFlag ? flags[0] : state.activeFlag, - activeProject: setDefaultProject - ? projects[0].id - : state.activeProject, - }); + if (setDefaultProject) { + updates.activeProject = projects[0].id; } - }); + + if (Object.keys(updates).length) { + updateState(updates); + } + }, [ + JSON.stringify(projects, null, 2), + JSON.stringify(flags, null, 2), + JSON.stringify(state, null, 2), + ]); const { activeFlag, activeProject } = state; const setActiveFlag = (flag: PersonalDashboardSchemaFlagsItem) => { - setState({ - ...state, + updateState({ activeFlag: flag, }); }; const setActiveProject = (projectId: string) => { - setState({ - ...state, + updateState({ activeProject: projectId, }); }; + const toggleSectionState = (section: 'flags' | 'projects') => { + const property = section === 'flags' ? 'expandFlags' : 'expandProjects'; + updateState({ + [property]: !(state[property] ?? true), + }); + }; + return { activeFlag, setActiveFlag, activeProject, setActiveProject, + expandFlags: state.expandFlags ?? true, + expandProjects: state.expandProjects ?? true, + toggleSectionState, }; }; @@ -173,13 +194,57 @@ const WelcomeSection = styled('div')(({ theme }) => ({ gap: theme.spacing(1), flexFlow: 'row wrap', alignItems: 'baseline', - marginBottom: theme.spacing(2), })); -const ViewKeyConceptsButton = styled(Button)(({ theme }) => ({ +const ViewKeyConceptsButton = styled(Button)({ fontWeight: 'normal', padding: 0, margin: 0, +}); + +const SectionAccordion = styled(Accordion)(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadiusMedium, + backgroundColor: theme.palette.background.paper, + boxShadow: 'none', + '& .expanded': { + '&:before': { + opacity: '0 !important', + }, + }, + + // add a top border to the region when the accordion is collapsed. + // This retains the border between the summary and the region + // during the collapsing animation + "[aria-expanded='false']+.MuiCollapse-root .MuiAccordion-region": { + borderTop: `1px solid ${theme.palette.divider}`, + }, + + overflow: 'hidden', +})); + +const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ + border: 'none', + padding: theme.spacing(2, 4), + margin: 0, + // increase specificity to override the default margin + '&>.MuiAccordionSummary-content.MuiAccordionSummary-content': { + margin: '0', + }, + "&[aria-expanded='true']": { + // only add the border when it's open + borderBottom: `1px solid ${theme.palette.divider}`, + }, +})); + +const StyledAccordionDetails = styled(AccordionDetails)({ + padding: 0, +}); + +const MainContent = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), })); export const PersonalDashboard = () => { @@ -187,32 +252,30 @@ export const PersonalDashboard = () => { const name = user?.name; - const { - personalDashboard, - refetch: refetchDashboard, - loading: personalDashboardLoading, - } = usePersonalDashboard(); + const { personalDashboard, refetch: refetchDashboard } = + usePersonalDashboard(); const projects = personalDashboard?.projects || []; - const { activeProject, setActiveProject, activeFlag, setActiveFlag } = - useDashboardState(projects, personalDashboard?.flags ?? []); + const { + activeProject, + setActiveProject, + activeFlag, + setActiveFlag, + toggleSectionState, + expandFlags, + expandProjects, + } = useDashboardState(projects, personalDashboard?.flags ?? []); const [welcomeDialog, setWelcomeDialog] = useLocalStorageState< 'open' | 'closed' >('welcome-dialog:v1', 'open'); - const { - personalDashboardProjectDetails, - loading: loadingDetails, - error: detailsError, - } = usePersonalDashboardProjectDetails(activeProject); + const { personalDashboardProjectDetails, error: detailsError } = + usePersonalDashboardProjectDetails(activeProject); const activeProjectStage = personalDashboardProjectDetails?.onboardingStatus.status ?? 'loading'; - const setupIncomplete = - activeProjectStage === 'onboarding-started' || - activeProjectStage === 'first-flag-created'; const noProjects = projects.length === 0; @@ -221,7 +284,7 @@ export const PersonalDashboard = () => { ); return ( -
+ Welcome {name} @@ -239,84 +302,113 @@ export const PersonalDashboard = () => { - {noProjects && personalDashboard ? ( - - ) : ( - toggleSectionState('projects')} + > + } - /> - )} + id='projects-panel-header' + aria-controls='projects-panel-content' + > + + My projects + + + + {noProjects && personalDashboard ? ( + + ) : ( + + )} + + - - - - My feature flags - - - {activeFlag ? ( - - ) : null} - - - {personalDashboard && - personalDashboard.flags.length > 0 ? ( - - {personalDashboard.flags.map((flag) => ( - setActiveFlag(flag)} + toggleSectionState('flags')} + > + + } + id='flags-panel-header' + aria-controls='flags-panel-content' + > + + My feature flags + + + + + + + {personalDashboard && + personalDashboard.flags.length > 0 ? ( + + {personalDashboard.flags.map((flag) => ( + + setActiveFlag(flag) + } + /> + ))} + + ) : ( + + You have not created or favorited any + feature flags. Once you do, they will + show up here. + + )} + + + + {activeFlag ? ( + - ))} - - ) : ( - - You have not created or favorited any feature - flags. Once you do, they will show up here. - - )} - - - - {activeFlag ? ( - - ) : ( - - )} - - - + ) : ( + + )} + + + + + setWelcomeDialog('closed')} /> -
+ ); };