mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: basic flag creation chart (#10411)
This is partial implementation of flag creation chart. This only shows the created flags line part, but I need to style it and also add bar charts in next PRs. <img width="1498" height="523" alt="image" src="https://github.com/user-attachments/assets/6d7a3145-95ff-4d31-85dd-47d687527d47" />
This commit is contained in:
		
							parent
							
								
									89f843fd0d
								
							
						
					
					
						commit
						8943cc0a3d
					
				@ -16,6 +16,7 @@ const setupApi = () => {
 | 
			
		||||
        flagTrends: [],
 | 
			
		||||
        environmentTypeTrends: [],
 | 
			
		||||
        lifecycleTrends: [],
 | 
			
		||||
        creationArchiveTrends: [],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    testServerRoute(server, '/api/admin/projects', {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,127 @@
 | 
			
		||||
import 'chartjs-adapter-date-fns';
 | 
			
		||||
import { type FC, useMemo } from 'react';
 | 
			
		||||
import type { InstanceInsightsSchema } from 'openapi';
 | 
			
		||||
import { useProjectChartData } from 'component/insights/hooks/useProjectChartData';
 | 
			
		||||
import {
 | 
			
		||||
    fillGradientPrimary,
 | 
			
		||||
    LineChart,
 | 
			
		||||
    NotEnoughData,
 | 
			
		||||
} from 'component/insights/components/LineChart/LineChart';
 | 
			
		||||
import { useTheme } from '@mui/material';
 | 
			
		||||
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
 | 
			
		||||
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
 | 
			
		||||
 | 
			
		||||
interface ICreationArchiveChartProps {
 | 
			
		||||
    creationArchiveTrends: GroupedDataByProject<
 | 
			
		||||
        InstanceInsightsSchema['creationArchiveTrends']
 | 
			
		||||
    >;
 | 
			
		||||
    isAggregate?: boolean;
 | 
			
		||||
    isLoading?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WeekData = {
 | 
			
		||||
    archivedFlags: number;
 | 
			
		||||
    totalCreatedFlags: number;
 | 
			
		||||
    week: string;
 | 
			
		||||
    date?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type RawWeekData = {
 | 
			
		||||
    archivedFlags: number;
 | 
			
		||||
    createdFlags: Record<string, number>;
 | 
			
		||||
    week: string;
 | 
			
		||||
    date: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
 | 
			
		||||
    creationArchiveTrends,
 | 
			
		||||
    isAggregate,
 | 
			
		||||
    isLoading,
 | 
			
		||||
}) => {
 | 
			
		||||
    const creationArchiveData = useProjectChartData(creationArchiveTrends);
 | 
			
		||||
    const theme = useTheme();
 | 
			
		||||
    const placeholderData = usePlaceholderData();
 | 
			
		||||
 | 
			
		||||
    const aggregateHealthData = useMemo(() => {
 | 
			
		||||
        const labels: string[] = Array.from(
 | 
			
		||||
            new Set(
 | 
			
		||||
                creationArchiveData.datasets.flatMap((d) =>
 | 
			
		||||
                    d.data.map((item) => item.week),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const weeks: WeekData[] = labels
 | 
			
		||||
            .map((label) => {
 | 
			
		||||
                return creationArchiveData.datasets
 | 
			
		||||
                    .map((d) => d.data.find((item) => item.week === label))
 | 
			
		||||
                    .reduce(
 | 
			
		||||
                        (acc: WeekData, item: RawWeekData) => {
 | 
			
		||||
                            if (item) {
 | 
			
		||||
                                acc.archivedFlags += item.archivedFlags || 0;
 | 
			
		||||
                                const createdFlagsSum = item.createdFlags
 | 
			
		||||
                                    ? Object.values(item.createdFlags).reduce(
 | 
			
		||||
                                          (sum: number, count: number) =>
 | 
			
		||||
                                              sum + count,
 | 
			
		||||
                                          0,
 | 
			
		||||
                                      )
 | 
			
		||||
                                    : 0;
 | 
			
		||||
                                acc.totalCreatedFlags += createdFlagsSum;
 | 
			
		||||
                            }
 | 
			
		||||
                            if (!acc.date) {
 | 
			
		||||
                                acc.date = item?.date;
 | 
			
		||||
                            }
 | 
			
		||||
                            return acc;
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            archivedFlags: 0,
 | 
			
		||||
                            totalCreatedFlags: 0,
 | 
			
		||||
                            week: label,
 | 
			
		||||
                        } as WeekData,
 | 
			
		||||
                    );
 | 
			
		||||
            })
 | 
			
		||||
            .sort((a, b) => (a.week > b.week ? 1 : -1));
 | 
			
		||||
        return {
 | 
			
		||||
            datasets: [
 | 
			
		||||
                {
 | 
			
		||||
                    label: 'Number of created flags',
 | 
			
		||||
                    data: weeks,
 | 
			
		||||
                    borderColor: theme.palette.primary.light,
 | 
			
		||||
                    backgroundColor: fillGradientPrimary,
 | 
			
		||||
                    fill: true,
 | 
			
		||||
                    order: 3,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    }, [creationArchiveData, theme]);
 | 
			
		||||
 | 
			
		||||
    const aggregateOrProjectData = isAggregate
 | 
			
		||||
        ? aggregateHealthData
 | 
			
		||||
        : creationArchiveData;
 | 
			
		||||
    const notEnoughData = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            !isLoading &&
 | 
			
		||||
            !creationArchiveData.datasets.some((d) => d.data.length > 1),
 | 
			
		||||
        [creationArchiveData, isLoading],
 | 
			
		||||
    );
 | 
			
		||||
    const data =
 | 
			
		||||
        notEnoughData || isLoading ? placeholderData : aggregateOrProjectData;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <LineChart
 | 
			
		||||
            key={isAggregate ? 'aggregate' : 'project'}
 | 
			
		||||
            data={data}
 | 
			
		||||
            overrideOptions={
 | 
			
		||||
                notEnoughData
 | 
			
		||||
                    ? {}
 | 
			
		||||
                    : {
 | 
			
		||||
                          parsing: {
 | 
			
		||||
                              yAxisKey: 'totalCreatedFlags',
 | 
			
		||||
                              xAxisKey: 'date',
 | 
			
		||||
                          },
 | 
			
		||||
                      }
 | 
			
		||||
            }
 | 
			
		||||
            cover={notEnoughData ? <NotEnoughData /> : isLoading}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -22,6 +22,11 @@ export const useInsightsData = (
 | 
			
		||||
        projects,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const creationArchiveData = useFilteredTrends(
 | 
			
		||||
        instanceInsights.creationArchiveTrends,
 | 
			
		||||
        projects,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const groupedProjectsData = useGroupedProjectTrends(projectsData);
 | 
			
		||||
 | 
			
		||||
    const metricsData = useFilteredTrends(
 | 
			
		||||
@ -34,6 +39,9 @@ export const useInsightsData = (
 | 
			
		||||
 | 
			
		||||
    const groupedLifecycleData = useGroupedProjectTrends(lifecycleData);
 | 
			
		||||
 | 
			
		||||
    const groupedCreationArchiveData =
 | 
			
		||||
        useGroupedProjectTrends(creationArchiveData);
 | 
			
		||||
 | 
			
		||||
    return useMemo(
 | 
			
		||||
        () => ({
 | 
			
		||||
            ...instanceInsights,
 | 
			
		||||
@ -46,6 +54,7 @@ export const useInsightsData = (
 | 
			
		||||
            allMetricsDatapoints,
 | 
			
		||||
            lifecycleData,
 | 
			
		||||
            groupedLifecycleData,
 | 
			
		||||
            groupedCreationArchiveData,
 | 
			
		||||
        }),
 | 
			
		||||
        [
 | 
			
		||||
            instanceInsights,
 | 
			
		||||
@ -57,6 +66,7 @@ export const useInsightsData = (
 | 
			
		||||
            summary,
 | 
			
		||||
            lifecycleData,
 | 
			
		||||
            groupedLifecycleData,
 | 
			
		||||
            groupedCreationArchiveData,
 | 
			
		||||
        ],
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import useProjects from 'hooks/api/getters/useProjects/useProjects';
 | 
			
		||||
 | 
			
		||||
type ProjectFlagTrends = InstanceInsightsSchema['projectFlagTrends'];
 | 
			
		||||
type LifecycleTrends = InstanceInsightsSchema['lifecycleTrends'];
 | 
			
		||||
type CreationArchiveTrends = InstanceInsightsSchema['creationArchiveTrends'];
 | 
			
		||||
 | 
			
		||||
export const calculateTechDebt = (item: {
 | 
			
		||||
    total: number;
 | 
			
		||||
@ -25,7 +26,8 @@ export const calculateTechDebt = (item: {
 | 
			
		||||
export const useProjectChartData = (
 | 
			
		||||
    projectFlagTrends:
 | 
			
		||||
        | GroupedDataByProject<ProjectFlagTrends>
 | 
			
		||||
        | GroupedDataByProject<LifecycleTrends>,
 | 
			
		||||
        | GroupedDataByProject<LifecycleTrends>
 | 
			
		||||
        | GroupedDataByProject<CreationArchiveTrends>,
 | 
			
		||||
) => {
 | 
			
		||||
    const theme = useTheme();
 | 
			
		||||
    const getProjectColor = useProjectColor();
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import {
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
import { NewProductionFlagsChart } from '../componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx';
 | 
			
		||||
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
 | 
			
		||||
import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx';
 | 
			
		||||
 | 
			
		||||
export const PerformanceInsights: FC = () => {
 | 
			
		||||
    const statePrefix = 'performance-';
 | 
			
		||||
@ -63,6 +64,7 @@ export const PerformanceInsights: FC = () => {
 | 
			
		||||
        groupedMetricsData,
 | 
			
		||||
        allMetricsDatapoints,
 | 
			
		||||
        environmentTypeTrends,
 | 
			
		||||
        groupedCreationArchiveData,
 | 
			
		||||
    } = useInsightsData(insights, projects);
 | 
			
		||||
 | 
			
		||||
    const { isEnterprise } = useUiConfig();
 | 
			
		||||
@ -110,6 +112,21 @@ export const PerformanceInsights: FC = () => {
 | 
			
		||||
                </StyledWidget>
 | 
			
		||||
            ) : null}
 | 
			
		||||
 | 
			
		||||
            {isLifecycleGraphsEnabled && isEnterprise() ? (
 | 
			
		||||
                <StyledWidget>
 | 
			
		||||
                    <StyledWidgetStats width={275}>
 | 
			
		||||
                        <WidgetTitle title='Flags created vs archived' />
 | 
			
		||||
                    </StyledWidgetStats>
 | 
			
		||||
                    <StyledChartContainer>
 | 
			
		||||
                        <CreationArchiveChart
 | 
			
		||||
                            creationArchiveTrends={groupedCreationArchiveData}
 | 
			
		||||
                            isAggregate={showAllProjects}
 | 
			
		||||
                            isLoading={loading}
 | 
			
		||||
                        />
 | 
			
		||||
                    </StyledChartContainer>
 | 
			
		||||
                </StyledWidget>
 | 
			
		||||
            ) : null}
 | 
			
		||||
 | 
			
		||||
            {showAllProjects ? (
 | 
			
		||||
                <StyledWidget>
 | 
			
		||||
                    <StyledWidgetStats width={275}>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user