diff --git a/frontend/src/component/insights/InsightsCharts.styles.ts b/frontend/src/component/insights/InsightsCharts.styles.ts index 4d9dfa9698..f744a4f138 100644 --- a/frontend/src/component/insights/InsightsCharts.styles.ts +++ b/frontend/src/component/insights/InsightsCharts.styles.ts @@ -3,46 +3,60 @@ import { Box, Paper, styled, Typography } from '@mui/material'; export const StyledContainer = styled(Box)(({ theme }) => ({ display: 'flex', 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 }) => ({ borderRadius: `${theme.shape.borderRadiusLarge}px`, boxShadow: 'none', display: 'flex', - flexWrap: 'wrap', - [theme.breakpoints.up('md')]: { - flexDirection: 'row', - flexWrap: 'nowrap', + flexDirection: 'column', + overflow: 'hidden', + border: `1px solid ${theme.palette.divider}`, + transition: 'box-shadow 0.2s ease', + '&:hover': { + boxShadow: theme.shadows[2], }, })); export const StyledWidgetContent = styled(Box)(({ theme }) => ({ - padding: theme.spacing(3), + padding: theme.spacing(2), width: '100%', })); export const StyledWidgetStats = styled(Box)<{ width?: number; padding?: number; -}>(({ theme, width = 300, padding = 3 }) => ({ +}>(({ theme, width = 300, padding = 2 }) => ({ display: 'flex', flexDirection: 'column', - gap: theme.spacing(2), + gap: theme.spacing(1.5), padding: theme.spacing(padding), - minWidth: '100%', - [theme.breakpoints.up('md')]: { - minWidth: `${width}px`, - borderRight: `1px solid ${theme.palette.background.application}`, - }, + borderBottom: `1px solid ${theme.palette.divider}`, + backgroundColor: theme.palette.background.elevation1, })); export const StyledChartContainer = styled(Box)(({ theme }) => ({ position: 'relative', minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128 - flexGrow: 1, - margin: 'auto 0', - padding: theme.spacing(3), + width: '100%', + padding: theme.spacing(2), + height: '300px', + '& canvas': { + maxHeight: '280px', + }, })); export const StatsExplanation = styled(Typography)(({ theme }) => ({ diff --git a/frontend/src/component/insights/sections/PerformanceInsights.tsx b/frontend/src/component/insights/sections/PerformanceInsights.tsx index efc4d4895b..a817ff0ce3 100644 --- a/frontend/src/component/insights/sections/PerformanceInsights.tsx +++ b/frontend/src/component/insights/sections/PerformanceInsights.tsx @@ -6,29 +6,18 @@ import { usePersistentTableState } from 'hooks/usePersistentTableState'; import type { FC } from 'react'; import { withDefault } from 'use-query-params'; import { FilterItemParam } from 'utils/serializeQueryParams'; -import { WidgetTitle } from 'component/insights/components/WidgetTitle/WidgetTitle'; import { FlagsChart } from 'component/insights/componentsChart/FlagsChart/FlagsChart'; import { FlagsProjectChart } from 'component/insights/componentsChart/FlagsProjectChart/FlagsProjectChart'; import { MetricsSummaryChart } from 'component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart'; import { ProjectHealthChart } from 'component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart'; 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 { InsightsSection } from 'component/insights/sections/InsightsSection'; import { InsightsFilters } from 'component/insights/InsightsFilters'; -import { - StyledChartContainer, - StyledWidget, - StyledWidgetContent, - StyledWidgetStats, - StatsExplanation, -} from '../InsightsCharts.styles'; +import { Box, Typography, GlobalStyles, Tooltip, Link } from '@mui/material'; 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'; -import { CreationArchiveStats } from '../componentsStat/CreationArchiveStats/CreationArchiveStats.tsx'; export const PerformanceInsights: FC = () => { const statePrefix = 'performance-'; @@ -83,7 +72,50 @@ export const PerformanceInsights: FC = () => { 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 ( + <> + { /> } > + *': { + minWidth: 0, + } + }}> {isLifecycleGraphsEnabled && isEnterprise() ? ( - - - - - - How often do flags go live in production? - - - + + theme.palette.mode === 'light' + ? 'linear-gradient(to right, #f8f9fa, #ffffff)' + : theme.palette.background.default + }}> + + + + Production Deployments + + + + - - + + ) : null} {isLifecycleGraphsEnabled && isEnterprise() ? ( - - - - - - + + theme.palette.mode === 'light' + ? 'linear-gradient(to right, #f8f9fa, #ffffff)' + : theme.palette.background.default + }}> + + + + + Lifecycle Balance + + + + + + {loading ? '...' : `${currentRatio}%`} + + + + View cleanup + + + + + - - + + ) : null} {showAllProjects ? ( - - - - - - + + theme.palette.mode === 'light' + ? 'linear-gradient(to right, #f8f9fa, #ffffff)' + : theme.palette.background.default + }}> + + + + + Flags + + + + + + {loading ? '...' : flagsTotal} + + + + + + - - + + ) : ( - - - - - - + + theme.palette.mode === 'light' + ? 'linear-gradient(to right, #f8f9fa, #ffffff)' + : theme.palette.background.default + }}> + + + + + Flags + + + + + + {loading ? '...' : summary.total} + + + + + + - - + + )} {isEnterprise() ? ( - - - - } - /> - - + + theme.palette.mode === 'light' + ? 'linear-gradient(to right, #f8f9fa, #ffffff)' + : theme.palette.background.default + }}> + + + + + Technical Debt + + + + + 50 ? 'error.main' : 'primary.main', + fontWeight: 600, + cursor: 'help' + }} + > + {loading ? '...' : `${summary.technicalDebt}%`} + + + + Learn more + + + + + - - + + ) : null} {isEnterprise() ? ( - <> - - - - - - - - - - - - - - - + + theme.palette.mode === 'light' + ? 'linear-gradient(to right, #f8f9fa, #ffffff)' + : theme.palette.background.default + }}> + + + + + Flag Evaluation Metrics + + + + + ? + + + + + + + + ) : null} + {isEnterprise() ? ( + + theme.palette.mode === 'light' + ? 'linear-gradient(to right, #f8f9fa, #ffffff)' + : theme.palette.background.default + }}> + + + + + Updates per Environment Type + + + + + ? + + + + + + + + + ) : null} + + ); };