mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Insights dashboard refactor (#6404)
- reorganized dashboard components - added share link - health chart aggregated data - refactored chart placeholders
This commit is contained in:
		
							parent
							
								
									493f8e8a5b
								
							
						
					
					
						commit
						4fc0a806f1
					
				| @ -12,7 +12,9 @@ export const allOption = { label: 'ALL', id: '*' }; | ||||
| 
 | ||||
| interface IProjectSelectProps { | ||||
|     selectedProjects: string[]; | ||||
|     onChange: Dispatch<SetStateAction<string[]>>; | ||||
|     onChange: | ||||
|         | Dispatch<SetStateAction<string[]>> | ||||
|         | ((projects: string[]) => void); | ||||
|     dataTestId?: string; | ||||
|     sx?: SxProps; | ||||
|     disabled?: boolean; | ||||
|  | ||||
| @ -1,236 +1,195 @@ | ||||
| import { useMemo, useState, VFC } from 'react'; | ||||
| import { VFC } from 'react'; | ||||
| import { Box, styled } from '@mui/material'; | ||||
| import { ArrayParam, withDefault } from 'use-query-params'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { usePersistentTableState } from 'hooks/usePersistentTableState'; | ||||
| import { | ||||
|     Box, | ||||
|     styled, | ||||
|     Typography, | ||||
|     useMediaQuery, | ||||
|     useTheme, | ||||
| } from '@mui/material'; | ||||
| import { UsersChart } from './UsersChart/UsersChart'; | ||||
| import { FlagsChart } from './FlagsChart/FlagsChart'; | ||||
| import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary'; | ||||
| import { UserStats } from './UserStats/UserStats'; | ||||
| import { FlagStats } from './FlagStats/FlagStats'; | ||||
| import { Widget } from './Widget/Widget'; | ||||
| import { FlagsProjectChart } from './FlagsProjectChart/FlagsProjectChart'; | ||||
| import { ProjectHealthChart } from './ProjectHealthChart/ProjectHealthChart'; | ||||
| import { TimeToProductionChart } from './TimeToProductionChart/TimeToProductionChart'; | ||||
| import { TimeToProduction } from './TimeToProduction/TimeToProduction'; | ||||
| import { | ||||
|     ProjectSelect, | ||||
|     allOption, | ||||
| } from '../common/ProjectSelect/ProjectSelect'; | ||||
| import { MetricsSummaryChart } from './MetricsSummaryChart/MetricsSummaryChart'; | ||||
| import { | ||||
|     ExecutiveSummarySchemaMetricsSummaryTrendsItem, | ||||
|     ExecutiveSummarySchemaProjectFlagTrendsItem, | ||||
| } from 'openapi'; | ||||
| import { HealthStats } from './HealthStats/HealthStats'; | ||||
| import { DashboardHeader } from './DashboardHeader/DashboardHeader'; | ||||
|     ProjectSelect, | ||||
| } from 'component/common/ProjectSelect/ProjectSelect'; | ||||
| import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary'; | ||||
| 
 | ||||
| import { useFilteredFlagsSummary } from './hooks/useFilteredFlagsSummary'; | ||||
| import { useFilteredTrends } from './hooks/useFilteredTrends'; | ||||
| 
 | ||||
| import { Widget } from './components/Widget/Widget'; | ||||
| import { DashboardHeader } from './components/DashboardHeader/DashboardHeader'; | ||||
| 
 | ||||
| import { UserStats } from './componentsStat/UserStats/UserStats'; | ||||
| import { FlagStats } from './componentsStat/FlagStats/FlagStats'; | ||||
| import { HealthStats } from './componentsStat/HealthStats/HealthStats'; | ||||
| import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction'; | ||||
| 
 | ||||
| import { UsersChart } from './componentsChart/UsersChart/UsersChart'; | ||||
| import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart'; | ||||
| import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart'; | ||||
| import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart'; | ||||
| import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart'; | ||||
| import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart'; | ||||
| import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart'; | ||||
| 
 | ||||
| const StyledGrid = styled(Box)(({ theme }) => ({ | ||||
|     display: 'grid', | ||||
|     gridTemplateColumns: `300px 1fr`, | ||||
|     gridTemplateColumns: `repeat(2, 1fr)`, | ||||
|     gridAutoRows: 'auto', | ||||
|     gap: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledBox = styled(Box)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(4), | ||||
|     marginTop: theme.spacing(4), | ||||
|     [theme.breakpoints.down('lg')]: { | ||||
|         width: '100%', | ||||
|         marginLeft: 0, | ||||
|     paddingBottom: theme.spacing(2), | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|         gridTemplateColumns: `300px 1fr`, | ||||
|     }, | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
| })); | ||||
| 
 | ||||
| const useDashboardGrid = () => { | ||||
|     const theme = useTheme(); | ||||
|     const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); | ||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
| 
 | ||||
|     if (isSmallScreen) { | ||||
|         return { | ||||
|             gridTemplateColumns: `1fr`, | ||||
|             chartSpan: 1, | ||||
|             userTrendsOrder: 3, | ||||
|             flagStatsOrder: 2, | ||||
|             largeChartSpan: 1, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     if (isMediumScreen) { | ||||
|         return { | ||||
|             gridTemplateColumns: `1fr 1fr`, | ||||
|             chartSpan: 2, | ||||
|             userTrendsOrder: 3, | ||||
|             flagStatsOrder: 2, | ||||
|             largeChartSpan: 2, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         gridTemplateColumns: `300px auto`, | ||||
|         chartSpan: 1, | ||||
|         userTrendsOrder: 2, | ||||
|         flagStatsOrder: 3, | ||||
|         largeChartSpan: 2, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| interface FilteredProjectData { | ||||
|     filteredProjectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[]; | ||||
|     filteredMetricsSummaryTrends: ExecutiveSummarySchemaMetricsSummaryTrendsItem[]; | ||||
| } | ||||
| const ChartWidget = styled(Widget)(({ theme }) => ({ | ||||
|     [theme.breakpoints.down('md')]: { | ||||
|         gridColumnStart: 'span 2', | ||||
|         order: 2, | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| export const ExecutiveDashboard: VFC = () => { | ||||
|     const { executiveDashboardData, loading, error } = useExecutiveDashboard(); | ||||
|     const [projects, setProjects] = useState([allOption.id]); | ||||
| 
 | ||||
|     const flagPerUsers = useMemo(() => { | ||||
|         if ( | ||||
|             executiveDashboardData.users.total === 0 || | ||||
|             executiveDashboardData.flags.total === 0 | ||||
|         ) | ||||
|             return '0'; | ||||
| 
 | ||||
|         return ( | ||||
|             executiveDashboardData.flags.total / | ||||
|             executiveDashboardData.users.total | ||||
|         ).toFixed(1); | ||||
|     }, [executiveDashboardData]); | ||||
| 
 | ||||
|     const { filteredProjectFlagTrends, filteredMetricsSummaryTrends } = | ||||
|         useMemo<FilteredProjectData>(() => { | ||||
|             if (projects[0] === allOption.id) { | ||||
|                 return { | ||||
|                     filteredProjectFlagTrends: | ||||
|     const stateConfig = { | ||||
|         projects: withDefault(ArrayParam, [allOption.id]), | ||||
|     }; | ||||
|     const [state, setState] = usePersistentTableState(`insights`, stateConfig); | ||||
|     const setProjects = (projects: string[]) => { | ||||
|         setState({ projects }); | ||||
|     }; | ||||
|     const projects = state.projects | ||||
|         ? (state.projects.filter(Boolean) as string[]) | ||||
|         : []; | ||||
|     const showAllProjects = projects[0] === allOption.id; | ||||
|     const projectsData = useFilteredTrends( | ||||
|         executiveDashboardData.projectFlagTrends, | ||||
|                     filteredMetricsSummaryTrends: | ||||
|         projects, | ||||
|     ); | ||||
|     const metricsData = useFilteredTrends( | ||||
|         executiveDashboardData.metricsSummaryTrends, | ||||
|                 }; | ||||
|             } | ||||
|         projects, | ||||
|     ); | ||||
|     const { users } = executiveDashboardData; | ||||
| 
 | ||||
|             const filteredProjectFlagTrends = | ||||
|                 executiveDashboardData.projectFlagTrends.filter((trend) => | ||||
|                     projects.includes(trend.project), | ||||
|                 ) as ExecutiveSummarySchemaProjectFlagTrendsItem[]; | ||||
| 
 | ||||
|             const filteredImpressionsSummary = | ||||
|                 executiveDashboardData.metricsSummaryTrends.filter((summary) => | ||||
|                     projects.includes(summary.project), | ||||
|                 ) as ExecutiveSummarySchemaMetricsSummaryTrendsItem[]; | ||||
| 
 | ||||
|             return { | ||||
|                 filteredProjectFlagTrends, | ||||
|                 filteredMetricsSummaryTrends: filteredImpressionsSummary, | ||||
|             }; | ||||
|         }, [executiveDashboardData, projects]); | ||||
| 
 | ||||
|     const { | ||||
|         gridTemplateColumns, | ||||
|         chartSpan, | ||||
|         userTrendsOrder, | ||||
|         flagStatsOrder, | ||||
|         largeChartSpan, | ||||
|     } = useDashboardGrid(); | ||||
|     const summary = useFilteredFlagsSummary(projectsData); | ||||
|     const isOneProjectSelected = projects.length === 1; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Box sx={(theme) => ({ paddingBottom: theme.spacing(4) })}> | ||||
|                 <DashboardHeader /> | ||||
|             </Box> | ||||
|             <StyledGrid sx={{ gridTemplateColumns }}> | ||||
|                 <Widget title='Total users' order={1}> | ||||
|                     <UserStats | ||||
|                         count={executiveDashboardData.users.total} | ||||
|                         active={executiveDashboardData.users.active} | ||||
|                         inactive={executiveDashboardData.users.inactive} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <Widget title='Users' order={userTrendsOrder} span={chartSpan}> | ||||
|                     <UsersChart | ||||
|                         userTrends={executiveDashboardData.userTrends} | ||||
|                         isLoading={loading} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <Widget | ||||
|                     title='Total flags' | ||||
|                     tooltip='Total flags represent the total active flags (not archived) that currently exist across all projects of your application.' | ||||
|                     order={flagStatsOrder} | ||||
|                 > | ||||
|                     <FlagStats | ||||
|                         count={executiveDashboardData.flags.total} | ||||
|                         flagsPerUser={flagPerUsers} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <Widget title='Number of flags' order={4} span={chartSpan}> | ||||
|                     <FlagsChart | ||||
|                         flagTrends={executiveDashboardData.flagTrends} | ||||
|                         isLoading={loading} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|             </StyledGrid> | ||||
|             <StyledBox> | ||||
|                 <Typography variant='h2' component='span'> | ||||
|                     Insights per project | ||||
|                 </Typography> | ||||
|                 <DashboardHeader | ||||
|                     actions={ | ||||
|                         <ProjectSelect | ||||
|                             selectedProjects={projects} | ||||
|                             onChange={setProjects} | ||||
|                             dataTestId={'DASHBOARD_PROJECT_SELECT'} | ||||
|                             sx={{ flex: 1, maxWidth: '360px' }} | ||||
|                         /> | ||||
|             </StyledBox> | ||||
|                     } | ||||
|                 /> | ||||
|             </Box> | ||||
|             <StyledGrid> | ||||
|                 <ConditionallyRender | ||||
|                     condition={showAllProjects} | ||||
|                     show={ | ||||
|                         <Widget title='Total users'> | ||||
|                             <UserStats | ||||
|                                 count={users.total} | ||||
|                                 active={users.active} | ||||
|                                 inactive={users.inactive} | ||||
|                             /> | ||||
|                         </Widget> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <Widget | ||||
|                     title='Number of flags per project' | ||||
|                     order={5} | ||||
|                     span={largeChartSpan} | ||||
|                             title={ | ||||
|                                 isOneProjectSelected | ||||
|                                     ? 'Users in project' | ||||
|                                     : 'Users per project on average' | ||||
|                             } | ||||
|                         > | ||||
|                             <UserStats count={summary.averageUsers} /> | ||||
|                         </Widget> | ||||
|                     } | ||||
|                 /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={showAllProjects} | ||||
|                     show={ | ||||
|                         <ChartWidget title='Users'> | ||||
|                             <UsersChart | ||||
|                                 userTrends={executiveDashboardData.userTrends} | ||||
|                                 isLoading={loading} | ||||
|                             /> | ||||
|                         </ChartWidget> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <ChartWidget title='Users per project'> | ||||
|                             <UsersPerProjectChart | ||||
|                                 projectFlagTrends={projectsData} | ||||
|                             /> | ||||
|                         </ChartWidget> | ||||
|                     } | ||||
|                 /> | ||||
|                 <Widget | ||||
|                     title='Total flags' | ||||
|                     tooltip='Active flags (not archived) that currently exist across selected projects.' | ||||
|                 > | ||||
|                     <FlagStats | ||||
|                         count={summary.total} | ||||
|                         flagsPerUser={ | ||||
|                             showAllProjects | ||||
|                                 ? (summary.total / users.total).toFixed(2) | ||||
|                                 : '' | ||||
|                         } | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <ConditionallyRender | ||||
|                     condition={showAllProjects} | ||||
|                     show={ | ||||
|                         <ChartWidget title='Number of flags'> | ||||
|                             <FlagsChart | ||||
|                                 flagTrends={executiveDashboardData.flagTrends} | ||||
|                                 isLoading={loading} | ||||
|                             /> | ||||
|                         </ChartWidget> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <ChartWidget title='Flags per project'> | ||||
|                             <FlagsProjectChart | ||||
|                         projectFlagTrends={filteredProjectFlagTrends} | ||||
|                                 projectFlagTrends={projectsData} | ||||
|                             /> | ||||
|                 </Widget> | ||||
|                 <Widget title='Average health' order={6}> | ||||
|                         </ChartWidget> | ||||
|                     } | ||||
|                 /> | ||||
|                 <Widget title='Average health'> | ||||
|                     <HealthStats | ||||
|                         // FIXME: data from API
 | ||||
|                         value={80} | ||||
|                         healthy={4} | ||||
|                         stale={1} | ||||
|                         potentiallyStale={0} | ||||
|                         value={summary.averageHealth} | ||||
|                         healthy={summary.active} | ||||
|                         stale={summary.stale} | ||||
|                         potentiallyStale={summary.potentiallyStale} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <Widget title='Health per project' order={7} span={chartSpan}> | ||||
|                     <ProjectHealthChart | ||||
|                         projectFlagTrends={filteredProjectFlagTrends} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <Widget | ||||
|                     title='Metrics over time per project' | ||||
|                     order={8} | ||||
|                     span={largeChartSpan} | ||||
|                 <ChartWidget | ||||
|                     title={ | ||||
|                         showAllProjects ? 'Healthy flags' : 'Health per project' | ||||
|                     } | ||||
|                 > | ||||
|                     <MetricsSummaryChart | ||||
|                         metricsSummaryTrends={filteredMetricsSummaryTrends} | ||||
|                     <ProjectHealthChart | ||||
|                         projectFlagTrends={projectsData} | ||||
|                         isAggregate={showAllProjects} | ||||
|                     /> | ||||
|                 </Widget> | ||||
| 
 | ||||
|                 <Widget title='Average time to production' order={9}> | ||||
|                 </ChartWidget> | ||||
|                 {/* <Widget title='Average time to production'> | ||||
|                     <TimeToProduction | ||||
|                         //FIXME: data from API
 | ||||
|                         //FIXME: data from API
 | ||||
|                         daysToProduction={5.2} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <Widget title='Time to production' order={10} span={chartSpan}> | ||||
|                     <TimeToProductionChart | ||||
|                         projectFlagTrends={filteredProjectFlagTrends} | ||||
|                     /> | ||||
|                 </Widget> | ||||
|                 <ChartWidget title='Time to production'> | ||||
|                     <TimeToProductionChart projectFlagTrends={projectsData} /> | ||||
|                 </ChartWidget> */} | ||||
|             </StyledGrid> | ||||
|             <Widget title='Metrics'> | ||||
|                 <MetricsSummaryChart metricsSummaryTrends={metricsData} /> | ||||
|             </Widget> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -1,27 +0,0 @@ | ||||
| import { type VFC } from 'react'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { ExecutiveSummarySchema } from 'openapi'; | ||||
| import { LineChart } from '../LineChart/LineChart'; | ||||
| import { useProjectChartData } from '../useProjectChartData'; | ||||
| 
 | ||||
| interface IFlagsProjectChartProps { | ||||
|     projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends']; | ||||
| } | ||||
| 
 | ||||
| export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({ | ||||
|     projectFlagTrends, | ||||
| }) => { | ||||
|     const data = useProjectChartData(projectFlagTrends); | ||||
|     return ( | ||||
|         <LineChart | ||||
|             data={data} | ||||
|             isLocalTooltip | ||||
|             overrideOptions={{ | ||||
|                 parsing: { | ||||
|                     yAxisKey: 'total', | ||||
|                     xAxisKey: 'date', | ||||
|                 }, | ||||
|             }} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -1,27 +0,0 @@ | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { type VFC } from 'react'; | ||||
| import { type ExecutiveSummarySchema } from 'openapi'; | ||||
| import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip'; | ||||
| import { LineChart } from '../LineChart/LineChart'; | ||||
| import { useProjectChartData } from '../useProjectChartData'; | ||||
| 
 | ||||
| interface IFlagsProjectChartProps { | ||||
|     projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends']; | ||||
| } | ||||
| 
 | ||||
| export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({ | ||||
|     projectFlagTrends, | ||||
| }) => { | ||||
|     const data = useProjectChartData(projectFlagTrends); | ||||
| 
 | ||||
|     return ( | ||||
|         <LineChart | ||||
|             data={data} | ||||
|             isLocalTooltip | ||||
|             TooltipComponent={HealthTooltip} | ||||
|             overrideOptions={{ | ||||
|                 parsing: { yAxisKey: 'health', xAxisKey: 'date' }, | ||||
|             }} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -1,11 +1,17 @@ | ||||
| import { VFC } from 'react'; | ||||
| import { ReactNode, VFC } from 'react'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { useFeedback } from 'component/feedbackNew/useFeedback'; | ||||
| import { ReviewsOutlined } from '@mui/icons-material'; | ||||
| import { Badge, Button, Typography } from '@mui/material'; | ||||
| import { Button, Typography } from '@mui/material'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { ShareLink } from './ShareLink/ShareLink'; | ||||
| 
 | ||||
| export const DashboardHeader: VFC = () => { | ||||
| type DashboardHeaderProps = { | ||||
|     actions?: ReactNode; | ||||
| }; | ||||
| 
 | ||||
| export const DashboardHeader: VFC<DashboardHeaderProps> = ({ actions }) => { | ||||
|     const showInactiveUsers = useUiFlag('showInactiveUsers'); | ||||
| 
 | ||||
|     const { openFeedback } = useFeedback( | ||||
| @ -34,18 +40,21 @@ export const DashboardHeader: VFC = () => { | ||||
|                         gap: theme.spacing(1), | ||||
|                     })} | ||||
|                 > | ||||
|                     <span>Insights</span> <Badge color='warning'>Beta</Badge> | ||||
|                     <span>Insights</span> <Badge color='success'>Beta</Badge> | ||||
|                 </Typography> | ||||
|             } | ||||
|             actions={ | ||||
|                 <> | ||||
|                     {actions} | ||||
|                     <ShareLink /> | ||||
|                     <Button | ||||
|                         startIcon={<ReviewsOutlined />} | ||||
|                         variant='outlined' | ||||
|                         onClick={createFeedbackContext} | ||||
|                     size='small' | ||||
|                     > | ||||
|                         Provide feedback | ||||
|                     </Button> | ||||
|                 </> | ||||
|             } | ||||
|         /> | ||||
|     ); | ||||
| @ -0,0 +1,41 @@ | ||||
| import { VFC, useState } from 'react'; | ||||
| import { Share } from '@mui/icons-material'; | ||||
| import { Box, Button, Typography } from '@mui/material'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import { LinkField } from 'component/admin/users/LinkField/LinkField'; | ||||
| 
 | ||||
| export const ShareLink: VFC = () => { | ||||
|     const [isOpen, setIsOpen] = useState(false); | ||||
|     const url = new URL(window.location.href); | ||||
|     url.searchParams.set('share', 'true'); | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Button | ||||
|                 startIcon={<Share />} | ||||
|                 variant='outlined' | ||||
|                 onClick={() => setIsOpen(true)} | ||||
|             > | ||||
|                 Share | ||||
|             </Button> | ||||
|             <Dialogue | ||||
|                 open={isOpen} | ||||
|                 onClick={() => setIsOpen(false)} | ||||
|                 primaryButtonText='Close' | ||||
|                 title='Share insights' | ||||
|             > | ||||
|                 <Box> | ||||
|                     <Typography variant='body1'> | ||||
|                         Link below will lead to insights dashboard with | ||||
|                         currently selected filter. | ||||
|                     </Typography> | ||||
|                     <LinkField | ||||
|                         inviteLink={url.toString()} | ||||
|                         successTitle='Successfully copied the link.' | ||||
|                         errorTitle='Could not copy the link.' | ||||
|                     /> | ||||
|                 </Box> | ||||
|             </Dialogue> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -92,7 +92,7 @@ const LineChartComponent: VFC<{ | ||||
|     }: { tooltip: TooltipState | null }) => ReturnType<VFC>; | ||||
| }> = ({ | ||||
|     data, | ||||
|     aspectRatio, | ||||
|     aspectRatio = 2.5, | ||||
|     cover, | ||||
|     isLocalTooltip, | ||||
|     overrideOptions, | ||||
| @ -122,8 +122,8 @@ const LineChartComponent: VFC<{ | ||||
|                 options={options} | ||||
|                 data={data} | ||||
|                 plugins={[customHighlightPlugin]} | ||||
|                 height={aspectRatio ? 100 : undefined} | ||||
|                 width={aspectRatio ? 100 * aspectRatio : undefined} | ||||
|                 height={100} | ||||
|                 width={100 * aspectRatio} | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={!cover} | ||||
| @ -62,7 +62,7 @@ export const createOptions = ( | ||||
|             x: { | ||||
|                 type: 'time', | ||||
|                 time: { | ||||
|                     unit: 'day', | ||||
|                     unit: 'week', | ||||
|                     tooltipFormat: 'PPP', | ||||
|                 }, | ||||
|                 grid: { | ||||
| @ -72,6 +72,9 @@ export const createOptions = ( | ||||
|                 ticks: { | ||||
|                     color: theme.palette.text.secondary, | ||||
|                     display: !isPlaceholder, | ||||
|                     source: 'data', | ||||
|                     maxRotation: 90, | ||||
|                     minRotation: 23.5, | ||||
|                 }, | ||||
|                 min: format(subMonths(new Date(), 3), 'yyyy-MM-dd'), | ||||
|             }, | ||||
| @ -12,13 +12,14 @@ export const legendOptions = { | ||||
|             } = chart?.legend?.options || { | ||||
|                 labels: {}, | ||||
|             }; | ||||
| 
 | ||||
|             return (chart as any)._getSortedDatasetMetas().map((meta: any) => { | ||||
|                 const style = meta.controller.getStyle( | ||||
|                     usePointStyle ? 0 : undefined, | ||||
|                 ); | ||||
|                 return { | ||||
|                     text: datasets[meta.index].label, | ||||
|                     fillStyle: style.backgroundColor, | ||||
|                     fillStyle: style.borderColor, | ||||
|                     fontColor: color, | ||||
|                     hidden: !meta.visible, | ||||
|                     lineWidth: 0, | ||||
| @ -12,17 +12,9 @@ const StyledPaper = styled(Paper)(({ theme }) => ({ | ||||
| 
 | ||||
| export const Widget: FC<{ | ||||
|     title: ReactNode; | ||||
|     order?: number; | ||||
|     span?: number; | ||||
|     tooltip?: ReactNode; | ||||
| }> = ({ title, order, children, span = 1, tooltip }) => ( | ||||
|     <StyledPaper | ||||
|         elevation={0} | ||||
|         sx={{ | ||||
|             order, | ||||
|             gridColumn: `span ${span}`, | ||||
|         }} | ||||
|     > | ||||
| }> = ({ title, children, tooltip, ...rest }) => ( | ||||
|     <StyledPaper elevation={0} {...rest}> | ||||
|         <Typography | ||||
|             variant='h3' | ||||
|             sx={(theme) => ({ | ||||
| @ -2,7 +2,8 @@ import { useMemo, type VFC } from 'react'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { useTheme } from '@mui/material'; | ||||
| import { ExecutiveSummarySchema } from 'openapi'; | ||||
| import { LineChart, NotEnoughData } from '../LineChart/LineChart'; | ||||
| import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart'; | ||||
| import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData'; | ||||
| 
 | ||||
| interface IFlagsChartProps { | ||||
|     flagTrends: ExecutiveSummarySchema['flagTrends']; | ||||
| @ -15,32 +16,7 @@ export const FlagsChart: VFC<IFlagsChartProps> = ({ | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const notEnoughData = flagTrends.length < 2; | ||||
|     const placeholderData = useMemo( | ||||
|         () => ({ | ||||
|             labels: Array.from({ length: 15 }, (_, i) => i + 1).map( | ||||
|                 (i) => | ||||
|                     new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000), | ||||
|             ), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: 'Total flags', | ||||
|                     data: [ | ||||
|                         43, 66, 55, 65, 62, 72, 75, 73, 80, 65, 62, 61, 69, 70, | ||||
|                         77, | ||||
|                     ], | ||||
|                     borderColor: theme.palette.primary.light, | ||||
|                     backgroundColor: theme.palette.primary.light, | ||||
|                 }, | ||||
|                 { | ||||
|                     label: 'Stale', | ||||
|                     data: [3, 5, 4, 6, 2, 7, 5, 3, 8, 3, 5, 11, 8, 4, 3], | ||||
|                     borderColor: theme.palette.warning.border, | ||||
|                     backgroundColor: theme.palette.warning.border, | ||||
|                 }, | ||||
|             ], | ||||
|         }), | ||||
|         [theme], | ||||
|     ); | ||||
|     const placeholderData = usePlaceholderData({ fill: true, type: 'double' }); | ||||
| 
 | ||||
|     const data = useMemo( | ||||
|         () => ({ | ||||
| @ -0,0 +1,38 @@ | ||||
| import { useMemo, type VFC } from 'react'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { ExecutiveSummarySchema } from 'openapi'; | ||||
| import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart'; | ||||
| import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData'; | ||||
| import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData'; | ||||
| 
 | ||||
| interface IFlagsProjectChartProps { | ||||
|     projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends']; | ||||
| } | ||||
| 
 | ||||
| export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({ | ||||
|     projectFlagTrends, | ||||
| }) => { | ||||
|     const placeholderData = usePlaceholderData({ | ||||
|         type: 'constant', | ||||
|     }); | ||||
| 
 | ||||
|     const data = useProjectChartData(projectFlagTrends); | ||||
|     const notEnoughData = useMemo( | ||||
|         () => (data.datasets.some((d) => d.data.length > 1) ? false : true), | ||||
|         [data], | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <LineChart | ||||
|             data={notEnoughData ? placeholderData : data} | ||||
|             isLocalTooltip | ||||
|             overrideOptions={{ | ||||
|                 parsing: { | ||||
|                     yAxisKey: 'total', | ||||
|                     xAxisKey: 'date', | ||||
|                 }, | ||||
|             }} | ||||
|             cover={notEnoughData ? <NotEnoughData /> : false} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -1,7 +1,7 @@ | ||||
| import { type VFC } from 'react'; | ||||
| import { ExecutiveSummarySchemaMetricsSummaryTrendsItem } from 'openapi'; | ||||
| import { Box, Divider, Paper, styled, Typography } from '@mui/material'; | ||||
| import { TooltipState } from '../../LineChart/ChartTooltip/ChartTooltip'; | ||||
| import { TooltipState } from '../../../components/LineChart/ChartTooltip/ChartTooltip'; | ||||
| 
 | ||||
| const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({ | ||||
|     padding: theme.spacing(2), | ||||
| @ -1,9 +1,9 @@ | ||||
| import { type VFC } from 'react'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { ExecutiveSummarySchema } from 'openapi'; | ||||
| import { LineChart } from '../LineChart/LineChart'; | ||||
| import { useMetricsSummary } from '../useMetricsSummary'; | ||||
| import { LineChart } from '../../components/LineChart/LineChart'; | ||||
| import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip'; | ||||
| import { useMetricsSummary } from '../../hooks/useMetricsSummary'; | ||||
| 
 | ||||
| interface IMetricsSummaryChartProps { | ||||
|     metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends']; | ||||
| @ -13,6 +13,7 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({ | ||||
|     metricsSummaryTrends, | ||||
| }) => { | ||||
|     const data = useMetricsSummary(metricsSummaryTrends); | ||||
| 
 | ||||
|     return ( | ||||
|         <LineChart | ||||
|             data={data} | ||||
| @ -2,8 +2,8 @@ import { type VFC } from 'react'; | ||||
| import { type ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi'; | ||||
| import { Box, Divider, Paper, Typography, styled } from '@mui/material'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { TooltipState } from '../../LineChart/ChartTooltip/ChartTooltip'; | ||||
| import { HorizontalDistributionChart } from '../../HorizontalDistributionChart/HorizontalDistributionChart'; | ||||
| import { TooltipState } from '../../../components/LineChart/ChartTooltip/ChartTooltip'; | ||||
| import { HorizontalDistributionChart } from '../../../components/HorizontalDistributionChart/HorizontalDistributionChart'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({ | ||||
| @ -0,0 +1,103 @@ | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { useMemo, type VFC } from 'react'; | ||||
| import { type ExecutiveSummarySchema } from 'openapi'; | ||||
| import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip'; | ||||
| import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData'; | ||||
| import { | ||||
|     LineChart, | ||||
|     NotEnoughData, | ||||
| } from 'component/executiveDashboard/components/LineChart/LineChart'; | ||||
| import { useTheme } from '@mui/material'; | ||||
| 
 | ||||
| interface IFlagsProjectChartProps { | ||||
|     projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends']; | ||||
|     isAggregate?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({ | ||||
|     projectFlagTrends, | ||||
|     isAggregate, | ||||
| }) => { | ||||
|     const projectsData = useProjectChartData(projectFlagTrends); | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     const aggregateHealthData = useMemo(() => { | ||||
|         const labels = Array.from( | ||||
|             new Set( | ||||
|                 projectsData.datasets.flatMap((d) => | ||||
|                     d.data.map((item) => item.week), | ||||
|                 ), | ||||
|             ), | ||||
|         ); | ||||
| 
 | ||||
|         const weeks = labels | ||||
|             .map((label) => { | ||||
|                 return projectsData.datasets | ||||
|                     .map((d) => d.data.find((item) => item.week === label)) | ||||
|                     .reduce( | ||||
|                         (acc, item) => { | ||||
|                             if (item) { | ||||
|                                 acc.total += item.total; | ||||
|                                 acc.stale += item.stale + item.potentiallyStale; | ||||
|                             } | ||||
|                             if (!acc.date) { | ||||
|                                 acc.date = item?.date; | ||||
|                             } | ||||
|                             return acc; | ||||
|                         }, | ||||
|                         { | ||||
|                             total: 0, | ||||
|                             stale: 0, | ||||
|                             week: label, | ||||
|                         } as { | ||||
|                             total: number; | ||||
|                             stale: number; | ||||
|                             week: string; | ||||
|                             date?: string; | ||||
|                         }, | ||||
|                     ); | ||||
|             }) | ||||
|             .sort((a, b) => (a.week > b.week ? 1 : -1)); | ||||
| 
 | ||||
|         return { | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: 'Health', | ||||
|                     data: weeks.map((item) => ({ | ||||
|                         health: item.total | ||||
|                             ? ((item.total - item.stale) / item.total) * 100 | ||||
|                             : undefined, | ||||
|                         date: item.date, | ||||
|                     })), | ||||
|                     borderColor: theme.palette.primary.light, | ||||
|                     fill: false, | ||||
|                     order: 3, | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|     }, [projectsData, theme]); | ||||
| 
 | ||||
|     const data = isAggregate ? aggregateHealthData : projectsData; | ||||
|     const notEnoughData = useMemo( | ||||
|         () => | ||||
|             projectsData.datasets.some((d) => d.data.length > 1) ? false : true, | ||||
|         [projectsData], | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <LineChart | ||||
|             key={isAggregate ? 'aggregate' : 'project'} | ||||
|             data={data} | ||||
|             isLocalTooltip | ||||
|             TooltipComponent={isAggregate ? undefined : HealthTooltip} | ||||
|             overrideOptions={ | ||||
|                 notEnoughData | ||||
|                     ? {} | ||||
|                     : { | ||||
|                           parsing: { yAxisKey: 'health', xAxisKey: 'date' }, | ||||
|                       } | ||||
|             } | ||||
|             cover={notEnoughData ? <NotEnoughData /> : false} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -1,8 +1,8 @@ | ||||
| import { type VFC } from 'react'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { ExecutiveSummarySchema } from 'openapi'; | ||||
| import { LineChart } from '../LineChart/LineChart'; | ||||
| import { useProjectChartData } from '../useProjectChartData'; | ||||
| import { LineChart } from '../../components/LineChart/LineChart'; | ||||
| import { useProjectChartData } from '../../hooks/useProjectChartData'; | ||||
| 
 | ||||
| interface IFlagsProjectChartProps { | ||||
|     projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends']; | ||||
| @ -12,7 +12,6 @@ export const TimeToProductionChart: VFC<IFlagsProjectChartProps> = ({ | ||||
|     projectFlagTrends, | ||||
| }) => { | ||||
|     const data = useProjectChartData(projectFlagTrends); | ||||
| 
 | ||||
|     return ( | ||||
|         <LineChart | ||||
|             data={data} | ||||
| @ -6,8 +6,9 @@ import { | ||||
|     fillGradientPrimary, | ||||
|     LineChart, | ||||
|     NotEnoughData, | ||||
| } from '../LineChart/LineChart'; | ||||
| } from '../../components/LineChart/LineChart'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData'; | ||||
| 
 | ||||
| interface IUsersChartProps { | ||||
|     userTrends: ExecutiveSummarySchema['userTrends']; | ||||
| @ -21,28 +22,7 @@ export const UsersChart: VFC<IUsersChartProps> = ({ | ||||
|     const showInactiveUsers = useUiFlag('showInactiveUsers'); | ||||
|     const theme = useTheme(); | ||||
|     const notEnoughData = userTrends.length < 2; | ||||
|     const placeholderData = useMemo( | ||||
|         () => ({ | ||||
|             labels: Array.from({ length: 15 }, (_, i) => i + 1).map( | ||||
|                 (i) => | ||||
|                     new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000), | ||||
|             ), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: 'Total users', | ||||
|                     data: [ | ||||
|                         3, 5, 15, 17, 25, 40, 47, 48, 55, 65, 62, 72, 75, 73, | ||||
|                         80, | ||||
|                     ], | ||||
|                     borderColor: theme.palette.primary.light, | ||||
|                     backgroundColor: fillGradientPrimary, | ||||
|                     fill: true, | ||||
|                     order: 3, | ||||
|                 }, | ||||
|             ], | ||||
|         }), | ||||
|         [theme], | ||||
|     ); | ||||
|     const placeholderData = usePlaceholderData({ fill: true, type: 'rising' }); | ||||
|     const data = useMemo( | ||||
|         () => ({ | ||||
|             labels: userTrends.map((item) => item.date), | ||||
| @ -0,0 +1,38 @@ | ||||
| import { useMemo, type VFC } from 'react'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { ExecutiveSummarySchema } from 'openapi'; | ||||
| import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart'; | ||||
| import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData'; | ||||
| import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData'; | ||||
| 
 | ||||
| interface IUsersPerProjectChartProps { | ||||
|     projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends']; | ||||
| } | ||||
| 
 | ||||
| export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({ | ||||
|     projectFlagTrends, | ||||
| }) => { | ||||
|     const placeholderData = usePlaceholderData({ | ||||
|         type: 'constant', | ||||
|     }); | ||||
| 
 | ||||
|     const data = useProjectChartData(projectFlagTrends); | ||||
|     const notEnoughData = useMemo( | ||||
|         () => (data.datasets.some((d) => d.data.length > 1) ? false : true), | ||||
|         [data], | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <LineChart | ||||
|             data={notEnoughData ? placeholderData : data} | ||||
|             isLocalTooltip | ||||
|             overrideOptions={{ | ||||
|                 parsing: { | ||||
|                     yAxisKey: 'users', | ||||
|                     xAxisKey: 'date', | ||||
|                 }, | ||||
|             }} | ||||
|             cover={notEnoughData ? <NotEnoughData /> : false} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -1,20 +1,6 @@ | ||||
| import { Settings } from '@mui/icons-material'; | ||||
| import { Box, Typography, styled } from '@mui/material'; | ||||
| 
 | ||||
| const StyledContent = styled(Box)(({ theme }) => ({ | ||||
|     borderRadius: `${theme.shape.borderRadiusLarge}px`, | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     maxWidth: 300, | ||||
|     padding: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled(Typography)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(3), | ||||
|     fontSize: theme.fontSizes.bodySize, | ||||
|     fontWeight: 'bold', | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
| })); | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| const StyledRingContainer = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
| @ -79,7 +65,7 @@ const StyledSettingsIcon = styled(Settings)(({ theme }) => ({ | ||||
| 
 | ||||
| interface IFlagStatsProps { | ||||
|     count: number; | ||||
|     flagsPerUser: string; | ||||
|     flagsPerUser?: string; | ||||
| } | ||||
| 
 | ||||
| export const FlagStats: React.FC<IFlagStatsProps> = ({ | ||||
| @ -94,6 +80,9 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({ | ||||
|                 </StyledRing> | ||||
|             </StyledRingContainer> | ||||
| 
 | ||||
|             <ConditionallyRender | ||||
|                 condition={flagsPerUser !== undefined && flagsPerUser !== ''} | ||||
|                 show={ | ||||
|                     <StyledInsightsContainer> | ||||
|                         <StyledTextContainer> | ||||
|                             <StyledHeaderContainer> | ||||
| @ -106,10 +95,16 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({ | ||||
|                                     Insights | ||||
|                                 </Typography> | ||||
|                             </StyledHeaderContainer> | ||||
|                     <Typography variant='body2'>Flags per user</Typography> | ||||
|                             <Typography variant='body2'> | ||||
|                                 Flags per user | ||||
|                             </Typography> | ||||
|                         </StyledTextContainer> | ||||
|                 <StyledFlagCountPerUser>{flagsPerUser}</StyledFlagCountPerUser> | ||||
|                         <StyledFlagCountPerUser> | ||||
|                             {flagsPerUser} | ||||
|                         </StyledFlagCountPerUser> | ||||
|                     </StyledInsightsContainer> | ||||
|                 } | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -3,7 +3,7 @@ import { useThemeMode } from 'hooks/useThemeMode'; | ||||
| import { useTheme } from '@mui/material'; | ||||
| 
 | ||||
| interface IHealthStatsProps { | ||||
|     value: number; | ||||
|     value?: string | number; | ||||
|     healthy: number; | ||||
|     stale: number; | ||||
|     potentiallyStale: number; | ||||
| @ -1,6 +1,6 @@ | ||||
| import { VFC } from 'react'; | ||||
| import { Typography, styled } from '@mui/material'; | ||||
| import { Gauge } from '../Gauge/Gauge'; | ||||
| import { Gauge } from '../../components/Gauge/Gauge'; | ||||
| 
 | ||||
| const StyledContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
| @ -4,7 +4,7 @@ import { Box, Typography, styled } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { HorizontalDistributionChart } from '../HorizontalDistributionChart/HorizontalDistributionChart'; | ||||
| import { HorizontalDistributionChart } from '../../components/HorizontalDistributionChart/HorizontalDistributionChart'; | ||||
| import { UserDistributionInfo } from './UserDistributionInfo'; | ||||
| 
 | ||||
| const StyledUserContainer = styled(Box)(({ theme }) => ({ | ||||
| @ -81,7 +81,11 @@ export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => { | ||||
|         <> | ||||
|             <StyledUserContainer> | ||||
|                 <StyledUserBox> | ||||
|                     <StyledUserCount variant='h2'>{count}</StyledUserCount> | ||||
|                     <StyledUserCount variant='h2'> | ||||
|                         {parseInt(`${count}`, 10) === count | ||||
|                             ? count | ||||
|                             : count.toFixed(2)} | ||||
|                     </StyledUserCount> | ||||
|                 </StyledUserBox> | ||||
|                 <StyledCustomShadow /> | ||||
|             </StyledUserContainer> | ||||
| @ -53,7 +53,7 @@ describe('useFilteredFlagTrends', () => { | ||||
|             active: 11, | ||||
|             stale: 2, | ||||
|             potentiallyStale: 1, | ||||
|             averageUsers: '2.00', | ||||
|             averageUsers: 2, | ||||
|             averageHealth: '79', | ||||
|         }); | ||||
|     }); | ||||
| @ -79,7 +79,7 @@ describe('useFilteredFlagTrends', () => { | ||||
|             active: 5, | ||||
|             stale: 0, | ||||
|             potentiallyStale: 0, | ||||
|             averageUsers: '0.00', | ||||
|             averageUsers: 0, | ||||
|             averageHealth: '100', | ||||
|         }); | ||||
|     }); | ||||
| @ -104,7 +104,7 @@ describe('useFilteredFlagTrends', () => { | ||||
|                     active: 5, | ||||
|                     stale: 0, | ||||
|                     potentiallyStale: 0, | ||||
|                     users: 2, | ||||
|                     users: 3, | ||||
|                     date: '', | ||||
|                 }, | ||||
|             ]), | ||||
| @ -115,8 +115,34 @@ describe('useFilteredFlagTrends', () => { | ||||
|             active: 10, | ||||
|             stale: 0, | ||||
|             potentiallyStale: 0, | ||||
|             averageUsers: '1.00', | ||||
|             averageUsers: 1.5, | ||||
|             averageHealth: '100', | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should set health of a project without feature toggles to undefined', () => { | ||||
|         const { result } = renderHook(() => | ||||
|             useFilteredFlagsSummary([ | ||||
|                 { | ||||
|                     week: '2024-01', | ||||
|                     project: 'project1', | ||||
|                     total: 0, | ||||
|                     active: 0, | ||||
|                     stale: 0, | ||||
|                     potentiallyStale: 0, | ||||
|                     users: 0, | ||||
|                     date: '', | ||||
|                 }, | ||||
|             ]), | ||||
|         ); | ||||
| 
 | ||||
|         expect(result.current).toEqual({ | ||||
|             total: 0, | ||||
|             active: 0, | ||||
|             stale: 0, | ||||
|             potentiallyStale: 0, | ||||
|             averageUsers: 0, | ||||
|             averageHealth: undefined, | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { useMemo } from 'react'; | ||||
| import { ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi'; | ||||
| 
 | ||||
| // NOTE: should we move project filtering to the backend?
 | ||||
| export const useFilteredFlagsSummary = ( | ||||
|     filteredProjectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[], | ||||
| ) => | ||||
| @ -14,12 +15,11 @@ export const useFilteredFlagsSummary = ( | ||||
|             (summary) => summary.week === lastWeekId, | ||||
|         ); | ||||
| 
 | ||||
|         const averageUsers = ( | ||||
|         const averageUsers = | ||||
|             lastWeekSummary.reduce( | ||||
|                 (acc, current) => acc + (current.users || 0), | ||||
|                 0, | ||||
|             ) / lastWeekSummary.length | ||||
|         ).toFixed(2); | ||||
|             ) / lastWeekSummary.length || 0; | ||||
| 
 | ||||
|         const sum = lastWeekSummary.reduce( | ||||
|             (acc, current) => ({ | ||||
| @ -41,6 +41,8 @@ export const useFilteredFlagsSummary = ( | ||||
|         return { | ||||
|             ...sum, | ||||
|             averageUsers, | ||||
|             averageHealth: ((sum.active / (sum.total || 1)) * 100).toFixed(0), | ||||
|             averageHealth: sum.total | ||||
|                 ? ((sum.active / (sum.total || 1)) * 100).toFixed(0) | ||||
|                 : undefined, | ||||
|         }; | ||||
|     }, [filteredProjectFlagTrends]); | ||||
|  | ||||
| @ -4,7 +4,7 @@ import { | ||||
|     ExecutiveSummarySchema, | ||||
|     ExecutiveSummarySchemaMetricsSummaryTrendsItem, | ||||
| } from 'openapi'; | ||||
| import { getProjectColor } from './executive-dashboard-utils'; | ||||
| import { getProjectColor } from '../executive-dashboard-utils'; | ||||
| 
 | ||||
| type MetricsSummaryTrends = ExecutiveSummarySchema['metricsSummaryTrends']; | ||||
| 
 | ||||
| @ -0,0 +1,64 @@ | ||||
| import { useTheme } from '@mui/material'; | ||||
| import { useMemo } from 'react'; | ||||
| import { fillGradientPrimary } from '../components/LineChart/LineChart'; | ||||
| 
 | ||||
| export const usePlaceholderData = ({ | ||||
|     fill = false, | ||||
|     type = 'constant', | ||||
| }: { | ||||
|     fill?: boolean; | ||||
|     type?: 'rising' | 'constant' | 'double'; | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     return useMemo( | ||||
|         () => ({ | ||||
|             labels: Array.from({ length: 15 }, (_, i) => i + 1).map( | ||||
|                 (i) => | ||||
|                     new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000), | ||||
|             ), | ||||
|             datasets: | ||||
|                 type === 'double' | ||||
|                     ? [ | ||||
|                           { | ||||
|                               label: 'Total flags', | ||||
|                               data: [ | ||||
|                                   43, 66, 55, 65, 62, 72, 75, 73, 80, 65, 62, | ||||
|                                   61, 69, 70, 77, | ||||
|                               ], | ||||
|                               borderColor: theme.palette.primary.light, | ||||
|                               backgroundColor: theme.palette.primary.light, | ||||
|                           }, | ||||
|                           { | ||||
|                               label: 'Stale', | ||||
|                               data: [ | ||||
|                                   3, 5, 4, 6, 2, 7, 5, 3, 8, 3, 5, 11, 8, 4, 3, | ||||
|                               ], | ||||
|                               borderColor: theme.palette.warning.border, | ||||
|                               backgroundColor: theme.palette.warning.border, | ||||
|                           }, | ||||
|                       ] | ||||
|                     : [ | ||||
|                           { | ||||
|                               label: '', | ||||
|                               data: | ||||
|                                   type === 'rising' | ||||
|                                       ? [ | ||||
|                                               3, 5, 15, 17, 25, 40, 47, 48, 55, | ||||
|                                               65, 62, 72, 75, 73, 80, | ||||
|                                           ] | ||||
|                                       : [ | ||||
|                                               54, 52, 53, 49, 54, 50, 47, 46, | ||||
|                                               51, 51, 50, 51, 49, 49, 51, | ||||
|                                           ], | ||||
|                               borderColor: theme.palette.primary.light, | ||||
|                               backgroundColor: fill | ||||
|                                   ? fillGradientPrimary | ||||
|                                   : theme.palette.primary.light, | ||||
|                               fill, | ||||
|                           }, | ||||
|                       ], | ||||
|         }), | ||||
|         [theme, fill], | ||||
|     ); | ||||
| }; | ||||
| @ -2,8 +2,8 @@ import { useMemo } from 'react'; | ||||
| import { | ||||
|     ExecutiveSummarySchema, | ||||
|     ExecutiveSummarySchemaProjectFlagTrendsItem, | ||||
| } from '../../openapi'; | ||||
| import { getProjectColor } from './executive-dashboard-utils'; | ||||
| } from '../../../openapi'; | ||||
| import { getProjectColor } from '../executive-dashboard-utils'; | ||||
| import { useTheme } from '@mui/material'; | ||||
| 
 | ||||
| type ProjectFlagTrends = ExecutiveSummarySchema['projectFlagTrends']; | ||||
| @ -1,17 +0,0 @@ | ||||
| /** | ||||
|  * Generated by Orval | ||||
|  * Do not edit manually. | ||||
|  * See `gen:api` script in package.json | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * The name of this action. | ||||
|  */ | ||||
| export type ApplicationOverviewIssuesSchemaType = | ||||
|     (typeof ApplicationOverviewIssuesSchemaType)[keyof typeof ApplicationOverviewIssuesSchemaType]; | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-redeclare
 | ||||
| export const ApplicationOverviewIssuesSchemaType = { | ||||
|     missingFeatures: 'missingFeatures', | ||||
|     missingStrategies: 'missingStrategies', | ||||
| } as const; | ||||
| @ -575,6 +575,7 @@ export * from './getAllToggles403'; | ||||
| export * from './getApiTokensByName401'; | ||||
| export * from './getApiTokensByName403'; | ||||
| export * from './getApplication404'; | ||||
| export * from './getApplicationEnvironmentInstances404'; | ||||
| export * from './getApplicationOverview404'; | ||||
| export * from './getApplicationsParams'; | ||||
| export * from './getArchivedFeatures401'; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user