mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: change request header (#2317)
* feat: change request header * fix: dom nesting paragraphs * fix: change path
This commit is contained in:
parent
6622346286
commit
2f1f9cecc2
@ -0,0 +1,37 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { Avatar, Box, Card, Paper, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
export const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2, 4),
|
||||||
|
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledInnerContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledHeader = styled(Typography)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledCard = styled(Card)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
backgroundColor: theme.palette.tertiary.light,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledAvatar = styled(Avatar)(() => ({
|
||||||
|
height: '30px',
|
||||||
|
width: '30px',
|
||||||
|
}));
|
@ -1,69 +1,60 @@
|
|||||||
|
import { Box } from '@mui/material';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Avatar, Box, Card, Paper, Typography } from '@mui/material';
|
import { Typography } from '@mui/material';
|
||||||
import { PlaygroundResultChip } from 'component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip';
|
|
||||||
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
|
||||||
import TimeAgo from 'react-timeago';
|
import TimeAgo from 'react-timeago';
|
||||||
|
import { resolveChangeRequestStatusIcon } from 'component/changeRequest/changeRequest.utils';
|
||||||
|
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
|
||||||
|
import {
|
||||||
|
StyledPaper,
|
||||||
|
StyledContainer,
|
||||||
|
StyledHeader,
|
||||||
|
StyledInnerContainer,
|
||||||
|
StyledAvatar,
|
||||||
|
StyledCard,
|
||||||
|
} from './ChangeRequestHeader.styles';
|
||||||
|
|
||||||
export const ChangeRequestHeader: FC<{ changeRequest: any }> = ({
|
export const ChangeRequestHeader: FC<{ changeRequest: IChangeRequest }> = ({
|
||||||
changeRequest,
|
changeRequest,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<StyledPaper elevation={0}>
|
||||||
elevation={0}
|
<StyledContainer>
|
||||||
sx={theme => ({
|
<StyledHeader variant="h1">
|
||||||
p: theme.spacing(2, 4),
|
Change request #{changeRequest.id}
|
||||||
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`,
|
</StyledHeader>
|
||||||
})}
|
{resolveChangeRequestStatusIcon(changeRequest.state)}
|
||||||
>
|
</StyledContainer>
|
||||||
<Box
|
<StyledInnerContainer>
|
||||||
sx={theme => ({
|
<Typography variant="body2" sx={{ margin: 'auto 0' }}>
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 2,
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
variant="h1"
|
|
||||||
>
|
|
||||||
Change request
|
|
||||||
<Typography variant="h1" component="p">
|
|
||||||
#{changeRequest.id}
|
|
||||||
</Typography>
|
|
||||||
</Typography>
|
|
||||||
<PlaygroundResultChip
|
|
||||||
// icon={<ChangesAppliedIcon strokeWidth="0.25" />}
|
|
||||||
label="Changes approved"
|
|
||||||
enabled
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}>
|
|
||||||
<Typography sx={{ margin: 'auto 0' }}>
|
|
||||||
Created <TimeAgo date={new Date(changeRequest.createdAt)} />{' '}
|
Created <TimeAgo date={new Date(changeRequest.createdAt)} />{' '}
|
||||||
by
|
by
|
||||||
</Typography>
|
</Typography>
|
||||||
<Avatar src={changeRequest?.createdBy?.avatar} />
|
<StyledAvatar src={changeRequest?.createdBy?.imageUrl} />
|
||||||
<Card
|
<Box>
|
||||||
variant="outlined"
|
<StyledCard variant="outlined">
|
||||||
sx={theme => ({
|
<Typography variant="body2">
|
||||||
padding: 1,
|
Environment:{' '}
|
||||||
backgroundColor: theme.palette.tertiary.light,
|
<Typography
|
||||||
})}
|
display="inline"
|
||||||
>
|
fontWeight="bold"
|
||||||
Environment:{' '}
|
variant="body2"
|
||||||
<Typography display="inline" fontWeight="bold">
|
component="span"
|
||||||
{changeRequest?.environment}
|
>
|
||||||
</Typography>{' '}
|
{changeRequest?.environment}
|
||||||
| Updates:{' '}
|
</Typography>{' '}
|
||||||
<Typography display="inline" fontWeight="bold">
|
| Updates:{' '}
|
||||||
{changeRequest?.features.length} feature toggles
|
<Typography
|
||||||
</Typography>
|
variant="body2"
|
||||||
</Card>
|
display="inline"
|
||||||
</Box>
|
fontWeight="bold"
|
||||||
</Paper>
|
component="span"
|
||||||
|
>
|
||||||
|
{changeRequest?.features.length} feature toggles
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
</StyledCard>
|
||||||
|
</Box>
|
||||||
|
</StyledInnerContainer>
|
||||||
|
</StyledPaper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@ import TimelineDot from '@mui/lab/TimelineDot';
|
|||||||
import TimelineConnector from '@mui/lab/TimelineConnector';
|
import TimelineConnector from '@mui/lab/TimelineConnector';
|
||||||
import TimelineContent from '@mui/lab/TimelineContent';
|
import TimelineContent from '@mui/lab/TimelineContent';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { ChangeRequestState } from '../changeRequest.types';
|
import { ChangeRequestState } from '../../changeRequest.types';
|
||||||
interface ISuggestChangeTimelineProps {
|
interface ISuggestChangeTimelineProps {
|
||||||
state: ChangeRequestState;
|
state: ChangeRequestState;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
export type ChangeRequestState =
|
|
||||||
| 'Draft'
|
|
||||||
| 'Approved'
|
|
||||||
| 'In review'
|
|
||||||
| 'Applied'
|
|
||||||
| 'Cancelled';
|
|
@ -1,115 +1,23 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { Chip, styled } from '@mui/material';
|
|
||||||
import { colors } from 'themes/colors';
|
|
||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { Check, CircleOutlined, Close } from '@mui/icons-material';
|
import { resolveChangeRequestStatusIcon } from 'component/changeRequest/changeRequest.utils';
|
||||||
|
import { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
|
||||||
|
|
||||||
interface IChangeRequestStatusCellProps {
|
interface IChangeRequestStatusCellProps {
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChangeRequestState {
|
|
||||||
DRAFT = 'Draft',
|
|
||||||
APPROVED = 'Approved',
|
|
||||||
IN_REVIEW = 'In review',
|
|
||||||
APPLIED = 'Applied',
|
|
||||||
CANCELLED = 'Cancelled',
|
|
||||||
REJECTED = 'Rejected',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StyledChip = styled(Chip)(({ theme, icon }) => ({
|
|
||||||
padding: theme.spacing(0, 1),
|
|
||||||
height: 30,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
|
||||||
gap: theme.spacing(1, 1),
|
|
||||||
['& .MuiChip-label']: {
|
|
||||||
padding: 0,
|
|
||||||
paddingLeft: Boolean(icon) ? theme.spacing(0.5) : 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledRejectedChip = styled(StyledChip)(({ theme }) => ({
|
|
||||||
border: `1px solid ${theme.palette.error.main}`,
|
|
||||||
backgroundColor: colors.red['100'],
|
|
||||||
['& .MuiChip-label']: {
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
},
|
|
||||||
['& .MuiChip-icon']: {
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledApprovedChip = styled(StyledChip)(({ theme }) => ({
|
|
||||||
border: `1px solid ${theme.palette.success.main}`,
|
|
||||||
backgroundColor: colors.green['100'],
|
|
||||||
['& .MuiChip-label']: {
|
|
||||||
color: theme.palette.success.main,
|
|
||||||
},
|
|
||||||
['& .MuiChip-icon']: {
|
|
||||||
color: theme.palette.success.main,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledReviewChip = styled(StyledChip)(({ theme }) => ({
|
|
||||||
border: `1px solid ${theme.palette.primary.main}`,
|
|
||||||
backgroundColor: colors.purple['100'],
|
|
||||||
['& .MuiChip-label']: {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
['& .MuiChip-icon']: {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ChangeRequestStatusCell: VFC<IChangeRequestStatusCellProps> = ({
|
export const ChangeRequestStatusCell: VFC<IChangeRequestStatusCellProps> = ({
|
||||||
value,
|
value,
|
||||||
}) => {
|
}) => {
|
||||||
const renderState = (state: string) => {
|
const renderState = () => {
|
||||||
switch (state) {
|
if (!value) return null;
|
||||||
case ChangeRequestState.IN_REVIEW:
|
return resolveChangeRequestStatusIcon(value as ChangeRequestState);
|
||||||
return (
|
|
||||||
<StyledReviewChip
|
|
||||||
label={'Review required'}
|
|
||||||
icon={<CircleOutlined fontSize={'small'} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case ChangeRequestState.APPROVED:
|
|
||||||
return (
|
|
||||||
<StyledApprovedChip
|
|
||||||
label={'Approved'}
|
|
||||||
icon={<Check fontSize={'small'} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case ChangeRequestState.APPLIED:
|
|
||||||
return (
|
|
||||||
<StyledApprovedChip
|
|
||||||
label={'Applied'}
|
|
||||||
icon={<Check fontSize={'small'} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case ChangeRequestState.CANCELLED:
|
|
||||||
return (
|
|
||||||
<StyledRejectedChip
|
|
||||||
label={'Cancelled'}
|
|
||||||
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case ChangeRequestState.REJECTED:
|
|
||||||
return (
|
|
||||||
<StyledRejectedChip
|
|
||||||
label={'Rejected'}
|
|
||||||
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return <TextCell />;
|
return <TextCell />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TextCell>{renderState(value)}</TextCell>;
|
return <TextCell>{renderState()}</TextCell>;
|
||||||
};
|
};
|
||||||
|
36
frontend/src/component/changeRequest/changeRequest.types.ts
Normal file
36
frontend/src/component/changeRequest/changeRequest.types.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export type ChangeRequestState =
|
||||||
|
| 'Draft'
|
||||||
|
| 'Approved'
|
||||||
|
| 'In review'
|
||||||
|
| 'Applied'
|
||||||
|
| 'Cancelled';
|
||||||
|
|
||||||
|
export interface IChangeRequest {
|
||||||
|
id: number;
|
||||||
|
environment: string;
|
||||||
|
state: ChangeRequestState;
|
||||||
|
project: string;
|
||||||
|
createdBy: ICreatedBy;
|
||||||
|
createdAt: string;
|
||||||
|
features: IChangeRequestFeatures[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICreatedBy {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
imageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IChangeRequestFeatures {
|
||||||
|
name: string;
|
||||||
|
changes: IChangeRequestFeatureChanges[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IChangeRequestFeatureChanges {
|
||||||
|
id: number;
|
||||||
|
action: string;
|
||||||
|
payload: unknown;
|
||||||
|
createdAt: string;
|
||||||
|
createdBy: ICreatedBy;
|
||||||
|
warning?: string;
|
||||||
|
}
|
40
frontend/src/component/changeRequest/changeRequest.utils.tsx
Normal file
40
frontend/src/component/changeRequest/changeRequest.utils.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ChangeRequestState } from './changeRequest.types';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { Check, CircleOutlined, Close } from '@mui/icons-material';
|
||||||
|
|
||||||
|
export const resolveChangeRequestStatusIcon = (state: ChangeRequestState) => {
|
||||||
|
const reviewRequired = (
|
||||||
|
<Badge color="secondary" icon={<CircleOutlined fontSize={'small'} />}>
|
||||||
|
Review required
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
switch (state) {
|
||||||
|
case 'Draft':
|
||||||
|
return reviewRequired;
|
||||||
|
case 'In review':
|
||||||
|
return reviewRequired;
|
||||||
|
case 'Approved':
|
||||||
|
return (
|
||||||
|
<Badge color="success" icon={<Check fontSize={'small'} />}>
|
||||||
|
Approved
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case 'Applied':
|
||||||
|
return (
|
||||||
|
<Badge color="success" icon={<Check fontSize={'small'} />}>
|
||||||
|
Applied
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case 'Cancelled':
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color="error"
|
||||||
|
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
|
||||||
|
>
|
||||||
|
Cancelled
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return reviewRequired;
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user