mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-15 17:50:48 +02: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:
parent
aeb6291863
commit
bb847e2935
@ -3,6 +3,7 @@ import { ChangeRequests } from './ChangeRequests/ChangeRequests';
|
|||||||
import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges';
|
import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges';
|
||||||
import { ProjectHealth } from './ProjectHealth/ProjectHealth';
|
import { ProjectHealth } from './ProjectHealth/ProjectHealth';
|
||||||
import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed';
|
import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed';
|
||||||
|
import { ProjectInsightsStats } from './ProjectInsightsStats/ProjectInsightsStats';
|
||||||
|
|
||||||
const Container = styled(Box)(({ theme }) => ({
|
const Container = styled(Box)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
@ -16,7 +17,7 @@ const Grid = styled(Box)(({ theme }) => ({
|
|||||||
gridTemplateColumns: 'repeat(10, 1fr)',
|
gridTemplateColumns: 'repeat(10, 1fr)',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FullWidthContainer = styled(Container)(() => ({
|
const FullWidthContainer = styled(Box)(() => ({
|
||||||
gridColumn: '1 / -1',
|
gridColumn: '1 / -1',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -32,12 +33,24 @@ const NarrowContainer = styled(Container)(() => ({
|
|||||||
gridColumn: 'span 2',
|
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 = () => {
|
export const ProjectInsights = () => {
|
||||||
return (
|
return (
|
||||||
<Grid>
|
<Grid>
|
||||||
<FullWidthContainer>
|
<FullWidthContainer>
|
||||||
Total changes / avg time to production / feature flags /stale
|
<ProjectInsightsStats {...statsData} />
|
||||||
flags
|
|
||||||
</FullWidthContainer>
|
</FullWidthContainer>
|
||||||
<MediumWideContainer>
|
<MediumWideContainer>
|
||||||
<ProjectHealth />
|
<ProjectHealth />
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
@ -1,3 +1,4 @@
|
|||||||
|
// biome-ignore lint: we need this to correctly extend the MUI theme
|
||||||
import { FormHelperTextOwnProps } from '@mui/material/FormHelperText';
|
import { FormHelperTextOwnProps } from '@mui/material/FormHelperText';
|
||||||
|
|
||||||
declare module '@mui/material/styles' {
|
declare module '@mui/material/styles' {
|
||||||
|
Loading…
Reference in New Issue
Block a user