From 2b668bc5c83ac2011830041ac2a1ede2e9ff4b19 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 10 Feb 2025 10:40:26 +0100 Subject: [PATCH] fix: open/close animation on personal dashboard is choppy (#9253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts each panel into its own component for the personal dashboard. This lets us use separate states for each panel, which in turn lets each panel change its open / close state without causing the other panels to re-render. When you have a lot of flags and/or projects, the list to render becomes very long, which causes performance problems, especially when you need to rerender both flags and projects and the timeline whenever one of them changes. The problems were especially noticeable in Firefox for me. Even with this, the event timeline is a little choppy. I suspect that's because of it might take a long time to paint? But we can look into that later. Also updates the dashboard state hook to let you only pass in the flags/projects you want. We could extract this into three different hooks that all use the same localhost key, but I'm not sure whether that's better or worse 🤷🏼 --- .../personalDashboard/PersonalDashboard.tsx | 295 ++++++++++-------- .../personalDashboard/useDashboardState.ts | 42 ++- 2 files changed, 177 insertions(+), 160 deletions(-) diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index e1979dfe78..74295e52e7 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -111,6 +111,158 @@ const AccordionSummarySubtitle = styled(Typography)(({ theme }) => ({ fontWeight: theme.typography.body2.fontWeight, })); +const EventTimelinePanel = () => { + const { toggleSectionState, expandTimeline } = useDashboardState(); + const { trackEvent } = usePlausibleTracker(); + + const signalsLink = '/integrations/signals'; + return ( + toggleSectionState('timeline')} + > + + } + id='timeline-panel-header' + aria-controls='timeline-panel-content' + > + + + Event timeline + + + Overview of recent activities across all projects in + Unleash. Make debugging easier and{' '} + { + trackEvent('event-timeline', { + props: { + eventType: 'signals clicked', + }, + }); + }} + > + include external signals + {' '} + to get a fuller overview. + + + + + + + + + + ); +}; + +const FlagPanel = () => { + const { personalDashboard, refetch: refetchDashboard } = + usePersonalDashboard(); + + const projects = personalDashboard?.projects || []; + + const { activeFlag, setActiveFlag, toggleSectionState, expandFlags } = + useDashboardState({ flags: personalDashboard?.flags ?? [] }); + + return ( + toggleSectionState('flags')} + > + } + id='flags-panel-header' + aria-controls='flags-panel-content' + > + + + My feature flags + + + Feature flags you have created or favorited + + + + + 0} + flagData={ + personalDashboard?.flags.length + ? { + state: 'flags' as const, + activeFlag, + flags: personalDashboard.flags, + } + : { state: 'no flags' as const } + } + setActiveFlag={setActiveFlag} + refetchDashboard={refetchDashboard} + /> + + + ); +}; + +const ProjectPanel = () => { + const { personalDashboard } = usePersonalDashboard(); + + const projects = personalDashboard?.projects || []; + + const { + activeProject, + setActiveProject, + toggleSectionState, + expandProjects, + } = useDashboardState({ projects }); + + const personalDashboardProjectDetails = + fromPersonalDashboardProjectDetailsOutput( + usePersonalDashboardProjectDetails(activeProject), + ); + + return ( + toggleSectionState('projects')} + > + + } + id='projects-panel-header' + aria-controls='projects-panel-content' + > + + My projects + + Favorite projects, projects you own, and projects you + are a member of + + + + + + + + ); +}; + export const PersonalDashboard = () => { const { user } = useAuthUser(); const { trackEvent } = usePlausibleTracker(); @@ -122,24 +274,6 @@ export const PersonalDashboard = () => { usePageTitle(name ? `Dashboard: ${name}` : 'Dashboard'); - const { personalDashboard, refetch: refetchDashboard } = - usePersonalDashboard(); - - const projects = personalDashboard?.projects || []; - - const { - activeProject, - setActiveProject, - activeFlag, - setActiveFlag, - toggleSectionState, - expandFlags, - expandProjects, - expandTimeline, - } = useDashboardState(projects, personalDashboard?.flags ?? []); - - const signalsLink = '/integrations/signals'; - const [welcomeDialog, setWelcomeDialog] = useLocalStorageState< 'open' | 'closed' >( @@ -147,11 +281,6 @@ export const PersonalDashboard = () => { splash?.personalDashboardKeyConcepts ? 'closed' : 'open', ); - const personalDashboardProjectDetails = - fromPersonalDashboardProjectDetailsOutput( - usePersonalDashboardProjectDetails(activeProject), - ); - useEffect(() => { trackEvent('personal-dashboard', { props: { @@ -188,122 +317,12 @@ export const PersonalDashboard = () => { - {showTimelinePanel && ( - toggleSectionState('timeline')} - > - - } - id='timeline-panel-header' - aria-controls='timeline-panel-content' - > - - - Event timeline - - - Overview of recent activities across all - projects in Unleash. Make debugging easier and{' '} - { - trackEvent('event-timeline', { - props: { - eventType: 'signals clicked', - }, - }); - }} - > - include external signals - {' '} - to get a fuller overview. - - - - - - - - - - )} - toggleSectionState('projects')} - > - - } - id='projects-panel-header' - aria-controls='projects-panel-content' - > - - - My projects - - - Favorite projects, projects you own, and projects - you are a member of - - - - - - - + {showTimelinePanel && } + + + + - toggleSectionState('flags')} - > - - } - id='flags-panel-header' - aria-controls='flags-panel-content' - > - - - My feature flags - - - Feature flags you have created or favorited - - - - - 0} - flagData={ - personalDashboard?.flags.length - ? { - state: 'flags' as const, - activeFlag, - flags: personalDashboard.flags, - } - : { state: 'no flags' as const } - } - setActiveFlag={setActiveFlag} - refetchDashboard={refetchDashboard} - /> - - { diff --git a/frontend/src/component/personalDashboard/useDashboardState.ts b/frontend/src/component/personalDashboard/useDashboardState.ts index c9652063e3..7d89cb43d2 100644 --- a/frontend/src/component/personalDashboard/useDashboardState.ts +++ b/frontend/src/component/personalDashboard/useDashboardState.ts @@ -5,10 +5,11 @@ import type { } from 'openapi'; import { useEffect } from 'react'; -export const useDashboardState = ( - projects: PersonalDashboardSchemaProjectsItem[], - flags: PersonalDashboardSchemaFlagsItem[], -) => { +type StateProps = { + projects?: PersonalDashboardSchemaProjectsItem[]; + flags?: PersonalDashboardSchemaFlagsItem[]; +}; +export const useDashboardState = (props?: StateProps) => { type State = { activeProject: string | undefined; activeFlag: PersonalDashboardSchemaFlagsItem | undefined; @@ -35,34 +36,31 @@ export const useDashboardState = ( useEffect(() => { const updates: Partial = {}; - const setDefaultFlag = - flags.length && - (!state.activeFlag || - !flags.some((flag) => flag.name === state.activeFlag?.name)); - if (setDefaultFlag) { - updates.activeFlag = flags[0]; + if ( + props?.flags?.length && + (!state.activeFlag || + !props.flags.some( + (flag) => flag.name === state.activeFlag?.name, + )) + ) { + updates.activeFlag = props.flags[0]; } - const setDefaultProject = - projects.length && + if ( + props?.projects?.length && (!state.activeProject || - !projects.some( + !props.projects.some( (project) => project.id === state.activeProject, - )); - - if (setDefaultProject) { - updates.activeProject = projects[0].id; + )) + ) { + updates.activeProject = props.projects[0].id; } if (Object.keys(updates).length) { updateState(updates); } - }, [ - JSON.stringify(projects), - JSON.stringify(flags), - JSON.stringify(state), - ]); + }, [JSON.stringify(props), JSON.stringify(state)]); const { activeFlag, activeProject } = state;