1
0
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:
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> </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:&nbsp;
</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:&nbsp;
</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>

View File

@ -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>
); );
}; };

View File

@ -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>
); );
}; };

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 { 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>
); );
}; };

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'; 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),
}));

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 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>
); );

View File

@ -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>
); );
}; };

View File

@ -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 />

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 { 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>
); );
}; };

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 { 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>
);
};