1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +02:00

feat: compact graphs

This commit is contained in:
FredrikOseberg 2025-08-15 16:12:14 +02:00
parent b655291e85
commit 175f7b801e
No known key found for this signature in database
GPG Key ID: 282FD8A6D8F9BCF0
2 changed files with 468 additions and 130 deletions

View File

@ -3,46 +3,60 @@ import { Box, Paper, styled, Typography } from '@mui/material';
export const StyledContainer = styled(Box)(({ theme }) => ({ export const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: theme.spacing(4), gap: theme.spacing(3),
}));
export const StyledCompactCard = styled(Paper)(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(2),
boxShadow: 'none',
border: `1px solid ${theme.palette.divider}`,
transition: 'all 0.2s ease',
'&:hover': {
boxShadow: theme.shadows[2],
transform: 'translateY(-2px)',
},
})); }));
export const StyledWidget = styled(Paper)(({ theme }) => ({ export const StyledWidget = styled(Paper)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusLarge}px`, borderRadius: `${theme.shape.borderRadiusLarge}px`,
boxShadow: 'none', boxShadow: 'none',
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexDirection: 'column',
[theme.breakpoints.up('md')]: { overflow: 'hidden',
flexDirection: 'row', border: `1px solid ${theme.palette.divider}`,
flexWrap: 'nowrap', transition: 'box-shadow 0.2s ease',
'&:hover': {
boxShadow: theme.shadows[2],
}, },
})); }));
export const StyledWidgetContent = styled(Box)(({ theme }) => ({ export const StyledWidgetContent = styled(Box)(({ theme }) => ({
padding: theme.spacing(3), padding: theme.spacing(2),
width: '100%', width: '100%',
})); }));
export const StyledWidgetStats = styled(Box)<{ export const StyledWidgetStats = styled(Box)<{
width?: number; width?: number;
padding?: number; padding?: number;
}>(({ theme, width = 300, padding = 3 }) => ({ }>(({ theme, width = 300, padding = 2 }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: theme.spacing(2), gap: theme.spacing(1.5),
padding: theme.spacing(padding), padding: theme.spacing(padding),
minWidth: '100%', borderBottom: `1px solid ${theme.palette.divider}`,
[theme.breakpoints.up('md')]: { backgroundColor: theme.palette.background.elevation1,
minWidth: `${width}px`,
borderRight: `1px solid ${theme.palette.background.application}`,
},
})); }));
export const StyledChartContainer = styled(Box)(({ theme }) => ({ export const StyledChartContainer = styled(Box)(({ theme }) => ({
position: 'relative', position: 'relative',
minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128 minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128
flexGrow: 1, width: '100%',
margin: 'auto 0', padding: theme.spacing(2),
padding: theme.spacing(3), height: '300px',
'& canvas': {
maxHeight: '280px',
},
})); }));
export const StatsExplanation = styled(Typography)(({ theme }) => ({ export const StatsExplanation = styled(Typography)(({ theme }) => ({

View File

@ -6,29 +6,18 @@ import { usePersistentTableState } from 'hooks/usePersistentTableState';
import type { FC } from 'react'; import type { FC } from 'react';
import { withDefault } from 'use-query-params'; import { withDefault } from 'use-query-params';
import { FilterItemParam } from 'utils/serializeQueryParams'; import { FilterItemParam } from 'utils/serializeQueryParams';
import { WidgetTitle } from 'component/insights/components/WidgetTitle/WidgetTitle';
import { FlagsChart } from 'component/insights/componentsChart/FlagsChart/FlagsChart'; import { FlagsChart } from 'component/insights/componentsChart/FlagsChart/FlagsChart';
import { FlagsProjectChart } from 'component/insights/componentsChart/FlagsProjectChart/FlagsProjectChart'; import { FlagsProjectChart } from 'component/insights/componentsChart/FlagsProjectChart/FlagsProjectChart';
import { MetricsSummaryChart } from 'component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart'; import { MetricsSummaryChart } from 'component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart';
import { ProjectHealthChart } from 'component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart'; import { ProjectHealthChart } from 'component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart';
import { UpdatesPerEnvironmentTypeChart } from 'component/insights/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart'; import { UpdatesPerEnvironmentTypeChart } from 'component/insights/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart';
import { FlagStats } from 'component/insights/componentsStat/FlagStats/FlagStats';
import { HealthStats } from 'component/insights/componentsStat/HealthStats/HealthStats';
import { useInsightsData } from 'component/insights/hooks/useInsightsData'; import { useInsightsData } from 'component/insights/hooks/useInsightsData';
import { InsightsSection } from 'component/insights/sections/InsightsSection'; import { InsightsSection } from 'component/insights/sections/InsightsSection';
import { InsightsFilters } from 'component/insights/InsightsFilters'; import { InsightsFilters } from 'component/insights/InsightsFilters';
import { import { Box, Typography, GlobalStyles, Tooltip, Link } from '@mui/material';
StyledChartContainer,
StyledWidget,
StyledWidgetContent,
StyledWidgetStats,
StatsExplanation,
} from '../InsightsCharts.styles';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { NewProductionFlagsChart } from '../componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx'; import { NewProductionFlagsChart } from '../componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx';
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx'; import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx';
import { CreationArchiveStats } from '../componentsStat/CreationArchiveStats/CreationArchiveStats.tsx';
export const PerformanceInsights: FC = () => { export const PerformanceInsights: FC = () => {
const statePrefix = 'performance-'; const statePrefix = 'performance-';
@ -83,7 +72,50 @@ export const PerformanceInsights: FC = () => {
const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs'); const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs');
function getCurrentArchiveRatio() {
if (!groupedCreationArchiveData || Object.keys(groupedCreationArchiveData).length === 0) {
return 0;
}
let totalArchived = 0;
let totalCreated = 0;
Object.values(groupedCreationArchiveData).forEach((projectData) => {
const latestData = projectData[projectData.length - 1];
if (latestData) {
totalArchived += latestData.archivedFlags || 0;
const createdSum = latestData.createdFlags
? Object.values(latestData.createdFlags).reduce(
(sum: number, count: number) => sum + count,
0,
)
: 0;
totalCreated += createdSum;
}
});
return totalCreated > 0
? Math.round((totalArchived / totalCreated) * 100)
: 0;
}
const currentRatio = getCurrentArchiveRatio();
return ( return (
<>
<GlobalStyles styles={{
'@keyframes pulse': {
'0%': {
boxShadow: '0 0 0 0 rgba(16, 185, 129, 0.7)',
},
'70%': {
boxShadow: '0 0 0 6px rgba(16, 185, 129, 0)',
},
'100%': {
boxShadow: '0 0 0 0 rgba(16, 185, 129, 0)',
},
}
}} />
<InsightsSection <InsightsSection
title='Performance insights' title='Performance insights'
filters={ filters={
@ -94,143 +126,435 @@ export const PerformanceInsights: FC = () => {
/> />
} }
> >
<Box sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
md: 'repeat(2, 1fr)'
},
gap: 2,
'& > *': {
minWidth: 0,
}
}}>
{isLifecycleGraphsEnabled && isEnterprise() ? ( {isLifecycleGraphsEnabled && isEnterprise() ? (
<StyledWidget> <Box sx={{
<StyledWidgetStats width={275}> borderRadius: 3,
<WidgetTitle title='New flags in production' /> overflow: 'hidden',
<StatsExplanation> backgroundColor: 'background.paper',
<Lightbulb color='primary' /> border: '1px solid',
How often do flags go live in production? borderColor: 'divider',
</StatsExplanation> transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
</StyledWidgetStats> '&:hover': {
<StyledChartContainer> boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
transform: 'translateY(-4px)',
}
}}>
<Box sx={{
p: 2,
borderBottom: '1px solid',
borderColor: 'divider',
background: (theme) => theme.palette.mode === 'light'
? 'linear-gradient(to right, #f8f9fa, #ffffff)'
: theme.palette.background.default
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#10b981',
animation: 'pulse 2s infinite'
}} />
<Typography variant='body1' sx={{ fontWeight: 600, letterSpacing: '-0.01em' }}>
Production Deployments
</Typography>
</Box>
</Box>
<Box sx={{ p: 3 }}>
<NewProductionFlagsChart <NewProductionFlagsChart
lifecycleTrends={groupedLifecycleData} lifecycleTrends={groupedLifecycleData}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading} isLoading={loading}
/> />
</StyledChartContainer> </Box>
</StyledWidget> </Box>
) : null} ) : null}
{isLifecycleGraphsEnabled && isEnterprise() ? ( {isLifecycleGraphsEnabled && isEnterprise() ? (
<StyledWidget> <Box sx={{
<StyledWidgetStats width={275}> borderRadius: 3,
<WidgetTitle title='Flags created vs archived' /> overflow: 'hidden',
<CreationArchiveStats backgroundColor: 'background.paper',
groupedCreationArchiveData={ border: '1px solid',
groupedCreationArchiveData borderColor: 'divider',
} transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
isLoading={loading} '&:hover': {
/> boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
</StyledWidgetStats> transform: 'translateY(-4px)',
<StyledChartContainer> }
}}>
<Box sx={{
p: 2,
borderBottom: '1px solid',
borderColor: 'divider',
background: (theme) => theme.palette.mode === 'light'
? 'linear-gradient(to right, #f8f9fa, #ffffff)'
: theme.palette.background.default
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#8b5cf6',
}}/>
<Typography variant='body1' sx={{ fontWeight: 600, letterSpacing: '-0.01em' }}>
Lifecycle Balance
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Tooltip title="Current ratio of archived flags to created flags" placement="top">
<Typography
variant='body2'
sx={{
color: 'primary.main',
fontWeight: 600,
cursor: 'help'
}}
>
{loading ? '...' : `${currentRatio}%`}
</Typography>
</Tooltip>
<Link
href='/search?lifecycle=IS:completed'
sx={{
color: 'primary.main',
textDecoration: 'none',
fontSize: '0.875rem',
'&:hover': {
textDecoration: 'underline'
}
}}
>
View cleanup
</Link>
</Box>
</Box>
</Box>
<Box sx={{ p: 3 }}>
<CreationArchiveChart <CreationArchiveChart
creationArchiveTrends={groupedCreationArchiveData} creationArchiveTrends={groupedCreationArchiveData}
isLoading={loading} isLoading={loading}
/> />
</StyledChartContainer> </Box>
</StyledWidget> </Box>
) : null} ) : null}
{showAllProjects ? ( {showAllProjects ? (
<StyledWidget> <Box sx={{
<StyledWidgetStats width={275}> borderRadius: 3,
<WidgetTitle title='Flags' /> overflow: 'hidden',
<FlagStats backgroundColor: 'background.paper',
count={flagsTotal} border: '1px solid',
flagsPerUser={getFlagsPerUser( borderColor: 'divider',
flagsTotal, transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
usersTotal, '&:hover': {
)} boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
isLoading={loading} transform: 'translateY(-4px)',
/> }
</StyledWidgetStats> }}>
<StyledChartContainer> <Box sx={{
p: 2,
borderBottom: '1px solid',
borderColor: 'divider',
background: (theme) => theme.palette.mode === 'light'
? 'linear-gradient(to right, #f8f9fa, #ffffff)'
: theme.palette.background.default
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#3b82f6',
}}/>
<Typography variant='body1' sx={{ fontWeight: 600, letterSpacing: '-0.01em' }}>
Flags
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Tooltip title={`Total: ${flagsTotal} flags, ${getFlagsPerUser(flagsTotal, usersTotal)} per user`} placement="top">
<Typography
variant='body2'
sx={{
color: 'primary.main',
fontWeight: 600,
cursor: 'help'
}}
>
{loading ? '...' : flagsTotal}
</Typography>
</Tooltip>
</Box>
</Box>
</Box>
<Box sx={{ p: 3 }}>
<FlagsChart <FlagsChart
flagTrends={flagTrends} flagTrends={flagTrends}
isLoading={loading} isLoading={loading}
/> />
</StyledChartContainer> </Box>
</StyledWidget> </Box>
) : ( ) : (
<StyledWidget> <Box sx={{
<StyledWidgetStats width={275}> borderRadius: 3,
<WidgetTitle title='Flags' /> overflow: 'hidden',
<FlagStats backgroundColor: 'background.paper',
count={summary.total} border: '1px solid',
flagsPerUser={''} borderColor: 'divider',
isLoading={loading} transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
/> '&:hover': {
</StyledWidgetStats> boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
<StyledChartContainer> transform: 'translateY(-4px)',
}
}}>
<Box sx={{
p: 2,
borderBottom: '1px solid',
borderColor: 'divider',
background: (theme) => theme.palette.mode === 'light'
? 'linear-gradient(to right, #f8f9fa, #ffffff)'
: theme.palette.background.default
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#3b82f6',
}}/>
<Typography variant='body1' sx={{ fontWeight: 600, letterSpacing: '-0.01em' }}>
Flags
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Tooltip title={`Total: ${summary.total} flags in selected projects`} placement="top">
<Typography
variant='body2'
sx={{
color: 'primary.main',
fontWeight: 600,
cursor: 'help'
}}
>
{loading ? '...' : summary.total}
</Typography>
</Tooltip>
</Box>
</Box>
</Box>
<Box sx={{ p: 3 }}>
<FlagsProjectChart <FlagsProjectChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isLoading={loading} isLoading={loading}
/> />
</StyledChartContainer> </Box>
</StyledWidget> </Box>
)} )}
{isEnterprise() ? ( {isEnterprise() ? (
<StyledWidget> <Box sx={{
<StyledWidgetStats width={350} padding={0}> borderRadius: 3,
<HealthStats overflow: 'hidden',
value={summary.averageHealth} backgroundColor: 'background.paper',
technicalDebt={summary.technicalDebt} border: '1px solid',
healthy={summary.active} borderColor: 'divider',
stale={summary.stale} transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
potentiallyStale={summary.potentiallyStale} '&:hover': {
title={ boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
<WidgetTitle transform: 'translateY(-4px)',
title='Technical debt' }
tooltip={ }}>
'Percentage of flags that are stale or potentially stale.' <Box sx={{
} p: 2,
/> borderBottom: '1px solid',
} borderColor: 'divider',
/> background: (theme) => theme.palette.mode === 'light'
</StyledWidgetStats> ? 'linear-gradient(to right, #f8f9fa, #ffffff)'
<StyledChartContainer> : theme.palette.background.default
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#ef4444',
}}/>
<Typography variant='body1' sx={{ fontWeight: 600, letterSpacing: '-0.01em' }}>
Technical Debt
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Tooltip title={`${summary.technicalDebt}% of flags are stale or potentially stale. Healthy: ${summary.active}, Stale: ${summary.stale}, Potentially stale: ${summary.potentiallyStale}`} placement="top">
<Typography
variant='body2'
sx={{
color: summary.technicalDebt > 50 ? 'error.main' : 'primary.main',
fontWeight: 600,
cursor: 'help'
}}
>
{loading ? '...' : `${summary.technicalDebt}%`}
</Typography>
</Tooltip>
<Link
href='https://docs.getunleash.io/reference/insights#health'
target="_blank"
sx={{
color: 'primary.main',
textDecoration: 'none',
fontSize: '0.875rem',
'&:hover': {
textDecoration: 'underline'
}
}}
>
Learn more
</Link>
</Box>
</Box>
</Box>
<Box sx={{ p: 3 }}>
<ProjectHealthChart <ProjectHealthChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading} isLoading={loading}
/> />
</StyledChartContainer> </Box>
</StyledWidget> </Box>
) : null} ) : null}
{isEnterprise() ? ( {isEnterprise() ? (
<> <Box sx={{
<StyledWidget> borderRadius: 3,
<StyledWidgetContent> overflow: 'hidden',
<WidgetTitle backgroundColor: 'background.paper',
title='Flag evaluation metrics' border: '1px solid',
tooltip='Summary of all flag evaluations reported by SDKs.' borderColor: 'divider',
/> transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
<StyledChartContainer> '&:hover': {
<MetricsSummaryChart boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
metricsSummaryTrends={groupedMetricsData} transform: 'translateY(-4px)',
allDatapointsSorted={allMetricsDatapoints} }
isAggregate={showAllProjects} }}>
isLoading={loading} <Box sx={{
/> p: 2,
</StyledChartContainer> borderBottom: '1px solid',
</StyledWidgetContent> borderColor: 'divider',
</StyledWidget> background: (theme) => theme.palette.mode === 'light'
<StyledWidget> ? 'linear-gradient(to right, #f8f9fa, #ffffff)'
<StyledWidgetContent> : theme.palette.background.default
<WidgetTitle }}>
title='Updates per environment type' <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
tooltip='Summary of all configuration updates per environment type.' <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
/> <Box sx={{
<UpdatesPerEnvironmentTypeChart width: 8,
environmentTypeTrends={environmentTypeTrends} height: 8,
isLoading={loading} borderRadius: '50%',
/> backgroundColor: '#f59e0b',
</StyledWidgetContent> }}/>
</StyledWidget> <Typography variant='body1' sx={{ fontWeight: 600, letterSpacing: '-0.01em' }}>
</> Flag Evaluation Metrics
</Typography>
</Box>
<Tooltip title="Summary of all flag evaluations reported by SDKs" placement="top">
<Box sx={{
width: 20,
height: 20,
borderRadius: '50%',
backgroundColor: 'action.hover',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'help'
}}>
<Typography variant='caption' sx={{ fontWeight: 600 }}>?</Typography>
</Box>
</Tooltip>
</Box>
</Box>
<Box sx={{ p: 3 }}>
<MetricsSummaryChart
metricsSummaryTrends={groupedMetricsData}
allDatapointsSorted={allMetricsDatapoints}
isAggregate={showAllProjects}
isLoading={loading}
/>
</Box>
</Box>
) : null} ) : null}
{isEnterprise() ? (
<Box sx={{
borderRadius: 3,
overflow: 'hidden',
backgroundColor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
transform: 'translateY(-4px)',
}
}}>
<Box sx={{
p: 2,
borderBottom: '1px solid',
borderColor: 'divider',
background: (theme) => theme.palette.mode === 'light'
? 'linear-gradient(to right, #f8f9fa, #ffffff)'
: theme.palette.background.default
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#06b6d4',
}}/>
<Typography variant='body1' sx={{ fontWeight: 600, letterSpacing: '-0.01em' }}>
Updates per Environment Type
</Typography>
</Box>
<Tooltip title="Summary of all configuration updates per environment type" placement="top">
<Box sx={{
width: 20,
height: 20,
borderRadius: '50%',
backgroundColor: 'action.hover',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'help'
}}>
<Typography variant='caption' sx={{ fontWeight: 600 }}>?</Typography>
</Box>
</Tooltip>
</Box>
</Box>
<Box sx={{ p: 3 }}>
<UpdatesPerEnvironmentTypeChart
environmentTypeTrends={environmentTypeTrends}
isLoading={loading}
/>
</Box>
</Box>
) : null}
</Box>
</InsightsSection> </InsightsSection>
</>
); );
}; };