mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02:00
Project overview UI (#3034)
This commit is contained in:
parent
0a6f9f647b
commit
b98dd4d76c
@ -187,9 +187,11 @@ export const Project = () => {
|
|||||||
</StyledDiv>
|
</StyledDiv>
|
||||||
</StyledTopRow>
|
</StyledTopRow>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!uiConfig?.flags?.newProjectOverview}
|
condition={
|
||||||
|
!Boolean(uiConfig?.flags?.newProjectOverview)
|
||||||
|
}
|
||||||
// TODO: !!! Remove entire block when removing feature flag!
|
// TODO: !!! Remove entire block when removing feature flag!
|
||||||
show={
|
show={() => (
|
||||||
<StyledColumn>
|
<StyledColumn>
|
||||||
<StyledProjectTitle>
|
<StyledProjectTitle>
|
||||||
<div>
|
<div>
|
||||||
@ -200,7 +202,7 @@ export const Project = () => {
|
|||||||
show={
|
show={
|
||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
<StyledTitle data-loading>
|
<StyledTitle data-loading>
|
||||||
Description:{' '}
|
Description:
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
<StyledText data-loading>
|
<StyledText data-loading>
|
||||||
{project.description}
|
{project.description}
|
||||||
@ -210,7 +212,7 @@ export const Project = () => {
|
|||||||
/>
|
/>
|
||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
<StyledTitle data-loading>
|
<StyledTitle data-loading>
|
||||||
projectId:{' '}
|
projectId:
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
<StyledText data-loading>
|
<StyledText data-loading>
|
||||||
{projectId}
|
{projectId}
|
||||||
@ -219,7 +221,7 @@ export const Project = () => {
|
|||||||
</div>
|
</div>
|
||||||
</StyledProjectTitle>
|
</StyledProjectTitle>
|
||||||
</StyledColumn>
|
</StyledColumn>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
</StyledInnerContainer>
|
</StyledInnerContainer>
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
import { Box, styled, Typography, Link } from '@mui/material';
|
|
||||||
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
|
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,17 +9,25 @@ import {
|
|||||||
StyledWidgetTitle,
|
StyledWidgetTitle,
|
||||||
} from './ProjectInfo.styles';
|
} from './ProjectInfo.styles';
|
||||||
import { useProjectChangeRequests } from 'hooks/api/getters/useProjectChangeRequests/useProjectChangeRequests';
|
import { useProjectChangeRequests } from 'hooks/api/getters/useProjectChangeRequests/useProjectChangeRequests';
|
||||||
|
import { WidgetFooterLink } from './WidgetFooterLink';
|
||||||
|
|
||||||
interface IChangeRequestsWidgetProps {
|
interface IChangeRequestsWidgetProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledContentBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledChangeBox = styled(Box)(({ theme }) => ({
|
const StyledChangeBox = styled(Box)(({ theme }) => ({
|
||||||
|
flex: 1,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
padding: theme.spacing(1.5),
|
padding: theme.spacing(1.5),
|
||||||
marginBottom: theme.spacing(1.5),
|
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
minWidth: 175,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledChangeRequestStatusInfo = styled(Box)(({ theme }) => ({
|
const StyledChangeRequestStatusInfo = styled(Box)(({ theme }) => ({
|
||||||
@ -42,8 +49,18 @@ const StyledInReviewCount = styled(StyledCount)(({ theme }) => ({
|
|||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledSubtitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
marginBottom: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
const ChangeRequestsLabel = () => (
|
const ChangeRequestsLabel = () => (
|
||||||
<Typography component="span" variant="body2" color="text.secondary">
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
lineHeight={1}
|
||||||
|
>
|
||||||
change requests
|
change requests
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@ -63,33 +80,31 @@ export const ChangeRequestsWidget: FC<IChangeRequestsWidgetProps> = ({
|
|||||||
return (
|
return (
|
||||||
<StyledProjectInfoWidgetContainer ref={loadingRef}>
|
<StyledProjectInfoWidgetContainer ref={loadingRef}>
|
||||||
<StyledWidgetTitle>Open change requests</StyledWidgetTitle>
|
<StyledWidgetTitle>Open change requests</StyledWidgetTitle>
|
||||||
|
<StyledContentBox>
|
||||||
<StyledChangeBox
|
<StyledChangeBox
|
||||||
sx={{ background: theme => theme.palette.success.light }}
|
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`}
|
|
||||||
>
|
>
|
||||||
View change requests
|
<StyledSubtitle>To be applied</StyledSubtitle>
|
||||||
</Link>
|
<StyledChangeRequestStatusInfo>
|
||||||
</Typography>
|
<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>
|
</StyledProjectInfoWidgetContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
import {
|
import { Box, styled } from '@mui/material';
|
||||||
StyledArrowIcon,
|
|
||||||
StyledCount,
|
|
||||||
StyledProjectInfoWidgetContainer,
|
|
||||||
StyledDivPercentageContainer,
|
|
||||||
StyledLink,
|
|
||||||
StyledParagraphEmphasizedText,
|
|
||||||
StyledWidgetTitle,
|
|
||||||
StyledSpanLinkText,
|
|
||||||
} from './ProjectInfo.styles';
|
|
||||||
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import {
|
||||||
import { Box, styled, Typography } from '@mui/material';
|
StyledProjectInfoWidgetContainer,
|
||||||
|
StyledWidgetTitle,
|
||||||
|
} from './ProjectInfo.styles';
|
||||||
|
import { WidgetFooterLink } from './WidgetFooterLink';
|
||||||
|
|
||||||
interface IHealthWidgetProps {
|
interface IHealthWidgetProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -19,69 +13,43 @@ interface IHealthWidgetProps {
|
|||||||
stale?: number;
|
stale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledWarning = styled('span')<{ active?: boolean }>(
|
const StyledParagraphEmphasizedText = styled('p')(({ theme }) => ({
|
||||||
({ theme, active }) => ({
|
fontSize: '1.5rem',
|
||||||
color: active ? theme.palette.warning.dark : 'inherit',
|
[theme.breakpoints.down('md')]: {
|
||||||
})
|
fontSize: theme.fontSizes.bodySize,
|
||||||
);
|
marginBottom: theme.spacing(4),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export const HealthWidget = ({
|
const StyledPercentageText = styled('p')(({ theme }) => ({
|
||||||
projectId,
|
fontSize: '1.5rem',
|
||||||
health,
|
[theme.breakpoints.down('md')]: {
|
||||||
total,
|
fontSize: theme.fontSizes.bodySize,
|
||||||
stale,
|
},
|
||||||
}: IHealthWidgetProps) => {
|
}));
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
if (uiConfig?.flags?.newProjectOverview) {
|
export const HealthWidget = ({ projectId, health }: IHealthWidgetProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledProjectInfoWidgetContainer>
|
<StyledProjectInfoWidgetContainer>
|
||||||
<StyledWidgetTitle data-loading>
|
<StyledWidgetTitle data-loading>Project health</StyledWidgetTitle>
|
||||||
Project health
|
<Box
|
||||||
</StyledWidgetTitle>
|
sx={{
|
||||||
<StyledDivPercentageContainer>
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: theme => theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledPercentageText>
|
||||||
<PercentageCircle percentage={health} />
|
<PercentageCircle percentage={health} />
|
||||||
</StyledDivPercentageContainer>
|
</StyledPercentageText>
|
||||||
<StyledParagraphEmphasizedText data-loading>
|
<StyledParagraphEmphasizedText data-loading>
|
||||||
{health}%
|
{health}%
|
||||||
</StyledParagraphEmphasizedText>
|
</StyledParagraphEmphasizedText>
|
||||||
<Typography data-loading>
|
</Box>
|
||||||
<StyledCount>{total}</StyledCount> toggles in total
|
<WidgetFooterLink to={`/projects/${projectId}/health`}>
|
||||||
</Typography>
|
View project health
|
||||||
<Typography data-loading sx={{ marginBottom: 2 }}>
|
</WidgetFooterLink>
|
||||||
<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>
|
|
||||||
</StyledProjectInfoWidgetContainer>
|
</StyledProjectInfoWidgetContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,12 +1,12 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { styled, Typography } from '@mui/material';
|
||||||
import { Box, styled, Typography, Link } from '@mui/material';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StyledProjectInfoWidgetContainer,
|
StyledProjectInfoWidgetContainer,
|
||||||
StyledWidgetTitle,
|
StyledWidgetTitle,
|
||||||
} from './ProjectInfo.styles';
|
} from './ProjectInfo.styles';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { WidgetFooterLink } from './WidgetFooterLink';
|
||||||
|
|
||||||
interface IMetaWidgetProps {
|
interface IMetaWidgetProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -35,35 +35,29 @@ export const MetaWidget: FC<IMetaWidgetProps> = ({ id, description }) => {
|
|||||||
</Typography>{' '}
|
</Typography>{' '}
|
||||||
<code data-loading>{id || '__________'}</code>
|
<code data-loading>{id || '__________'}</code>
|
||||||
</StyledIDContainer>
|
</StyledIDContainer>
|
||||||
<Typography mt={1.5} textAlign="left">
|
<ConditionallyRender
|
||||||
<ConditionallyRender
|
condition={Boolean(description)}
|
||||||
condition={Boolean(description)}
|
show={
|
||||||
show={
|
<Typography
|
||||||
<>
|
variant="body2"
|
||||||
<Typography
|
sx={{
|
||||||
component="span"
|
marginTop: theme => theme.spacing(1.5),
|
||||||
variant="body2"
|
marginBottom: 0,
|
||||||
color="text.secondary"
|
textAlign: 'left',
|
||||||
>
|
}}
|
||||||
Description:{' '}
|
>
|
||||||
</Typography>
|
{description}
|
||||||
<Typography component="span" variant="body2">
|
</Typography>
|
||||||
{description}
|
}
|
||||||
</Typography>
|
/>
|
||||||
</>
|
<ConditionallyRender
|
||||||
}
|
condition={!Boolean(description)}
|
||||||
elseShow={
|
show={
|
||||||
<Typography variant="body2" textAlign="center">
|
<WidgetFooterLink to={`/projects/${id}/edit`}>
|
||||||
<Link
|
Add description
|
||||||
component={RouterLink}
|
</WidgetFooterLink>
|
||||||
to={`/projects/${id}/edit`}
|
}
|
||||||
>
|
/>
|
||||||
Add description
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
</StyledProjectInfoWidgetContainer>
|
</StyledProjectInfoWidgetContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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';
|
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 }) => ({
|
export const StyledProjectInfoWidgetContainer = styled('div')(({ theme }) => ({
|
||||||
margin: '0',
|
margin: '0',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: theme.spacing(3, 2, 3, 2),
|
minWidth: 225,
|
||||||
|
padding: theme.spacing(3),
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
margin: theme.spacing(0, 1),
|
display: 'flex',
|
||||||
...flexRow,
|
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
padding: theme.spacing(1.5),
|
padding: theme.spacing(1.5),
|
||||||
'&:first-of-type': {
|
|
||||||
marginLeft: '0',
|
|
||||||
},
|
|
||||||
'&:last-of-type': {
|
|
||||||
marginRight: '0',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledWidgetTitle = styled('p')(({ theme }) => ({
|
export const StyledWidgetTitle = styled('p')(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2.5),
|
||||||
}));
|
|
||||||
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledCount = styled('span')(({ theme }) => ({
|
export const StyledCount = styled('span')(({ theme }) => ({
|
||||||
@ -76,25 +25,3 @@ export const StyledCount = styled('span')(({ theme }) => ({
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: theme.palette.text.primary,
|
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),
|
|
||||||
}));
|
|
||||||
|
@ -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 type { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
||||||
import { StyledProjectInfoSidebarContainer } from './ProjectInfo.styles';
|
|
||||||
import { HealthWidget } from './HealthWidget';
|
import { HealthWidget } from './HealthWidget';
|
||||||
import { ToggleTypesWidget } from './ToggleTypesWidget';
|
import { ToggleTypesWidget } from './ToggleTypesWidget';
|
||||||
import { MetaWidget } from './MetaWidget';
|
import { MetaWidget } from './MetaWidget';
|
||||||
import { ProjectMembersWidget } from './ProjectMembersWidget';
|
import { ProjectMembersWidget } from './ProjectMembersWidget';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ProjectStatsSchema } from 'openapi/models/projectStatsSchema';
|
|
||||||
import { ChangeRequestsWidget } from './ChangeRequestsWidget';
|
import { ChangeRequestsWidget } from './ChangeRequestsWidget';
|
||||||
|
import { flexRow } from 'themes/themeStyles';
|
||||||
|
import { LegacyHealthWidget } from './LegacyHealthWidget';
|
||||||
|
import { LegacyProjectMembersWidget } from './LegacyProjectMembersWidget';
|
||||||
|
|
||||||
interface IProjectInfoProps {
|
interface IProjectInfoProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -19,6 +22,23 @@ interface IProjectInfoProps {
|
|||||||
stats: ProjectStatsSchema;
|
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 = ({
|
const ProjectInfo = ({
|
||||||
id,
|
id,
|
||||||
description,
|
description,
|
||||||
@ -28,29 +48,74 @@ const ProjectInfo = ({
|
|||||||
stats,
|
stats,
|
||||||
}: IProjectInfoProps) => {
|
}: IProjectInfoProps) => {
|
||||||
const { uiConfig, isEnterprise } = useUiConfig();
|
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 (
|
return (
|
||||||
<aside>
|
<aside>
|
||||||
<StyledProjectInfoSidebarContainer>
|
<StyledProjectInfoSidebarContainer
|
||||||
|
sx={
|
||||||
|
fitMoreColumns
|
||||||
|
? {
|
||||||
|
gridTemplateColumns:
|
||||||
|
'repeat(auto-fill, minmax(225px, 1fr))',
|
||||||
|
}
|
||||||
|
: { gridTemplateColumns: 'repeat(2, 1fr)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={showChangeRequestsWidget}
|
||||||
isEnterprise() &&
|
show={
|
||||||
Boolean(uiConfig?.flags.newProjectOverview)
|
<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
|
<ConditionallyRender
|
||||||
condition={Boolean(uiConfig?.flags.newProjectOverview)}
|
condition={showProjectMembersWidget}
|
||||||
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}
|
|
||||||
show={
|
show={
|
||||||
<ProjectMembersWidget
|
<ProjectMembersWidget
|
||||||
projectId={id}
|
projectId={id}
|
||||||
@ -59,10 +124,7 @@ const ProjectInfo = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ToggleTypesWidget features={features} />
|
||||||
condition={Boolean(uiConfig?.flags.newProjectOverview)}
|
|
||||||
show={<ToggleTypesWidget features={features} />}
|
|
||||||
/>
|
|
||||||
</StyledProjectInfoSidebarContainer>
|
</StyledProjectInfoSidebarContainer>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
StyledLink,
|
|
||||||
StyledProjectInfoWidgetContainer,
|
StyledProjectInfoWidgetContainer,
|
||||||
StyledSpanLinkText,
|
StyledWidgetTitle,
|
||||||
} from './ProjectInfo.styles';
|
} from './ProjectInfo.styles';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { StatusBox } from '../ProjectStats/StatusBox';
|
import { StatusBox } from '../ProjectStats/StatusBox';
|
||||||
|
import { WidgetFooterLink } from './WidgetFooterLink';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
interface IProjectMembersWidgetProps {
|
interface IProjectMembersWidgetProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -26,20 +27,17 @@ export const ProjectMembersWidget = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledProjectInfoWidgetContainer
|
<StyledProjectInfoWidgetContainer>
|
||||||
sx={{ padding: theme => theme.spacing(0, 0, 3, 0) }}
|
<StyledWidgetTitle data-loading>Project members</StyledWidgetTitle>
|
||||||
>
|
<Box
|
||||||
<StatusBox
|
sx={{
|
||||||
title={'Project members'}
|
display: 'flex',
|
||||||
boxText={`${memberCount}`}
|
justifyContent: 'center',
|
||||||
change={change}
|
}}
|
||||||
fullWidthBodyText
|
>
|
||||||
/>
|
<StatusBox boxText={`${memberCount}`} change={change} />
|
||||||
<StyledLink data-loading to={link}>
|
</Box>
|
||||||
<StyledSpanLinkText data-loading>
|
<WidgetFooterLink to={link}>View all members</WidgetFooterLink>
|
||||||
View all members
|
|
||||||
</StyledSpanLinkText>
|
|
||||||
</StyledLink>
|
|
||||||
</StyledProjectInfoWidgetContainer>
|
</StyledProjectInfoWidgetContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,6 @@ import type { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
|||||||
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||||
import {
|
import {
|
||||||
StyledCount,
|
StyledCount,
|
||||||
StyledParagraphGridRow,
|
|
||||||
StyledProjectInfoWidgetContainer,
|
StyledProjectInfoWidgetContainer,
|
||||||
StyledWidgetTitle,
|
StyledWidgetTitle,
|
||||||
} from './ProjectInfo.styles';
|
} from './ProjectInfo.styles';
|
||||||
@ -14,8 +13,10 @@ export interface IToggleTypesWidgetProps {
|
|||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledTypeCount = styled(StyledCount)(() => ({
|
const StyledTypeCount = styled(StyledCount)(({ theme }) => ({
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IToggleTypeRowProps {
|
interface IToggleTypeRowProps {
|
||||||
@ -23,10 +24,26 @@ interface IToggleTypeRowProps {
|
|||||||
Icon: OverridableComponent<SvgIconTypeMap>;
|
Icon: OverridableComponent<SvgIconTypeMap>;
|
||||||
count: number;
|
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 ToggleTypesRow = ({ type, Icon, count }: IToggleTypeRowProps) => {
|
||||||
const getTitleText = (str: string) => {
|
const getTitleText = (str: string) => {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1).replace('-', ' ');
|
return str.charAt(0).toUpperCase() + str.slice(1).replace('-', ' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledParagraphGridRow data-loading>
|
<StyledParagraphGridRow data-loading>
|
||||||
<Icon fontSize="small" data-loading />
|
<Icon fontSize="small" data-loading />
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,21 +1,39 @@
|
|||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
import { ProjectStatsSchema } from 'openapi/models';
|
import { ProjectStatsSchema } from 'openapi/models';
|
||||||
import { object } from 'prop-types';
|
import { object } from 'prop-types';
|
||||||
import { StatusBox } from './StatusBox';
|
import { StatusBox } from './StatusBox';
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
padding: theme.spacing(0, 0, 2, 2),
|
padding: theme.spacing(0, 0, 2, 2),
|
||||||
display: 'flex',
|
display: 'grid',
|
||||||
justifyContent: 'space-between',
|
gap: theme.spacing(2),
|
||||||
|
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
|
[theme.breakpoints.down('lg')]: {
|
||||||
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||||
|
},
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
paddingLeft: 0,
|
padding: theme.spacing(0, 0, 2),
|
||||||
},
|
},
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
flexDirection: 'column',
|
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 {
|
interface IProjectStatsProps {
|
||||||
stats: ProjectStatsSchema;
|
stats: ProjectStatsSchema;
|
||||||
}
|
}
|
||||||
@ -47,32 +65,53 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<StatusBox
|
<StyledWidget>
|
||||||
title="Total changes"
|
<StatusBox
|
||||||
boxText={String(projectActivityCurrentWindow)}
|
title="Total changes"
|
||||||
change={
|
boxText={String(projectActivityCurrentWindow)}
|
||||||
projectActivityCurrentWindow - projectActivityPastWindow
|
change={
|
||||||
}
|
projectActivityCurrentWindow -
|
||||||
/>
|
projectActivityPastWindow -
|
||||||
<StatusBox
|
20
|
||||||
title="Avg. time to production"
|
}
|
||||||
boxText={`${avgTimeToProdCurrentWindow} days`}
|
/>
|
||||||
change={calculatePercentage(
|
</StyledWidget>
|
||||||
avgTimeToProdCurrentWindow,
|
<StyledWidget>
|
||||||
avgTimeToProdPastWindow
|
<StatusBox
|
||||||
)}
|
title="Avg. time to production"
|
||||||
percentage
|
boxText={
|
||||||
/>{' '}
|
<Box
|
||||||
<StatusBox
|
sx={{
|
||||||
title="Features created"
|
display: 'flex',
|
||||||
boxText={String(createdCurrentWindow)}
|
alignItems: 'center',
|
||||||
change={createdCurrentWindow - createdPastWindow}
|
gap: theme => theme.spacing(1),
|
||||||
/>
|
}}
|
||||||
<StatusBox
|
>
|
||||||
title="Features archived"
|
{avgTimeToProdCurrentWindow}{' '}
|
||||||
boxText={String(archivedCurrentWindow)}
|
<Typography component="span">days</Typography>
|
||||||
change={archivedCurrentWindow - archivedPastWindow}
|
</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>
|
</StyledBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 { Box, Typography, styled } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { flexRow } from 'themes/themeStyles';
|
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 }) => ({
|
const StyledTypographyHeader = styled(Typography)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTypographyCount = styled(Typography)(({ theme }) => ({
|
const StyledTypographyCount = styled(Typography)(({ theme }) => ({
|
||||||
fontSize: theme.fontSizes.largeHeader,
|
fontSize: theme.fontSizes.largeHeader,
|
||||||
fontWeight: 'bold',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledBoxChangeContainer = styled(Box)(({ theme }) => ({
|
const StyledBoxChangeContainer = styled(Box)(({ theme }) => ({
|
||||||
...flexRow,
|
...flexRow,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginLeft: theme.spacing(1.5),
|
marginLeft: theme.spacing(2.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTypographySubtext = styled(Typography)(({ theme }) => ({
|
const StyledTypographySubtext = styled(Typography)(({ theme }) => ({
|
||||||
color: theme.palette.neutral.main,
|
color: theme.palette.neutral.main,
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.typography.body2.fontSize,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTypographyChange = styled(Typography)(({ theme }) => ({
|
const StyledTypographyChange = styled(Typography)(({ theme }) => ({
|
||||||
marginLeft: theme.spacing(1),
|
marginLeft: theme.spacing(1),
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.typography.body1.fontSize,
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IStatusBoxProps {
|
interface IStatusBoxProps {
|
||||||
title: string;
|
title?: string;
|
||||||
boxText: string;
|
boxText: ReactNode;
|
||||||
change: number;
|
change: number;
|
||||||
percentage?: boolean;
|
percentage?: boolean;
|
||||||
fullWidthBodyText?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveIcon = (change: number) => {
|
const resolveIcon = (change: number) => {
|
||||||
if (change > 0) {
|
if (change > 0) {
|
||||||
return (
|
return (
|
||||||
<ArrowOutward
|
<CallMade sx={{ color: 'success.dark', height: 20, width: 20 }} />
|
||||||
sx={{ color: 'success.main', height: 18, width: 18 }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <SouthEast sx={{ color: 'warning.dark', height: 18, width: 18 }} />;
|
return <SouthEast sx={{ color: 'warning.dark', height: 20, width: 20 }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveColor = (change: number) => {
|
const resolveColor = (change: number) => {
|
||||||
if (change > 0) {
|
if (change > 0) {
|
||||||
return 'success.main';
|
return 'success.dark';
|
||||||
}
|
}
|
||||||
return 'error.main';
|
return 'warning.dark';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StatusBox = ({
|
export const StatusBox = ({
|
||||||
@ -82,52 +58,51 @@ export const StatusBox = ({
|
|||||||
boxText,
|
boxText,
|
||||||
change,
|
change,
|
||||||
percentage,
|
percentage,
|
||||||
fullWidthBodyText,
|
}: IStatusBoxProps) => (
|
||||||
}: IStatusBoxProps) => {
|
<>
|
||||||
return (
|
<ConditionallyRender
|
||||||
<StyledBox>
|
condition={Boolean(title)}
|
||||||
<StyledTypographyHeader>{title}</StyledTypographyHeader>
|
show={<StyledTypographyHeader>{title}</StyledTypographyHeader>}
|
||||||
<Box
|
/>
|
||||||
sx={{
|
<Box
|
||||||
...flexRow,
|
sx={{
|
||||||
justifyContent: fullWidthBodyText
|
...flexRow,
|
||||||
? 'space-between'
|
justifyContent: 'center',
|
||||||
: 'center',
|
width: 'auto',
|
||||||
width: fullWidthBodyText ? '65%' : 'auto',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<StyledTypographyCount>{boxText}</StyledTypographyCount>
|
||||||
<StyledTypographyCount>{boxText}</StyledTypographyCount>
|
<ConditionallyRender
|
||||||
<ConditionallyRender
|
condition={change !== 0}
|
||||||
condition={change !== 0}
|
show={
|
||||||
show={
|
<StyledBoxChangeContainer>
|
||||||
<StyledBoxChangeContainer>
|
<Box
|
||||||
<Box
|
sx={{
|
||||||
sx={{
|
...flexRow,
|
||||||
...flexRow,
|
}}
|
||||||
}}
|
>
|
||||||
|
{resolveIcon(change)}
|
||||||
|
<StyledTypographyChange
|
||||||
|
color={resolveColor(change)}
|
||||||
>
|
>
|
||||||
{resolveIcon(change)}
|
{change > 0 ? '+' : ''}
|
||||||
<StyledTypographyChange
|
{change}
|
||||||
color={resolveColor(change)}
|
{percentage ? '%' : ''}
|
||||||
>
|
</StyledTypographyChange>
|
||||||
{change}
|
</Box>
|
||||||
{percentage ? '%' : ''}
|
<StyledTypographySubtext>
|
||||||
</StyledTypographyChange>
|
this month
|
||||||
</Box>
|
</StyledTypographySubtext>
|
||||||
<StyledTypographySubtext>
|
</StyledBoxChangeContainer>
|
||||||
this month
|
}
|
||||||
</StyledTypographySubtext>
|
elseShow={
|
||||||
</StyledBoxChangeContainer>
|
<StyledBoxChangeContainer>
|
||||||
}
|
<StyledTypographySubtext>
|
||||||
elseShow={
|
No change
|
||||||
<StyledBoxChangeContainer>
|
</StyledTypographySubtext>
|
||||||
<StyledTypographySubtext>
|
</StyledBoxChangeContainer>
|
||||||
No change
|
}
|
||||||
</StyledTypographySubtext>
|
/>
|
||||||
</StyledBoxChangeContainer>
|
</Box>
|
||||||
}
|
</>
|
||||||
/>
|
);
|
||||||
</Box>
|
|
||||||
</StyledBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user