1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-12 01:17:04 +02:00

feat: scheduled change request state (#5261)

Adds the scheduled state to ChangeRequestOverview.tsx

<img width="1523" alt="Screenshot 2023-11-03 at 12 52 07"
src="https://github.com/Unleash/unleash/assets/104830839/710b5f26-04a0-4ee9-b646-8ff3090ad86a">

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
andreas-unleash 2023-11-03 15:02:51 +02:00 committed by GitHub
parent 43298e16e2
commit 6b637d5fa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 10 deletions

View File

@ -9,7 +9,8 @@ export const ApplyButton: FC<{
disabled: boolean;
onSchedule: () => void;
onApply: () => void;
}> = ({ disabled, onSchedule, onApply, children }) => (
variant?: 'create' | 'update';
}> = ({ disabled, onSchedule, onApply, variant = 'create', children }) => (
<MultiActionButton
permission={APPLY_CHANGE_REQUEST}
disabled={disabled}
@ -20,7 +21,10 @@ export const ApplyButton: FC<{
icon: <CheckBox fontSize='small' />,
},
{
label: 'Schedule changes',
label:
variant === 'create'
? 'Schedule changes'
: 'Update schedule',
onSelect: onSchedule,
icon: <Today fontSize='small' />,
},

View File

@ -26,7 +26,6 @@ import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestRe
import { ChangeRequestRejectDialogue } from './ChangeRequestRejectDialog/ChangeRequestRejectDialog';
import { ApplyButton } from './ApplyButton/ApplyButton';
import { useUiFlag } from 'hooks/useUiFlag';
import { scheduler } from 'timers/promises';
const StyledAsideBox = styled(Box)(({ theme }) => ({
width: '30%',
@ -85,7 +84,6 @@ export const ChangeRequestOverview: FC = () => {
const { setToastData, setToastApiError } = useToast();
const { isChangeRequestConfiguredForReview } =
useChangeRequestsEnabled(projectId);
const scheduleChangeRequests = useUiFlag('scheduledConfigurationChanges');
if (!changeRequest) {
@ -191,7 +189,7 @@ export const ChangeRequestOverview: FC = () => {
changeRequest.state === 'In review' &&
!isAdmin;
const hasApprovedAlready = changeRequest.approvals.some(
const hasApprovedAlready = changeRequest.approvals?.some(
(approval) => approval.createdBy.id === user?.id,
);
@ -312,6 +310,35 @@ export const ChangeRequestOverview: FC = () => {
/>
}
/>
<ConditionallyRender
condition={
scheduleChangeRequests &&
changeRequest.state === 'Scheduled' &&
changeRequest.schedule?.status === 'pending'
}
show={
<ApplyButton
onApply={() => {
console.log(
'I would show the apply now dialog',
);
}}
disabled={
!allowChangeRequestActions ||
loading
}
onSchedule={() => {
console.log(
'I would schedule changes now',
);
}}
variant={'update'}
>
Apply or schedule changes
</ApplyButton>
}
/>
<ConditionallyRender
condition={
changeRequest.state !== 'Applied' &&
@ -329,7 +356,10 @@ export const ChangeRequestOverview: FC = () => {
variant='outlined'
onClick={onCancel}
>
Cancel changes
{changeRequest.schedule
? 'Reject'
: 'Cancel'}{' '}
changes
</Button>
}
/>

View File

@ -1,5 +1,5 @@
import { styled } from '@mui/material';
import { Cancel, CheckCircle } from '@mui/icons-material';
import { Cancel, CheckCircle, Schedule, Edit } from '@mui/icons-material';
import { Box, Typography, Divider } from '@mui/material';
const styledComponentPropCheck = () => (prop: string) =>
@ -36,6 +36,18 @@ export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({
marginRight: theme.spacing(1),
}));
export const StyledScheduledIcon = styled(Schedule)(({ theme }) => ({
color: theme.palette.warning.main,
height: '35px',
width: '35px',
marginRight: theme.spacing(1),
}));
export const StyledEditIcon = styled(Edit)(({ theme }) => ({
color: theme.palette.text.secondary,
height: '24px',
width: '24px',
}));
export const StyledOuterContainer = styled(Box)(({ theme }) => ({
display: 'flex',
marginTop: theme.spacing(2),
@ -77,3 +89,10 @@ export const StyledReviewTitle = styled(Typography, {
fontWeight: 'bold',
color,
}));
export const StyledScheduledBox = styled(Box)({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
});

View File

@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { Box, Theme, Typography, useTheme } from '@mui/material';
import { Box, IconButton, Theme, Typography, useTheme } from '@mui/material';
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
import {
StyledOuterContainer,
@ -11,6 +11,9 @@ import {
StyledWarningIcon,
StyledReviewTitle,
StyledDivider,
StyledScheduledIcon,
StyledEditIcon,
StyledScheduledBox,
} from './ChangeRequestReviewStatus.styles';
import {
ChangeRequestState,
@ -21,7 +24,7 @@ interface ISuggestChangeReviewsStatusProps {
changeRequest: IChangeRequest;
}
const resolveBorder = (state: ChangeRequestState, theme: Theme) => {
if (state === 'Approved') {
if (state === 'Approved' || state === 'Scheduled') {
return `2px solid ${theme.palette.success.main}`;
}
@ -109,6 +112,12 @@ const ResolveComponent = ({ changeRequest }: IResolveComponentProps) => {
return <Rejected />;
}
if (state === 'Scheduled') {
return (
<Scheduled scheduledDate={changeRequest.schedule?.scheduledAt} />
);
}
return <ReviewRequired minApprovals={changeRequest.minApprovals} />;
};
@ -194,6 +203,69 @@ const Applied = () => {
);
};
interface IScheduledProps {
scheduledDate?: string;
}
const Scheduled = ({ scheduledDate }: IScheduledProps) => {
const theme = useTheme();
if (!scheduledDate) {
return null;
}
const getBrowserTimezone = (): string => {
const offset = -new Date().getTimezoneOffset();
const hours = Math.floor(Math.abs(offset) / 60);
const minutes = Math.abs(offset) % 60;
let sign = '+';
if (offset < 0) {
sign = '-';
}
// Ensure that hours and minutes are two digits
const zeroPaddedHours = hours.toString().padStart(2, '0');
const zeroPaddedMinutes = minutes.toString().padStart(2, '0');
return `UTC${sign}${zeroPaddedHours}:${zeroPaddedMinutes}`;
};
const timezone = getBrowserTimezone();
return (
<>
<StyledFlexAlignCenterBox>
<StyledSuccessIcon />
<Box>
<StyledReviewTitle color={theme.palette.success.dark}>
Changes approved
</StyledReviewTitle>
<Typography>
One approving review from requested approvers
</Typography>
</Box>
</StyledFlexAlignCenterBox>
<StyledDivider />
<StyledScheduledBox>
<StyledFlexAlignCenterBox>
<StyledScheduledIcon />
<Box>
<StyledReviewTitle color={theme.palette.warning.dark}>
Changes are scheduled to be applied on:{' '}
{new Date(scheduledDate).toLocaleString()}
</StyledReviewTitle>
<Typography>Your timezone is {timezone}</Typography>
</Box>
</StyledFlexAlignCenterBox>
<IconButton>
<StyledEditIcon />
</IconButton>
</StyledScheduledBox>
</>
);
};
const Cancelled = () => {
const theme = useTheme();

View File

@ -1,7 +1,7 @@
import { VFC } from 'react';
import { ChangeRequestState } from '../changeRequest.types';
import { Badge } from 'component/common/Badge/Badge';
import { Check, CircleOutlined, Close } from '@mui/icons-material';
import { AccessTime, Check, CircleOutlined, Close } from '@mui/icons-material';
interface IChangeRequestStatusBadgeProps {
state: ChangeRequestState;
@ -47,6 +47,12 @@ export const ChangeRequestStatusBadge: VFC<IChangeRequestStatusBadgeProps> = ({
Rejected
</Badge>
);
case 'Scheduled':
return (
<Badge color='warning' icon={<AccessTime fontSize={'small'} />}>
Scheduled
</Badge>
);
default:
return <ReviewRequiredBadge />;
}

View File

@ -18,6 +18,12 @@ export interface IChangeRequest {
rejections: IChangeRequestApproval[];
comments: IChangeRequestComment[];
conflict?: string;
schedule?: IChangeRequestSchedule;
}
export interface IChangeRequestSchedule {
scheduledAt: string;
status: 'pending' | 'failed';
}
export interface IChangeRequestEnvironmentConfig {
@ -67,6 +73,7 @@ export type ChangeRequestState =
| 'Approved'
| 'In review'
| 'Applied'
| 'Scheduled'
| 'Cancelled'
| 'Rejected';