From 23b040138127ef0a2dd3c0380a0316568cfb52a8 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 9 Oct 2024 14:25:58 +0200 Subject: [PATCH] feat: make panels collapsible (#8395) This PR makes the projects and flags panels collapsible. The panels are expanded by default and can be collapsed by clicking on the panel header. The state of the panels is saved in localstorage. As part of this, it also: - moves the flag exposure metrics next to the metric selectors - fixes the alignment of the "no exposure" line ![image](https://github.com/user-attachments/assets/b41ca808-f5f0-4e17-8bb1-b1388256354d) Line alignment: before: ![image](https://github.com/user-attachments/assets/119320d6-d39d-4c34-815a-8a25c6856ad6) after: ![image](https://github.com/user-attachments/assets/f5b0fe51-1cda-49f9-8b22-e03988429799) --- .../FeatureEnvironmentSeen.tsx | 9 +- .../FeatureLifecycle/FlagExposure.tsx | 5 +- .../ContentGridNoProjects.tsx | 6 - .../personalDashboard/FlagMetricsChart.tsx | 19 +- .../src/component/personalDashboard/Grid.tsx | 3 - .../personalDashboard/MyProjects.tsx | 3 - .../PersonalDashboard.test.tsx | 6 +- .../personalDashboard/PersonalDashboard.tsx | 312 ++++++++++++------ 8 files changed, 231 insertions(+), 132 deletions(-) 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')} /> -
+ ); };