1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

Project overview UI (#3034)

This commit is contained in:
Tymoteusz Czech 2023-02-03 12:58:21 +01:00 committed by GitHub
parent 0a6f9f647b
commit b98dd4d76c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 560 additions and 367 deletions

View File

@ -187,9 +187,11 @@ export const Project = () => {
</StyledDiv>
</StyledTopRow>
<ConditionallyRender
condition={!uiConfig?.flags?.newProjectOverview}
condition={
!Boolean(uiConfig?.flags?.newProjectOverview)
}
// TODO: !!! Remove entire block when removing feature flag!
show={
show={() => (
<StyledColumn>
<StyledProjectTitle>
<div>
@ -200,7 +202,7 @@ export const Project = () => {
show={
<StyledDiv>
<StyledTitle data-loading>
Description:{' '}
Description:&nbsp;
</StyledTitle>
<StyledText data-loading>
{project.description}
@ -210,7 +212,7 @@ export const Project = () => {
/>
<StyledDiv>
<StyledTitle data-loading>
projectId:{' '}
projectId:&nbsp;
</StyledTitle>
<StyledText data-loading>
{projectId}
@ -219,7 +221,7 @@ export const Project = () => {
</div>
</StyledProjectTitle>
</StyledColumn>
}
)}
/>
</StyledInnerContainer>

View File

@ -1,7 +1,6 @@
import { FC } from 'react';
import useLoading from 'hooks/useLoading';
import { Link as RouterLink } from 'react-router-dom';
import { Box, styled, Typography, Link } from '@mui/material';
import { Box, styled, Typography } from '@mui/material';
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
import {
@ -10,17 +9,25 @@ import {
StyledWidgetTitle,
} from './ProjectInfo.styles';
import { useProjectChangeRequests } from 'hooks/api/getters/useProjectChangeRequests/useProjectChangeRequests';
import { WidgetFooterLink } from './WidgetFooterLink';
interface IChangeRequestsWidgetProps {
projectId: string;
}
const StyledContentBox = styled(Box)(({ theme }) => ({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(1.5),
}));
const StyledChangeBox = styled(Box)(({ theme }) => ({
flex: 1,
textAlign: 'left',
padding: theme.spacing(1.5),
marginBottom: theme.spacing(1.5),
borderRadius: theme.shape.borderRadiusMedium,
alignItems: 'center',
minWidth: 175,
}));
const StyledChangeRequestStatusInfo = styled(Box)(({ theme }) => ({
@ -42,8 +49,18 @@ const StyledInReviewCount = styled(StyledCount)(({ theme }) => ({
borderRadius: theme.shape.borderRadius,
}));
const StyledSubtitle = styled(Typography)(({ theme }) => ({
fontSize: theme.typography.body2.fontSize,
marginBottom: theme.spacing(0.5),
}));
const ChangeRequestsLabel = () => (
<Typography component="span" variant="body2" color="text.secondary">
<Typography
component="span"
variant="body2"
color="text.secondary"
lineHeight={1}
>
change requests
</Typography>
);
@ -63,33 +80,31 @@ export const ChangeRequestsWidget: FC<IChangeRequestsWidgetProps> = ({
return (
<StyledProjectInfoWidgetContainer ref={loadingRef}>
<StyledWidgetTitle>Open change requests</StyledWidgetTitle>
<StyledChangeBox
sx={{ background: theme => theme.palette.success.light }}
>
<Typography variant="body2">To be applied</Typography>
<StyledChangeRequestStatusInfo>
<StyledApprovedCount>{toBeApplied}</StyledApprovedCount>{' '}
<ChangeRequestsLabel />
</StyledChangeRequestStatusInfo>
</StyledChangeBox>
<StyledChangeBox
sx={{ background: theme => theme.palette.secondary.light }}
>
<Typography variant="body2">To be reviewed</Typography>
<StyledChangeRequestStatusInfo>
<StyledInReviewCount>{toBeReviewed}</StyledInReviewCount>{' '}
<ChangeRequestsLabel />
</StyledChangeRequestStatusInfo>
</StyledChangeBox>
<Typography variant="body2" textAlign="center">
<Link
component={RouterLink}
to={`/projects/${projectId}/change-requests`}
<StyledContentBox>
<StyledChangeBox
sx={{ background: theme => theme.palette.success.light }}
>
View change requests
</Link>
</Typography>
<StyledSubtitle>To be applied</StyledSubtitle>
<StyledChangeRequestStatusInfo>
<StyledApprovedCount>{toBeApplied}</StyledApprovedCount>{' '}
<ChangeRequestsLabel />
</StyledChangeRequestStatusInfo>
</StyledChangeBox>
<StyledChangeBox
sx={{ background: theme => theme.palette.secondary.light }}
>
<StyledSubtitle>To be reviewed</StyledSubtitle>
<StyledChangeRequestStatusInfo>
<StyledInReviewCount>
{toBeReviewed}
</StyledInReviewCount>{' '}
<ChangeRequestsLabel />
</StyledChangeRequestStatusInfo>
</StyledChangeBox>
</StyledContentBox>
<WidgetFooterLink to={`/projects/${projectId}/change-requests`}>
View change requests
</WidgetFooterLink>
</StyledProjectInfoWidgetContainer>
);
};

View File

@ -1,16 +1,10 @@
import {
StyledArrowIcon,
StyledCount,
StyledProjectInfoWidgetContainer,
StyledDivPercentageContainer,
StyledLink,
StyledParagraphEmphasizedText,
StyledWidgetTitle,
StyledSpanLinkText,
} from './ProjectInfo.styles';
import { Box, styled } from '@mui/material';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Box, styled, Typography } from '@mui/material';
import {
StyledProjectInfoWidgetContainer,
StyledWidgetTitle,
} from './ProjectInfo.styles';
import { WidgetFooterLink } from './WidgetFooterLink';
interface IHealthWidgetProps {
projectId: string;
@ -19,69 +13,43 @@ interface IHealthWidgetProps {
stale?: number;
}
const StyledWarning = styled('span')<{ active?: boolean }>(
({ theme, active }) => ({
color: active ? theme.palette.warning.dark : 'inherit',
})
);
const StyledParagraphEmphasizedText = styled('p')(({ theme }) => ({
fontSize: '1.5rem',
[theme.breakpoints.down('md')]: {
fontSize: theme.fontSizes.bodySize,
marginBottom: theme.spacing(4),
},
}));
export const HealthWidget = ({
projectId,
health,
total,
stale,
}: IHealthWidgetProps) => {
const { uiConfig } = useUiConfig();
const StyledPercentageText = styled('p')(({ theme }) => ({
fontSize: '1.5rem',
[theme.breakpoints.down('md')]: {
fontSize: theme.fontSizes.bodySize,
},
}));
if (uiConfig?.flags?.newProjectOverview) {
return (
<StyledProjectInfoWidgetContainer>
<StyledWidgetTitle data-loading>
Project health
</StyledWidgetTitle>
<StyledDivPercentageContainer>
export const HealthWidget = ({ projectId, health }: IHealthWidgetProps) => {
return (
<StyledProjectInfoWidgetContainer>
<StyledWidgetTitle data-loading>Project health</StyledWidgetTitle>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: theme => theme.spacing(2),
}}
>
<StyledPercentageText>
<PercentageCircle percentage={health} />
</StyledDivPercentageContainer>
</StyledPercentageText>
<StyledParagraphEmphasizedText data-loading>
{health}%
</StyledParagraphEmphasizedText>
<Typography data-loading>
<StyledCount>{total}</StyledCount> toggles in total
</Typography>
<Typography data-loading sx={{ marginBottom: 2 }}>
<StyledCount>
<StyledWarning active={Boolean(stale)}>
{stale}
</StyledWarning>
</StyledCount>{' '}
<StyledWarning active={Boolean(stale)}>
potentially stale
</StyledWarning>
</Typography>
<StyledLink data-loading to={`/projects/${projectId}/health`}>
<StyledSpanLinkText data-loading>
View project health
</StyledSpanLinkText>
</StyledLink>
</StyledProjectInfoWidgetContainer>
);
}
return (
<StyledProjectInfoWidgetContainer>
<StyledDivPercentageContainer>
<PercentageCircle percentage={health} />
</StyledDivPercentageContainer>
<StyledWidgetTitle data-loading>
Overall health rating
</StyledWidgetTitle>
<StyledParagraphEmphasizedText data-loading>
{health}%
</StyledParagraphEmphasizedText>
<StyledLink data-loading to={`/projects/${projectId}/health`}>
<StyledSpanLinkText data-loading>view more </StyledSpanLinkText>
<StyledArrowIcon data-loading />
</StyledLink>
</Box>
<WidgetFooterLink to={`/projects/${projectId}/health`}>
View project health
</WidgetFooterLink>
</StyledProjectInfoWidgetContainer>
);
};

View File

@ -0,0 +1,75 @@
import { Box, styled, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { flexRow } from 'themes/themeStyles';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { StyledProjectInfoWidgetContainer } from './ProjectInfo.styles';
interface ILegacyHealthWidgetProps {
projectId: string;
health: number;
total?: number;
stale?: number;
}
const StyledParagraphEmphasizedText = styled('p')(({ theme }) => ({
fontSize: '1.5rem',
[theme.breakpoints.down('md')]: {
fontSize: theme.fontSizes.bodySize,
marginBottom: theme.spacing(4),
},
}));
const StyledDivPercentageContainer = styled('div')(() => ({
display: 'flex',
justifyContent: 'center',
}));
const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',
...flexRow,
justifyContent: 'center',
color: theme.palette.primary.main,
[theme.breakpoints.down('md')]: {
position: 'absolute',
bottom: theme.spacing(1.5),
right: theme.spacing(1.5),
},
}));
const StyledSpanLinkText = styled('p')(({ theme }) => ({
[theme.breakpoints.down('md')]: {
display: 'none',
},
}));
const StyledArrowIcon = styled(ArrowForwardIcon)(({ theme }) => ({
color: theme.palette.primary.main,
marginLeft: theme.spacing(1),
}));
/**
* @deprecated
*/
export const LegacyHealthWidget = ({
projectId,
health,
}: ILegacyHealthWidgetProps) => (
<StyledProjectInfoWidgetContainer>
<StyledDivPercentageContainer>
<PercentageCircle percentage={health} />
</StyledDivPercentageContainer>
<Typography data-loading sx={{ marginTop: theme => theme.spacing(2) }}>
Overall health rating
</Typography>
<Box sx={{ marginBottom: theme => theme.spacing(2.5) }}>
<StyledParagraphEmphasizedText data-loading>
{health}%
</StyledParagraphEmphasizedText>
</Box>
<StyledLink data-loading to={`/projects/${projectId}/health`}>
<StyledSpanLinkText data-loading>view more </StyledSpanLinkText>
<StyledArrowIcon data-loading />
</StyledLink>
</StyledProjectInfoWidgetContainer>
);

View File

@ -0,0 +1,93 @@
import { Link } from 'react-router-dom';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { flexRow } from 'themes/themeStyles';
import { styled } from '@mui/material';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const StyledDivInfoContainer = styled('div')(({ theme }) => ({
textAlign: 'center',
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusLarge,
width: '100%',
padding: theme.spacing(3, 2, 3, 2),
[theme.breakpoints.down('md')]: {
...flexRow,
flexDirection: 'column',
justifyContent: 'center',
fontSize: theme.fontSizes.smallBody,
position: 'relative',
padding: theme.spacing(1.5),
},
}));
const StyledParagraphSubtitle = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(2),
}));
const StyledParagraphEmphasizedText = styled('p')(({ theme }) => ({
fontSize: '1.5rem',
marginBottom: theme.spacing(2),
[theme.breakpoints.down('md')]: {
fontSize: theme.fontSizes.bodySize,
marginBottom: theme.spacing(4),
},
}));
const StyledSpanLinkText = styled('p')(({ theme }) => ({
[theme.breakpoints.down('md')]: {
display: 'none',
},
}));
const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',
...flexRow,
justifyContent: 'center',
color: theme.palette.primary.main,
[theme.breakpoints.down('md')]: {
position: 'absolute',
right: theme.spacing(1.5),
bottom: theme.spacing(1.5),
},
}));
const StyledArrowIcon = styled(ArrowForwardIcon)(({ theme }) => ({
color: theme.palette.primary.main,
marginLeft: theme.spacing(1),
}));
interface ILegacyProjectMembersWidgetProps {
projectId: string;
memberCount: number;
}
/**
* @deprecated
*/
export const LegacyProjectMembersWidget = ({
projectId,
memberCount,
}: ILegacyProjectMembersWidgetProps) => {
const { uiConfig } = useUiConfig();
let link = `/admin/users`;
if (uiConfig?.versionInfo?.current?.enterprise) {
link = `/projects/${projectId}/settings/access`;
}
return (
<StyledDivInfoContainer>
<StyledParagraphSubtitle data-loading>
Project members
</StyledParagraphSubtitle>
<StyledParagraphEmphasizedText data-loading>
{memberCount}
</StyledParagraphEmphasizedText>
<StyledLink data-loading to={link}>
<StyledSpanLinkText data-loading>view more </StyledSpanLinkText>
<StyledArrowIcon data-loading />
</StyledLink>
</StyledDivInfoContainer>
);
};

View File

@ -1,12 +1,12 @@
import { FC } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Box, styled, Typography, Link } from '@mui/material';
import { styled, Typography } from '@mui/material';
import {
StyledProjectInfoWidgetContainer,
StyledWidgetTitle,
} from './ProjectInfo.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { WidgetFooterLink } from './WidgetFooterLink';
interface IMetaWidgetProps {
id?: string;
@ -35,35 +35,29 @@ export const MetaWidget: FC<IMetaWidgetProps> = ({ id, description }) => {
</Typography>{' '}
<code data-loading>{id || '__________'}</code>
</StyledIDContainer>
<Typography mt={1.5} textAlign="left">
<ConditionallyRender
condition={Boolean(description)}
show={
<>
<Typography
component="span"
variant="body2"
color="text.secondary"
>
Description:{' '}
</Typography>
<Typography component="span" variant="body2">
{description}
</Typography>
</>
}
elseShow={
<Typography variant="body2" textAlign="center">
<Link
component={RouterLink}
to={`/projects/${id}/edit`}
>
Add description
</Link>
</Typography>
}
/>
</Typography>
<ConditionallyRender
condition={Boolean(description)}
show={
<Typography
variant="body2"
sx={{
marginTop: theme => theme.spacing(1.5),
marginBottom: 0,
textAlign: 'left',
}}
>
{description}
</Typography>
}
/>
<ConditionallyRender
condition={!Boolean(description)}
show={
<WidgetFooterLink to={`/projects/${id}/edit`}>
Add description
</WidgetFooterLink>
}
/>
</StyledProjectInfoWidgetContainer>
);
};

View File

@ -1,74 +1,23 @@
import { Link } from 'react-router-dom';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { flexRow } from 'themes/themeStyles';
import { styled } from '@mui/material';
export const StyledProjectInfoSidebarContainer = styled('div')(({ theme }) => ({
...flexRow,
width: '225px',
flexDirection: 'column',
gap: theme.spacing(2),
boxShadow: 'none',
[theme.breakpoints.down('md')]: {
flexDirection: 'row',
alignItems: 'stretch',
width: '100%',
marginBottom: theme.spacing(2),
},
}));
export const StyledDivPercentageContainer = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
margin: theme.spacing(2, 0),
}));
export const StyledProjectInfoWidgetContainer = styled('div')(({ theme }) => ({
margin: '0',
textAlign: 'center',
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusLarge,
width: '100%',
padding: theme.spacing(3, 2, 3, 2),
minWidth: 225,
padding: theme.spacing(3),
[theme.breakpoints.down('md')]: {
margin: theme.spacing(0, 1),
...flexRow,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
fontSize: theme.fontSizes.smallBody,
position: 'relative',
padding: theme.spacing(1.5),
'&:first-of-type': {
marginLeft: '0',
},
'&:last-of-type': {
marginRight: '0',
},
},
}));
export const StyledWidgetTitle = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(2),
}));
export const StyledParagraphGridRow = styled('div')(({ theme }) => ({
display: 'grid',
gridGap: theme.spacing(1.5),
gridTemplateColumns: `${theme.spacing(2.25)} auto auto`, //20px auto auto
margin: theme.spacing(1, 0, 1, 0),
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
textAlign: 'left',
alignItems: 'center',
}));
export const StyledParagraphEmphasizedText = styled('p')(({ theme }) => ({
fontSize: '1.5rem',
marginBottom: theme.spacing(2),
[theme.breakpoints.down('md')]: {
fontSize: theme.fontSizes.bodySize,
marginBottom: theme.spacing(4),
},
marginBottom: theme.spacing(2.5),
}));
export const StyledCount = styled('span')(({ theme }) => ({
@ -76,25 +25,3 @@ export const StyledCount = styled('span')(({ theme }) => ({
fontWeight: 'bold',
color: theme.palette.text.primary,
}));
export const StyledSpanLinkText = styled('p')(({ theme }) => ({
[theme.breakpoints.down('md')]: {
display: 'none',
},
}));
export const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',
...flexRow,
justifyContent: 'center',
color: theme.palette.primary.main,
[theme.breakpoints.down('md')]: {
position: 'absolute',
bottom: theme.spacing(1.5),
},
}));
export const StyledArrowIcon = styled(ArrowForwardIcon)(({ theme }) => ({
color: theme.palette.primary.main,
marginLeft: theme.spacing(1),
}));

View File

@ -1,14 +1,17 @@
import { Box, styled, useMediaQuery, useTheme } from '@mui/material';
import { ProjectStatsSchema } from 'openapi/models/projectStatsSchema';
import type { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import { StyledProjectInfoSidebarContainer } from './ProjectInfo.styles';
import { HealthWidget } from './HealthWidget';
import { ToggleTypesWidget } from './ToggleTypesWidget';
import { MetaWidget } from './MetaWidget';
import { ProjectMembersWidget } from './ProjectMembersWidget';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ProjectStatsSchema } from 'openapi/models/projectStatsSchema';
import { ChangeRequestsWidget } from './ChangeRequestsWidget';
import { flexRow } from 'themes/themeStyles';
import { LegacyHealthWidget } from './LegacyHealthWidget';
import { LegacyProjectMembersWidget } from './LegacyProjectMembersWidget';
interface IProjectInfoProps {
id: string;
@ -19,6 +22,23 @@ interface IProjectInfoProps {
stats: ProjectStatsSchema;
}
const StyledProjectInfoSidebarContainer = styled(Box)(({ theme }) => ({
...flexRow,
width: '225px',
flexDirection: 'column',
gap: theme.spacing(2),
boxShadow: 'none',
[theme.breakpoints.down('md')]: {
display: 'grid',
width: '100%',
alignItems: 'stretch',
marginBottom: theme.spacing(2),
},
[theme.breakpoints.down('sm')]: {
display: 'flex',
},
}));
const ProjectInfo = ({
id,
description,
@ -28,29 +48,74 @@ const ProjectInfo = ({
stats,
}: IProjectInfoProps) => {
const { uiConfig, isEnterprise } = useUiConfig();
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const showChangeRequestsWidget = isEnterprise();
const showProjectMembersWidget = id !== DEFAULT_PROJECT_ID;
const fitMoreColumns =
(!showChangeRequestsWidget && !showProjectMembersWidget) ||
(isSmallScreen && showChangeRequestsWidget && showProjectMembersWidget);
if (!Boolean(uiConfig?.flags.newProjectOverview)) {
return (
<aside>
<Box
sx={{
display: 'grid',
gridTemplateColumns:
'repeat(auto-fit, minmax(225px, 1fr))',
gap: theme => theme.spacing(2),
paddingBottom: theme => theme.spacing(2),
}}
>
<LegacyHealthWidget projectId={id} health={health} />
<ConditionallyRender
condition={showProjectMembersWidget}
show={
<LegacyProjectMembersWidget
projectId={id}
memberCount={memberCount}
/>
}
/>
</Box>
</aside>
);
}
return (
<aside>
<StyledProjectInfoSidebarContainer>
<StyledProjectInfoSidebarContainer
sx={
fitMoreColumns
? {
gridTemplateColumns:
'repeat(auto-fill, minmax(225px, 1fr))',
}
: { gridTemplateColumns: 'repeat(2, 1fr)' }
}
>
<ConditionallyRender
condition={
isEnterprise() &&
Boolean(uiConfig?.flags.newProjectOverview)
condition={showChangeRequestsWidget}
show={
<Box
sx={{
gridColumnStart: showProjectMembersWidget
? 'span 2'
: 'span 1',
flex: 1,
display: 'flex',
}}
>
<ChangeRequestsWidget projectId={id} />
</Box>
}
show={<ChangeRequestsWidget projectId={id} />}
/>
<MetaWidget id={id} description={description} />
<HealthWidget projectId={id} health={health} />
<ConditionallyRender
condition={Boolean(uiConfig?.flags.newProjectOverview)}
show={<MetaWidget id={id} description={description} />}
/>
<HealthWidget
projectId={id}
health={health}
total={features.length}
stale={features.filter(feature => feature.stale).length}
/>
<ConditionallyRender
condition={id !== DEFAULT_PROJECT_ID}
condition={showProjectMembersWidget}
show={
<ProjectMembersWidget
projectId={id}
@ -59,10 +124,7 @@ const ProjectInfo = ({
/>
}
/>
<ConditionallyRender
condition={Boolean(uiConfig?.flags.newProjectOverview)}
show={<ToggleTypesWidget features={features} />}
/>
<ToggleTypesWidget features={features} />
</StyledProjectInfoSidebarContainer>
</aside>
);

View File

@ -1,10 +1,11 @@
import {
StyledLink,
StyledProjectInfoWidgetContainer,
StyledSpanLinkText,
StyledWidgetTitle,
} from './ProjectInfo.styles';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { StatusBox } from '../ProjectStats/StatusBox';
import { WidgetFooterLink } from './WidgetFooterLink';
import { Box } from '@mui/material';
interface IProjectMembersWidgetProps {
projectId: string;
@ -26,20 +27,17 @@ export const ProjectMembersWidget = ({
}
return (
<StyledProjectInfoWidgetContainer
sx={{ padding: theme => theme.spacing(0, 0, 3, 0) }}
>
<StatusBox
title={'Project members'}
boxText={`${memberCount}`}
change={change}
fullWidthBodyText
/>
<StyledLink data-loading to={link}>
<StyledSpanLinkText data-loading>
View all members
</StyledSpanLinkText>
</StyledLink>
<StyledProjectInfoWidgetContainer>
<StyledWidgetTitle data-loading>Project members</StyledWidgetTitle>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
}}
>
<StatusBox boxText={`${memberCount}`} change={change} />
</Box>
<WidgetFooterLink to={link}>View all members</WidgetFooterLink>
</StyledProjectInfoWidgetContainer>
);
};

View File

@ -4,7 +4,6 @@ import type { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import {
StyledCount,
StyledParagraphGridRow,
StyledProjectInfoWidgetContainer,
StyledWidgetTitle,
} from './ProjectInfo.styles';
@ -14,8 +13,10 @@ export interface IToggleTypesWidgetProps {
features: IFeatureToggleListItem[];
}
const StyledTypeCount = styled(StyledCount)(() => ({
const StyledTypeCount = styled(StyledCount)(({ theme }) => ({
marginLeft: 'auto',
fontWeight: theme.typography.fontWeightRegular,
color: theme.palette.text.secondary,
}));
interface IToggleTypeRowProps {
@ -23,10 +24,26 @@ interface IToggleTypeRowProps {
Icon: OverridableComponent<SvgIconTypeMap>;
count: number;
}
const StyledParagraphGridRow = styled('div')(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1.5),
width: '100%',
gridTemplateColumns: `${theme.spacing(2.5)} auto auto`, //20px auto auto
margin: theme.spacing(1, 0),
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
alignItems: 'center',
[theme.breakpoints.down('md')]: {
margin: 0,
},
}));
const ToggleTypesRow = ({ type, Icon, count }: IToggleTypeRowProps) => {
const getTitleText = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1).replace('-', ' ');
};
return (
<StyledParagraphGridRow data-loading>
<Icon fontSize="small" data-loading />

View File

@ -0,0 +1,28 @@
import { Link as RouterLink } from 'react-router-dom';
import { Link, Typography } from '@mui/material';
import { FC } from 'react';
interface IWidgetFooterLinkProps {
to: string;
}
export const WidgetFooterLink: FC<IWidgetFooterLinkProps> = ({
children,
to,
}) => {
return (
<Typography
variant="body2"
textAlign="center"
sx={{
paddingTop: theme => theme.spacing(2.5),
marginTop: 'auto',
justifySelf: 'flex-end',
}}
>
<Link component={RouterLink} to={to}>
{children}
</Link>
</Typography>
);
};

View File

@ -1,21 +1,39 @@
import { Box, styled } from '@mui/material';
import { Box, styled, Typography } from '@mui/material';
import { ProjectStatsSchema } from 'openapi/models';
import { object } from 'prop-types';
import { StatusBox } from './StatusBox';
const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(0, 0, 2, 2),
display: 'flex',
justifyContent: 'space-between',
display: 'grid',
gap: theme.spacing(2),
gridTemplateColumns: 'repeat(4, 1fr)',
flexWrap: 'wrap',
[theme.breakpoints.down('lg')]: {
gridTemplateColumns: 'repeat(2, 1fr)',
},
[theme.breakpoints.down('md')]: {
paddingLeft: 0,
padding: theme.spacing(0, 0, 2),
},
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
},
}));
const StyledWidget = styled(Box)(({ theme }) => ({
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),
},
}));
interface IProjectStatsProps {
stats: ProjectStatsSchema;
}
@ -47,32 +65,53 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
return (
<StyledBox>
<StatusBox
title="Total changes"
boxText={String(projectActivityCurrentWindow)}
change={
projectActivityCurrentWindow - projectActivityPastWindow
}
/>
<StatusBox
title="Avg. time to production"
boxText={`${avgTimeToProdCurrentWindow} days`}
change={calculatePercentage(
avgTimeToProdCurrentWindow,
avgTimeToProdPastWindow
)}
percentage
/>{' '}
<StatusBox
title="Features created"
boxText={String(createdCurrentWindow)}
change={createdCurrentWindow - createdPastWindow}
/>
<StatusBox
title="Features archived"
boxText={String(archivedCurrentWindow)}
change={archivedCurrentWindow - archivedPastWindow}
/>
<StyledWidget>
<StatusBox
title="Total changes"
boxText={String(projectActivityCurrentWindow)}
change={
projectActivityCurrentWindow -
projectActivityPastWindow -
20
}
/>
</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>
}
change={calculatePercentage(
avgTimeToProdCurrentWindow,
avgTimeToProdPastWindow
)}
percentage
/>
</StyledWidget>
<StyledWidget>
<StatusBox
title="Features created"
boxText={String(createdCurrentWindow)}
change={createdCurrentWindow - createdPastWindow}
/>
</StyledWidget>
<StyledWidget>
<StatusBox
title="Features archived"
boxText={String(archivedCurrentWindow)}
change={archivedCurrentWindow - archivedPastWindow}
/>
</StyledWidget>
</StyledBox>
);
};

View File

@ -1,80 +1,56 @@
import { ArrowOutward, SouthEast } from '@mui/icons-material';
import type { ReactNode } from 'react';
import { CallMade, SouthEast } from '@mui/icons-material';
import { Box, Typography, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { flexRow } from 'themes/themeStyles';
const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(4, 2),
backgroundColor: theme.palette.background.paper,
minWidth: '24%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
borderRadius: `${theme.shape.borderRadiusLarge}px`,
[theme.breakpoints.down('lg')]: {
minWidth: '49%',
padding: theme.spacing(2),
':nth-child(n+3)': {
marginTop: theme.spacing(2),
},
},
[theme.breakpoints.down('sm')]: {
':nth-child(n+2)': {
marginTop: theme.spacing(2),
},
},
}));
const StyledTypographyHeader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(2),
marginBottom: theme.spacing(2.5),
}));
const StyledTypographyCount = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.largeHeader,
fontWeight: 'bold',
}));
const StyledBoxChangeContainer = styled(Box)(({ theme }) => ({
...flexRow,
flexDirection: 'column',
alignItems: 'center',
marginLeft: theme.spacing(1.5),
marginLeft: theme.spacing(2.5),
}));
const StyledTypographySubtext = styled(Typography)(({ theme }) => ({
color: theme.palette.neutral.main,
fontSize: theme.fontSizes.smallBody,
fontSize: theme.typography.body2.fontSize,
}));
const StyledTypographyChange = styled(Typography)(({ theme }) => ({
marginLeft: theme.spacing(1),
fontSize: theme.fontSizes.smallBody,
fontSize: theme.typography.body1.fontSize,
fontWeight: theme.typography.fontWeightBold,
}));
interface IStatusBoxProps {
title: string;
boxText: string;
title?: string;
boxText: ReactNode;
change: number;
percentage?: boolean;
fullWidthBodyText?: boolean;
}
const resolveIcon = (change: number) => {
if (change > 0) {
return (
<ArrowOutward
sx={{ color: 'success.main', height: 18, width: 18 }}
/>
<CallMade sx={{ color: 'success.dark', height: 20, width: 20 }} />
);
}
return <SouthEast sx={{ color: 'warning.dark', height: 18, width: 18 }} />;
return <SouthEast sx={{ color: 'warning.dark', height: 20, width: 20 }} />;
};
const resolveColor = (change: number) => {
if (change > 0) {
return 'success.main';
return 'success.dark';
}
return 'error.main';
return 'warning.dark';
};
export const StatusBox = ({
@ -82,52 +58,51 @@ export const StatusBox = ({
boxText,
change,
percentage,
fullWidthBodyText,
}: IStatusBoxProps) => {
return (
<StyledBox>
<StyledTypographyHeader>{title}</StyledTypographyHeader>
<Box
sx={{
...flexRow,
justifyContent: fullWidthBodyText
? 'space-between'
: 'center',
width: fullWidthBodyText ? '65%' : 'auto',
}}
>
<StyledTypographyCount>{boxText}</StyledTypographyCount>
<ConditionallyRender
condition={change !== 0}
show={
<StyledBoxChangeContainer>
<Box
sx={{
...flexRow,
}}
}: IStatusBoxProps) => (
<>
<ConditionallyRender
condition={Boolean(title)}
show={<StyledTypographyHeader>{title}</StyledTypographyHeader>}
/>
<Box
sx={{
...flexRow,
justifyContent: 'center',
width: 'auto',
}}
>
<StyledTypographyCount>{boxText}</StyledTypographyCount>
<ConditionallyRender
condition={change !== 0}
show={
<StyledBoxChangeContainer>
<Box
sx={{
...flexRow,
}}
>
{resolveIcon(change)}
<StyledTypographyChange
color={resolveColor(change)}
>
{resolveIcon(change)}
<StyledTypographyChange
color={resolveColor(change)}
>
{change}
{percentage ? '%' : ''}
</StyledTypographyChange>
</Box>
<StyledTypographySubtext>
this month
</StyledTypographySubtext>
</StyledBoxChangeContainer>
}
elseShow={
<StyledBoxChangeContainer>
<StyledTypographySubtext>
No change
</StyledTypographySubtext>
</StyledBoxChangeContainer>
}
/>
</Box>
</StyledBox>
);
};
{change > 0 ? '+' : ''}
{change}
{percentage ? '%' : ''}
</StyledTypographyChange>
</Box>
<StyledTypographySubtext>
this month
</StyledTypographySubtext>
</StyledBoxChangeContainer>
}
elseShow={
<StyledBoxChangeContainer>
<StyledTypographySubtext>
No change
</StyledTypographySubtext>
</StyledBoxChangeContainer>
}
/>
</Box>
</>
);