1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Fix: insights loading (#6834)

Loading state for
- charts (placeholder data, animation)
- user stats - loading skeleton animation
- empty flags stats
- kept other "stat" widgets as-is, usually not visible
This commit is contained in:
Tymoteusz Czech 2024-04-15 09:46:56 +02:00 committed by GitHub
parent 1f4febbd3c
commit e10ad7257f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 97 additions and 33 deletions

View File

@ -1,4 +1,6 @@
import { ConditionallyRender } from '../common/ConditionallyRender/ConditionallyRender'; import type { VFC } from 'react';
import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Widget } from './components/Widget/Widget'; import { Widget } from './components/Widget/Widget';
import { UserStats } from './componentsStat/UserStats/UserStats'; import { UserStats } from './componentsStat/UserStats/UserStats';
import { UsersChart } from './componentsChart/UsersChart/UsersChart'; import { UsersChart } from './componentsChart/UsersChart/UsersChart';
@ -18,9 +20,7 @@ import type {
InstanceInsightsSchemaUsers, InstanceInsightsSchemaUsers,
} from 'openapi'; } from 'openapi';
import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends'; import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends';
import { Box, styled } from '@mui/material'; import { allOption } from 'component/common/ProjectSelect/ProjectSelect';
import { allOption } from '../common/ProjectSelect/ProjectSelect';
import type { VFC } from 'react';
import { chartInfo } from './chart-info'; import { chartInfo } from './chart-info';
interface IChartsProps { interface IChartsProps {
@ -107,6 +107,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
count={users.total} count={users.total}
active={users.active} active={users.active}
inactive={users.inactive} inactive={users.inactive}
isLoading={loading}
/> />
</Widget> </Widget>
} }
@ -116,7 +117,10 @@ export const InsightsCharts: VFC<IChartsProps> = ({
? chartInfo.usersInProject ? chartInfo.usersInProject
: chartInfo.avgUsersPerProject)} : chartInfo.avgUsersPerProject)}
> >
<UserStats count={summary.averageUsers} /> <UserStats
count={summary.averageUsers}
isLoading={loading}
/>
</Widget> </Widget>
} }
/> />
@ -134,6 +138,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
<ChartWidget {...chartInfo.usersPerProject}> <ChartWidget {...chartInfo.usersPerProject}>
<UsersPerProjectChart <UsersPerProjectChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isLoading={loading}
/> />
</ChartWidget> </ChartWidget>
} }
@ -144,6 +149,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
flagsPerUser={ flagsPerUser={
showAllProjects ? getFlagsPerUser(flags, users) : '' showAllProjects ? getFlagsPerUser(flags, users) : ''
} }
isLoading={loading}
/> />
</Widget> </Widget>
<ConditionallyRender <ConditionallyRender
@ -160,6 +166,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
<ChartWidget {...chartInfo.flagsPerProject}> <ChartWidget {...chartInfo.flagsPerProject}>
<FlagsProjectChart <FlagsProjectChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isLoading={loading}
/> />
</ChartWidget> </ChartWidget>
} }
@ -180,6 +187,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
<ProjectHealthChart <ProjectHealthChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading}
/> />
</ChartWidget> </ChartWidget>
<Widget {...chartInfo.medianTimeToProduction}> <Widget {...chartInfo.medianTimeToProduction}>
@ -195,6 +203,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
<TimeToProductionChart <TimeToProductionChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading}
/> />
</ChartWidget> </ChartWidget>
</StyledGrid> </StyledGrid>
@ -207,6 +216,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
metricsSummaryTrends={groupedMetricsData} metricsSummaryTrends={groupedMetricsData}
allDatapointsSorted={allMetricsDatapoints} allDatapointsSorted={allMetricsDatapoints}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading}
/> />
</Widget> </Widget>
<Widget <Widget

View File

@ -116,6 +116,7 @@ const LineChartComponent: VFC<{
return ( return (
<StyledContainer> <StyledContainer>
<Line <Line
key={cover ? 'cover' : 'chart'}
options={options} options={options}
data={data} data={data}
plugins={[customHighlightPlugin]} plugins={[customHighlightPlugin]}

View File

@ -18,7 +18,7 @@ export const FlagsChart: VFC<IFlagsChartProps> = ({
isLoading, isLoading,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const notEnoughData = flagTrends.length < 2; const notEnoughData = !isLoading && flagTrends.length < 2;
const placeholderData = usePlaceholderData({ fill: true, type: 'double' }); const placeholderData = usePlaceholderData({ fill: true, type: 'double' });
const data = useMemo( const data = useMemo(

View File

@ -13,10 +13,12 @@ interface IFlagsProjectChartProps {
projectFlagTrends: GroupedDataByProject< projectFlagTrends: GroupedDataByProject<
InstanceInsightsSchema['projectFlagTrends'] InstanceInsightsSchema['projectFlagTrends']
>; >;
isLoading?: boolean;
} }
export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({ export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
projectFlagTrends, projectFlagTrends,
isLoading,
}) => { }) => {
const placeholderData = usePlaceholderData({ const placeholderData = usePlaceholderData({
type: 'constant', type: 'constant',
@ -24,20 +26,22 @@ export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
const data = useProjectChartData(projectFlagTrends); const data = useProjectChartData(projectFlagTrends);
const notEnoughData = useMemo( const notEnoughData = useMemo(
() => (data.datasets.some((d) => d.data.length > 1) ? false : true), () =>
[data], !isLoading &&
(data.datasets.some((d) => d.data.length > 1) ? false : true),
[data, isLoading],
); );
return ( return (
<LineChart <LineChart
data={notEnoughData ? placeholderData : data} data={notEnoughData || isLoading ? placeholderData : data}
overrideOptions={{ overrideOptions={{
parsing: { parsing: {
yAxisKey: 'total', yAxisKey: 'total',
xAxisKey: 'date', xAxisKey: 'date',
}, },
}} }}
cover={notEnoughData ? <NotEnoughData /> : false} cover={notEnoughData ? <NotEnoughData /> : isLoading}
/> />
); );
}; };

View File

@ -20,12 +20,14 @@ interface IMetricsSummaryChartProps {
>; >;
isAggregate?: boolean; isAggregate?: boolean;
allDatapointsSorted: string[]; allDatapointsSorted: string[];
isLoading?: boolean;
} }
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
metricsSummaryTrends, metricsSummaryTrends,
isAggregate, isAggregate,
allDatapointsSorted, allDatapointsSorted,
isLoading,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const metricsSummary = useFilledMetricsSummary( const metricsSummary = useFilledMetricsSummary(
@ -33,8 +35,10 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
allDatapointsSorted, allDatapointsSorted,
); );
const notEnoughData = useMemo( const notEnoughData = useMemo(
() => !metricsSummary.datasets.some((d) => d.data.length > 1), () =>
[metricsSummary], !isLoading &&
!metricsSummary.datasets.some((d) => d.data.length > 1),
[metricsSummary, isLoading],
); );
const placeholderData = usePlaceholderData(); const placeholderData = usePlaceholderData();
@ -67,7 +71,7 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
return ( return (
<LineChart <LineChart
data={notEnoughData ? placeholderData : data} data={notEnoughData || isLoading ? placeholderData : data}
TooltipComponent={MetricsSummaryTooltip} TooltipComponent={MetricsSummaryTooltip}
overrideOptions={ overrideOptions={
notEnoughData notEnoughData
@ -79,7 +83,7 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
}, },
} }
} }
cover={notEnoughData ? <NotEnoughData /> : false} cover={notEnoughData ? <NotEnoughData /> : isLoading}
/> />
); );
}; };

View File

@ -10,20 +10,24 @@ import {
} from 'component/insights/components/LineChart/LineChart'; } from 'component/insights/components/LineChart/LineChart';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
interface IProjectHealthChartProps { interface IProjectHealthChartProps {
projectFlagTrends: GroupedDataByProject< projectFlagTrends: GroupedDataByProject<
InstanceInsightsSchema['projectFlagTrends'] InstanceInsightsSchema['projectFlagTrends']
>; >;
isAggregate?: boolean; isAggregate?: boolean;
isLoading?: boolean;
} }
export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({ export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({
projectFlagTrends, projectFlagTrends,
isAggregate, isAggregate,
isLoading,
}) => { }) => {
const projectsData = useProjectChartData(projectFlagTrends); const projectsData = useProjectChartData(projectFlagTrends);
const theme = useTheme(); const theme = useTheme();
const placeholderData = usePlaceholderData();
const aggregateHealthData = useMemo(() => { const aggregateHealthData = useMemo(() => {
const labels = Array.from( const labels = Array.from(
@ -85,12 +89,19 @@ export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({
}; };
}, [projectsData, theme]); }, [projectsData, theme]);
const data = isAggregate ? aggregateHealthData : projectsData; const aggregateOrProjectData = isAggregate
? aggregateHealthData
: projectsData;
const notEnoughData = useMemo( const notEnoughData = useMemo(
() => () =>
projectsData.datasets.some((d) => d.data.length > 1) ? false : true, !isLoading &&
[projectsData], (projectsData.datasets.some((d) => d.data.length > 1)
? false
: true),
[projectsData, isLoading],
); );
const data =
notEnoughData || isLoading ? placeholderData : aggregateOrProjectData;
return ( return (
<LineChart <LineChart
@ -104,7 +115,7 @@ export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({
parsing: { yAxisKey: 'health', xAxisKey: 'date' }, parsing: { yAxisKey: 'health', xAxisKey: 'date' },
} }
} }
cover={notEnoughData ? <NotEnoughData /> : false} cover={notEnoughData ? <NotEnoughData /> : isLoading}
/> />
); );
}; };

View File

@ -18,17 +18,21 @@ interface ITimeToProductionChartProps {
InstanceInsightsSchema['projectFlagTrends'] InstanceInsightsSchema['projectFlagTrends']
>; >;
isAggregate?: boolean; isAggregate?: boolean;
isLoading?: boolean;
} }
export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({ export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({
projectFlagTrends, projectFlagTrends,
isAggregate, isAggregate,
isLoading,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const projectsDatasets = useProjectChartData(projectFlagTrends); const projectsDatasets = useProjectChartData(projectFlagTrends);
const notEnoughData = useMemo( const notEnoughData = useMemo(
() => !projectsDatasets.datasets.some((d) => d.data.length > 1), () =>
[projectsDatasets], !isLoading &&
!projectsDatasets.datasets.some((d) => d.data.length > 1),
[projectsDatasets, isLoading],
); );
const aggregatedPerDay = useMemo(() => { const aggregatedPerDay = useMemo(() => {
@ -62,7 +66,7 @@ export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({
const placeholderData = usePlaceholderData(); const placeholderData = usePlaceholderData();
return ( return (
<LineChart <LineChart
data={notEnoughData ? placeholderData : data} data={notEnoughData || isLoading ? placeholderData : data}
TooltipComponent={TimeToProductionTooltip} TooltipComponent={TimeToProductionTooltip}
overrideOptions={ overrideOptions={
notEnoughData notEnoughData
@ -74,7 +78,7 @@ export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({
}, },
} }
} }
cover={notEnoughData ? <NotEnoughData /> : false} cover={notEnoughData ? <NotEnoughData /> : isLoading}
/> />
); );
}; };

View File

@ -92,7 +92,7 @@ export const UpdatesPerEnvironmentTypeChart: VFC<
> = ({ environmentTypeTrends, isLoading }) => { > = ({ environmentTypeTrends, isLoading }) => {
const theme = useTheme(); const theme = useTheme();
const getEnvironmentTypeColor = useEnvironmentTypeColor(); const getEnvironmentTypeColor = useEnvironmentTypeColor();
const notEnoughData = environmentTypeTrends?.length < 2; const notEnoughData = !isLoading && environmentTypeTrends?.length < 2;
const placeholderData = usePlaceholderData({ fill: true, type: 'double' }); const placeholderData = usePlaceholderData({ fill: true, type: 'double' });
const data = useMemo(() => { const data = useMemo(() => {

View File

@ -21,7 +21,7 @@ export const UsersChart: VFC<IUsersChartProps> = ({
}) => { }) => {
const showInactiveUsers = useUiFlag('showInactiveUsers'); const showInactiveUsers = useUiFlag('showInactiveUsers');
const theme = useTheme(); const theme = useTheme();
const notEnoughData = userTrends.length < 2; const notEnoughData = !isLoading && userTrends.length < 2;
const placeholderData = usePlaceholderData({ fill: true, type: 'rising' }); const placeholderData = usePlaceholderData({ fill: true, type: 'rising' });
const data = useMemo( const data = useMemo(
() => ({ () => ({

View File

@ -13,10 +13,12 @@ interface IUsersPerProjectChartProps {
projectFlagTrends: GroupedDataByProject< projectFlagTrends: GroupedDataByProject<
InstanceInsightsSchema['projectFlagTrends'] InstanceInsightsSchema['projectFlagTrends']
>; >;
isLoading?: boolean;
} }
export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({ export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({
projectFlagTrends, projectFlagTrends,
isLoading,
}) => { }) => {
const placeholderData = usePlaceholderData({ const placeholderData = usePlaceholderData({
type: 'constant', type: 'constant',
@ -24,20 +26,22 @@ export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({
const data = useProjectChartData(projectFlagTrends); const data = useProjectChartData(projectFlagTrends);
const notEnoughData = useMemo( const notEnoughData = useMemo(
() => (data.datasets.some((d) => d.data.length > 1) ? false : true), () =>
[data], !isLoading &&
(data.datasets.some((d) => d.data.length > 1) ? false : true),
[data, isLoading],
); );
return ( return (
<LineChart <LineChart
data={notEnoughData ? placeholderData : data} data={notEnoughData || isLoading ? placeholderData : data}
overrideOptions={{ overrideOptions={{
parsing: { parsing: {
yAxisKey: 'users', yAxisKey: 'users',
xAxisKey: 'date', xAxisKey: 'date',
}, },
}} }}
cover={notEnoughData ? <NotEnoughData /> : false} cover={notEnoughData ? <NotEnoughData /> : isLoading}
/> />
); );
}; };

View File

@ -68,17 +68,21 @@ const StyledIcon = styled(Icon)(({ theme }) => ({
interface IFlagStatsProps { interface IFlagStatsProps {
count: number; count: number;
flagsPerUser?: string; flagsPerUser?: string;
isLoading?: boolean;
} }
export const FlagStats: React.FC<IFlagStatsProps> = ({ export const FlagStats: React.FC<IFlagStatsProps> = ({
count, count,
flagsPerUser, flagsPerUser,
isLoading,
}) => { }) => {
return ( return (
<> <>
<StyledRingContainer> <StyledRingContainer>
<StyledRing> <StyledRing>
<StyledRingContent>{count}</StyledRingContent> <StyledRingContent>
{isLoading ? '' : count}
</StyledRingContent>
</StyledRing> </StyledRing>
</StyledRingContainer> </StyledRingContainer>

View File

@ -70,9 +70,21 @@ interface IUserStatsProps {
count: number; count: number;
active?: number; active?: number;
inactive?: number; inactive?: number;
isLoading?: boolean;
} }
export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => { const StyledLoadingSkeleton = styled(Box)(() => ({
'&:before': {
background: 'transparent',
},
}));
export const UserStats: FC<IUserStatsProps> = ({
count,
active,
inactive,
isLoading,
}) => {
const showInactiveUsers = useUiFlag('showInactiveUsers'); const showInactiveUsers = useUiFlag('showInactiveUsers');
const showDistribution = const showDistribution =
showInactiveUsers && active !== undefined && inactive !== undefined; showInactiveUsers && active !== undefined && inactive !== undefined;
@ -83,9 +95,19 @@ export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => {
<StyledUserContainer> <StyledUserContainer>
<StyledUserBox> <StyledUserBox>
<StyledUserCount variant='h2'> <StyledUserCount variant='h2'>
{Number.parseInt(`${count}`, 10) === count <ConditionallyRender
? count condition={isLoading !== true}
: count.toFixed(2)} show={
Number.parseInt(`${count}`, 10) === count
? count
: count.toFixed(2)
}
elseShow={
<StyledLoadingSkeleton className='skeleton'>
&nbsp;
</StyledLoadingSkeleton>
}
/>
</StyledUserCount> </StyledUserCount>
</StyledUserBox> </StyledUserBox>
<StyledCustomShadow /> <StyledCustomShadow />