1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

chore: move project stats (#6602)

Add stats to project insights. Will follow up with UI enhancements in a
later iteration.

<img width="1408" alt="Skjermbilde 2024-03-19 kl 13 19 18"
src="https://github.com/Unleash/unleash/assets/16081982/f4726635-99eb-4f27-8c31-5c6d402f2ceb">
This commit is contained in:
Fredrik Strand Oseberg 2024-03-19 14:27:42 +01:00 committed by GitHub
parent aeb6291863
commit bb847e2935
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 364 additions and 3 deletions

View File

@ -3,6 +3,7 @@ import { ChangeRequests } from './ChangeRequests/ChangeRequests';
import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges';
import { ProjectHealth } from './ProjectHealth/ProjectHealth';
import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed';
import { ProjectInsightsStats } from './ProjectInsightsStats/ProjectInsightsStats';
const Container = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@ -16,7 +17,7 @@ const Grid = styled(Box)(({ theme }) => ({
gridTemplateColumns: 'repeat(10, 1fr)',
}));
const FullWidthContainer = styled(Container)(() => ({
const FullWidthContainer = styled(Box)(() => ({
gridColumn: '1 / -1',
}));
@ -32,12 +33,24 @@ const NarrowContainer = styled(Container)(() => ({
gridColumn: 'span 2',
}));
const statsData = {
stats: {
archivedCurrentWindow: 5,
archivedPastWindow: 3,
avgTimeToProdCurrentWindow: 2.5,
createdCurrentWindow: 7,
createdPastWindow: 4,
projectActivityCurrentWindow: 10,
projectActivityPastWindow: 8,
projectMembersAddedCurrentWindow: 2,
},
};
export const ProjectInsights = () => {
return (
<Grid>
<FullWidthContainer>
Total changes / avg time to production / feature flags /stale
flags
<ProjectInsightsStats {...statsData} />
</FullWidthContainer>
<MediumWideContainer>
<ProjectHealth />

View File

@ -0,0 +1,82 @@
import { type FC, useState } from 'react';
import Close from '@mui/icons-material/Close';
import HelpOutline from '@mui/icons-material/HelpOutline';
import {
Box,
IconButton,
Popper,
Paper,
ClickAwayListener,
styled,
} from '@mui/material';
import { Feedback } from 'component/common/Feedback/Feedback';
interface IHelpPopperProps {
id: string;
}
const StyledPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(3, 3),
maxWidth: '350px',
borderRadius: `${theme.shape.borderRadiusMedium}px`,
border: `1px solid ${theme.palette.neutral.border}`,
fontSize: theme.typography.body2.fontSize,
}));
export const HelpPopper: FC<IHelpPopperProps> = ({ children, id }) => {
const [anchor, setAnchorEl] = useState<null | Element>(null);
const onOpen = (event: React.FormEvent<HTMLButtonElement>) =>
setAnchorEl(event.currentTarget);
const onClose = () => setAnchorEl(null);
const open = Boolean(anchor);
return (
<Box
sx={{
position: 'absolute',
top: (theme) => theme.spacing(0.5),
right: (theme) => theme.spacing(0.5),
}}
>
<IconButton onClick={onOpen} aria-describedby={id} size='small'>
<HelpOutline
sx={{
fontSize: (theme) => theme.typography.body1.fontSize,
}}
/>
</IconButton>
<Popper
id={id}
open={open}
anchorEl={anchor}
sx={(theme) => ({ zIndex: theme.zIndex.tooltip })}
>
<ClickAwayListener onClickAway={onClose}>
<StyledPaper elevation={3}>
<IconButton
onClick={onClose}
sx={{ position: 'absolute', right: 4, top: 4 }}
>
<Close
sx={{
fontSize: (theme) =>
theme.typography.body1.fontSize,
}}
/>
</IconButton>
{children}
<Feedback
id={id}
eventName='project_overview'
localStorageKey='ProjectOverviewFeedback'
/>
</StyledPaper>
</ClickAwayListener>
</Popper>
</Box>
);
};

View File

@ -0,0 +1,136 @@
import { Box, styled, Typography } from '@mui/material';
import type { ProjectStatsSchema } from 'openapi/models';
import { HelpPopper } from './HelpPopper';
import { StatusBox } from './StatusBox';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'grid',
gap: theme.spacing(2),
gridTemplateColumns: 'repeat(5, 1fr)',
flexWrap: 'wrap',
[theme.breakpoints.down('lg')]: {
gridTemplateColumns: 'repeat(2, 1fr)',
},
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
},
}));
const StyledWidget = styled(Box)(({ theme }) => ({
position: 'relative',
padding: theme.spacing(3),
backgroundColor: theme.palette.background.paper,
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
borderRadius: `${theme.shape.borderRadiusLarge}px`,
[theme.breakpoints.down('lg')]: {
padding: theme.spacing(2),
},
}));
const StyledTimeToProductionDescription = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.typography.body2.fontSize,
lineHeight: theme.typography.body2.lineHeight,
}));
interface IProjectStatsProps {
stats: ProjectStatsSchema;
}
export const ProjectInsightsStats = ({ stats }: IProjectStatsProps) => {
if (Object.keys(stats).length === 0) {
return null;
}
const {
avgTimeToProdCurrentWindow,
projectActivityCurrentWindow,
projectActivityPastWindow,
createdCurrentWindow,
createdPastWindow,
archivedCurrentWindow,
archivedPastWindow,
} = stats;
return (
<StyledBox>
<StyledWidget>
<StatusBox
title='Total changes'
boxText={String(projectActivityCurrentWindow)}
change={
projectActivityCurrentWindow - projectActivityPastWindow
}
>
<HelpPopper id='total-changes'>
Sum of all configuration and state modifications in the
project.
</HelpPopper>
</StatusBox>
</StyledWidget>
<StyledWidget>
<StatusBox
title='Avg. time to production'
boxText={
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: (theme) => theme.spacing(1),
}}
>
{avgTimeToProdCurrentWindow}{' '}
<Typography component='span'>days</Typography>
</Box>
}
customChangeElement={
<StyledTimeToProductionDescription>
In project life
</StyledTimeToProductionDescription>
}
percentage
>
<HelpPopper id='avg-time-to-prod'>
How long did it take on average from a feature toggle
was created until it was enabled in an environment of
type production. This is calculated only from feature
toggles with the type of "release".
</HelpPopper>
</StatusBox>
</StyledWidget>
<StyledWidget>
<StatusBox
title='Features created'
boxText={String(createdCurrentWindow)}
change={createdCurrentWindow - createdPastWindow}
/>
</StyledWidget>
<StyledWidget>
<StatusBox
title='Stale toggles'
boxText={String(projectActivityCurrentWindow)}
change={
projectActivityCurrentWindow - projectActivityPastWindow
}
>
<HelpPopper id='stale-toggles'>
Sum of all stale toggles in the project
</HelpPopper>
</StatusBox>
</StyledWidget>
<StyledWidget>
<StatusBox
title='Features archived'
boxText={String(archivedCurrentWindow)}
change={archivedCurrentWindow - archivedPastWindow}
/>
</StyledWidget>
</StyledBox>
);
};

View File

@ -0,0 +1,129 @@
import type { FC, ReactNode } from 'react';
import CallMade from '@mui/icons-material/CallMade';
import SouthEast from '@mui/icons-material/SouthEast';
import { Box, Typography, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { flexRow } from 'themes/themeStyles';
const StyledTypographyHeader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(2.5),
}));
const StyledTypographyCount = styled(Box)(({ theme }) => ({
fontSize: theme.fontSizes.largeHeader,
}));
const StyledBoxChangeContainer = styled(Box)(({ theme }) => ({
...flexRow,
flexDirection: 'column',
alignItems: 'center',
marginLeft: theme.spacing(2.5),
}));
const StyledTypographySubtext = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.typography.body2.fontSize,
}));
const StyledTypographyChange = styled(Typography)(({ theme }) => ({
marginLeft: theme.spacing(1),
fontSize: theme.typography.body1.fontSize,
fontWeight: theme.typography.fontWeightBold,
}));
interface IStatusBoxProps {
title?: string;
boxText: ReactNode;
change?: number;
percentage?: boolean;
customChangeElement?: ReactNode;
}
const resolveIcon = (change: number) => {
if (change > 0) {
return (
<CallMade sx={{ color: 'success.dark', height: 20, width: 20 }} />
);
}
return <SouthEast sx={{ color: 'warning.dark', height: 20, width: 20 }} />;
};
const resolveColor = (change: number) => {
if (change > 0) {
return 'success.dark';
}
return 'warning.dark';
};
export const StatusBox: FC<IStatusBoxProps> = ({
title,
boxText,
change,
percentage,
children,
customChangeElement,
}) => (
<>
<ConditionallyRender
condition={Boolean(title)}
show={
<StyledTypographyHeader data-loading>
{title}
</StyledTypographyHeader>
}
/>
{children}
<Box
sx={{
...flexRow,
justifyContent: 'center',
width: 'auto',
}}
>
<StyledTypographyCount data-loading>
{boxText}
</StyledTypographyCount>
<ConditionallyRender
condition={Boolean(customChangeElement)}
show={
<StyledBoxChangeContainer data-loading>
{customChangeElement}
</StyledBoxChangeContainer>
}
elseShow={
<ConditionallyRender
condition={change !== undefined && change !== 0}
show={
<StyledBoxChangeContainer data-loading>
<Box
sx={{
...flexRow,
}}
>
{resolveIcon(change as number)}
<StyledTypographyChange
color={resolveColor(change as number)}
>
{(change as number) > 0 ? '+' : ''}
{change}
{percentage ? '%' : ''}
</StyledTypographyChange>
</Box>
<StyledTypographySubtext>
this month
</StyledTypographySubtext>
</StyledBoxChangeContainer>
}
elseShow={
<StyledBoxChangeContainer>
<StyledTypographySubtext data-loading>
No change
</StyledTypographySubtext>
</StyledBoxChangeContainer>
}
/>
}
/>
</Box>
</>
);

View File

@ -1,3 +1,4 @@
// biome-ignore lint: we need this to correctly extend the MUI theme
import { FormHelperTextOwnProps } from '@mui/material/FormHelperText';
declare module '@mui/material/styles' {