1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-19 00:15:43 +01:00

fix: open/close animation on personal dashboard is choppy (#9253)

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 🤷🏼
This commit is contained in:
Thomas Heartman 2025-02-10 10:40:26 +01:00 committed by GitHub
parent ccd8de6e74
commit 2b668bc5c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 177 additions and 160 deletions

View File

@ -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 (
<SectionAccordion
disableGutters
expanded={expandTimeline ?? false}
onChange={() => toggleSectionState('timeline')}
>
<StyledAccordionSummary
expandIcon={
<ExpandMore titleAccess='Toggle timeline section' />
}
id='timeline-panel-header'
aria-controls='timeline-panel-content'
>
<AccordionSummaryText>
<AccordionSummaryHeader>
Event timeline
</AccordionSummaryHeader>
<AccordionSummarySubtitle>
Overview of recent activities across all projects in
Unleash. Make debugging easier and{' '}
<Link
to={signalsLink}
onClick={() => {
trackEvent('event-timeline', {
props: {
eventType: 'signals clicked',
},
});
}}
>
include external signals
</Link>{' '}
to get a fuller overview.
</AccordionSummarySubtitle>
</AccordionSummaryText>
</StyledAccordionSummary>
<StyledAccordionDetails>
<AccordionContent>
<EventTimeline />
</AccordionContent>
</StyledAccordionDetails>
</SectionAccordion>
);
};
const FlagPanel = () => {
const { personalDashboard, refetch: refetchDashboard } =
usePersonalDashboard();
const projects = personalDashboard?.projects || [];
const { activeFlag, setActiveFlag, toggleSectionState, expandFlags } =
useDashboardState({ flags: personalDashboard?.flags ?? [] });
return (
<SectionAccordion
expanded={expandFlags ?? true}
onChange={() => toggleSectionState('flags')}
>
<StyledAccordionSummaryWithBorder
expandIcon={<ExpandMore titleAccess='Toggle flags section' />}
id='flags-panel-header'
aria-controls='flags-panel-content'
>
<AccordionSummaryText>
<AccordionSummaryHeader>
My feature flags
</AccordionSummaryHeader>
<AccordionSummarySubtitle>
Feature flags you have created or favorited
</AccordionSummarySubtitle>
</AccordionSummaryText>
</StyledAccordionSummaryWithBorder>
<StyledAccordionDetails>
<MyFlags
hasProjects={projects?.length > 0}
flagData={
personalDashboard?.flags.length
? {
state: 'flags' as const,
activeFlag,
flags: personalDashboard.flags,
}
: { state: 'no flags' as const }
}
setActiveFlag={setActiveFlag}
refetchDashboard={refetchDashboard}
/>
</StyledAccordionDetails>
</SectionAccordion>
);
};
const ProjectPanel = () => {
const { personalDashboard } = usePersonalDashboard();
const projects = personalDashboard?.projects || [];
const {
activeProject,
setActiveProject,
toggleSectionState,
expandProjects,
} = useDashboardState({ projects });
const personalDashboardProjectDetails =
fromPersonalDashboardProjectDetailsOutput(
usePersonalDashboardProjectDetails(activeProject),
);
return (
<SectionAccordion
disableGutters
expanded={expandProjects ?? true}
onChange={() => toggleSectionState('projects')}
>
<StyledAccordionSummaryWithBorder
expandIcon={
<ExpandMore titleAccess='Toggle projects section' />
}
id='projects-panel-header'
aria-controls='projects-panel-content'
>
<AccordionSummaryText>
<AccordionSummaryHeader>My projects</AccordionSummaryHeader>
<AccordionSummarySubtitle>
Favorite projects, projects you own, and projects you
are a member of
</AccordionSummarySubtitle>
</AccordionSummaryText>
</StyledAccordionSummaryWithBorder>
<StyledAccordionDetails>
<MyProjects
owners={personalDashboard?.projectOwners ?? []}
admins={personalDashboard?.admins ?? []}
projects={projects}
activeProject={activeProject || ''}
setActiveProject={setActiveProject}
personalDashboardProjectDetails={
personalDashboardProjectDetails
}
/>
</StyledAccordionDetails>
</SectionAccordion>
);
};
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 = () => {
</ViewKeyConceptsButton>
</WelcomeSection>
{showTimelinePanel && (
<SectionAccordion
disableGutters
expanded={expandTimeline ?? false}
onChange={() => toggleSectionState('timeline')}
>
<StyledAccordionSummary
expandIcon={
<ExpandMore titleAccess='Toggle timeline section' />
}
id='timeline-panel-header'
aria-controls='timeline-panel-content'
>
<AccordionSummaryText>
<AccordionSummaryHeader>
Event timeline
</AccordionSummaryHeader>
<AccordionSummarySubtitle>
Overview of recent activities across all
projects in Unleash. Make debugging easier and{' '}
<Link
to={signalsLink}
onClick={() => {
trackEvent('event-timeline', {
props: {
eventType: 'signals clicked',
},
});
}}
>
include external signals
</Link>{' '}
to get a fuller overview.
</AccordionSummarySubtitle>
</AccordionSummaryText>
</StyledAccordionSummary>
<StyledAccordionDetails>
<AccordionContent>
<EventTimeline />
</AccordionContent>
</StyledAccordionDetails>
</SectionAccordion>
)}
<SectionAccordion
disableGutters
expanded={expandProjects ?? true}
onChange={() => toggleSectionState('projects')}
>
<StyledAccordionSummaryWithBorder
expandIcon={
<ExpandMore titleAccess='Toggle projects section' />
}
id='projects-panel-header'
aria-controls='projects-panel-content'
>
<AccordionSummaryText>
<AccordionSummaryHeader>
My projects
</AccordionSummaryHeader>
<AccordionSummarySubtitle>
Favorite projects, projects you own, and projects
you are a member of
</AccordionSummarySubtitle>
</AccordionSummaryText>
</StyledAccordionSummaryWithBorder>
<StyledAccordionDetails>
<MyProjects
owners={personalDashboard?.projectOwners ?? []}
admins={personalDashboard?.admins ?? []}
projects={projects}
activeProject={activeProject || ''}
setActiveProject={setActiveProject}
personalDashboardProjectDetails={
personalDashboardProjectDetails
}
/>
</StyledAccordionDetails>
</SectionAccordion>
{showTimelinePanel && <EventTimelinePanel />}
<ProjectPanel />
<FlagPanel />
<SectionAccordion
expanded={expandFlags ?? true}
onChange={() => toggleSectionState('flags')}
>
<StyledAccordionSummaryWithBorder
expandIcon={
<ExpandMore titleAccess='Toggle flags section' />
}
id='flags-panel-header'
aria-controls='flags-panel-content'
>
<AccordionSummaryText>
<AccordionSummaryHeader>
My feature flags
</AccordionSummaryHeader>
<AccordionSummarySubtitle>
Feature flags you have created or favorited
</AccordionSummarySubtitle>
</AccordionSummaryText>
</StyledAccordionSummaryWithBorder>
<StyledAccordionDetails>
<MyFlags
hasProjects={projects?.length > 0}
flagData={
personalDashboard?.flags.length
? {
state: 'flags' as const,
activeFlag,
flags: personalDashboard.flags,
}
: { state: 'no flags' as const }
}
setActiveFlag={setActiveFlag}
refetchDashboard={refetchDashboard}
/>
</StyledAccordionDetails>
</SectionAccordion>
<WelcomeDialog
open={welcomeDialog === 'open'}
onClose={() => {

View File

@ -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<State> = {};
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;