mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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:
		
							parent
							
								
									aeb6291863
								
							
						
					
					
						commit
						bb847e2935
					
				@ -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 />
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
declare module '@mui/material/styles' {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user