mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +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:
parent
43298e16e2
commit
6b637d5fa9
@ -9,7 +9,8 @@ export const ApplyButton: FC<{
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onSchedule: () => void;
|
onSchedule: () => void;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
}> = ({ disabled, onSchedule, onApply, children }) => (
|
variant?: 'create' | 'update';
|
||||||
|
}> = ({ disabled, onSchedule, onApply, variant = 'create', children }) => (
|
||||||
<MultiActionButton
|
<MultiActionButton
|
||||||
permission={APPLY_CHANGE_REQUEST}
|
permission={APPLY_CHANGE_REQUEST}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -20,7 +21,10 @@ export const ApplyButton: FC<{
|
|||||||
icon: <CheckBox fontSize='small' />,
|
icon: <CheckBox fontSize='small' />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Schedule changes',
|
label:
|
||||||
|
variant === 'create'
|
||||||
|
? 'Schedule changes'
|
||||||
|
: 'Update schedule',
|
||||||
onSelect: onSchedule,
|
onSelect: onSchedule,
|
||||||
icon: <Today fontSize='small' />,
|
icon: <Today fontSize='small' />,
|
||||||
},
|
},
|
||||||
|
@ -26,7 +26,6 @@ import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestRe
|
|||||||
import { ChangeRequestRejectDialogue } from './ChangeRequestRejectDialog/ChangeRequestRejectDialog';
|
import { ChangeRequestRejectDialogue } from './ChangeRequestRejectDialog/ChangeRequestRejectDialog';
|
||||||
import { ApplyButton } from './ApplyButton/ApplyButton';
|
import { ApplyButton } from './ApplyButton/ApplyButton';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { scheduler } from 'timers/promises';
|
|
||||||
|
|
||||||
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
||||||
width: '30%',
|
width: '30%',
|
||||||
@ -85,7 +84,6 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { isChangeRequestConfiguredForReview } =
|
const { isChangeRequestConfiguredForReview } =
|
||||||
useChangeRequestsEnabled(projectId);
|
useChangeRequestsEnabled(projectId);
|
||||||
|
|
||||||
const scheduleChangeRequests = useUiFlag('scheduledConfigurationChanges');
|
const scheduleChangeRequests = useUiFlag('scheduledConfigurationChanges');
|
||||||
|
|
||||||
if (!changeRequest) {
|
if (!changeRequest) {
|
||||||
@ -191,7 +189,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
changeRequest.state === 'In review' &&
|
changeRequest.state === 'In review' &&
|
||||||
!isAdmin;
|
!isAdmin;
|
||||||
|
|
||||||
const hasApprovedAlready = changeRequest.approvals.some(
|
const hasApprovedAlready = changeRequest.approvals?.some(
|
||||||
(approval) => approval.createdBy.id === user?.id,
|
(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
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
changeRequest.state !== 'Applied' &&
|
changeRequest.state !== 'Applied' &&
|
||||||
@ -329,7 +356,10 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
variant='outlined'
|
variant='outlined'
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
Cancel changes
|
{changeRequest.schedule
|
||||||
|
? 'Reject'
|
||||||
|
: 'Cancel'}{' '}
|
||||||
|
changes
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { styled } from '@mui/material';
|
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';
|
import { Box, Typography, Divider } from '@mui/material';
|
||||||
|
|
||||||
const styledComponentPropCheck = () => (prop: string) =>
|
const styledComponentPropCheck = () => (prop: string) =>
|
||||||
@ -36,6 +36,18 @@ export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({
|
|||||||
marginRight: theme.spacing(1),
|
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 }) => ({
|
export const StyledOuterContainer = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
@ -77,3 +89,10 @@ export const StyledReviewTitle = styled(Typography, {
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color,
|
color,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const StyledScheduledBox = styled(Box)({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { FC } from 'react';
|
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 { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
||||||
import {
|
import {
|
||||||
StyledOuterContainer,
|
StyledOuterContainer,
|
||||||
@ -11,6 +11,9 @@ import {
|
|||||||
StyledWarningIcon,
|
StyledWarningIcon,
|
||||||
StyledReviewTitle,
|
StyledReviewTitle,
|
||||||
StyledDivider,
|
StyledDivider,
|
||||||
|
StyledScheduledIcon,
|
||||||
|
StyledEditIcon,
|
||||||
|
StyledScheduledBox,
|
||||||
} from './ChangeRequestReviewStatus.styles';
|
} from './ChangeRequestReviewStatus.styles';
|
||||||
import {
|
import {
|
||||||
ChangeRequestState,
|
ChangeRequestState,
|
||||||
@ -21,7 +24,7 @@ interface ISuggestChangeReviewsStatusProps {
|
|||||||
changeRequest: IChangeRequest;
|
changeRequest: IChangeRequest;
|
||||||
}
|
}
|
||||||
const resolveBorder = (state: ChangeRequestState, theme: Theme) => {
|
const resolveBorder = (state: ChangeRequestState, theme: Theme) => {
|
||||||
if (state === 'Approved') {
|
if (state === 'Approved' || state === 'Scheduled') {
|
||||||
return `2px solid ${theme.palette.success.main}`;
|
return `2px solid ${theme.palette.success.main}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +112,12 @@ const ResolveComponent = ({ changeRequest }: IResolveComponentProps) => {
|
|||||||
return <Rejected />;
|
return <Rejected />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state === 'Scheduled') {
|
||||||
|
return (
|
||||||
|
<Scheduled scheduledDate={changeRequest.schedule?.scheduledAt} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <ReviewRequired minApprovals={changeRequest.minApprovals} />;
|
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 Cancelled = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { ChangeRequestState } from '../changeRequest.types';
|
import { ChangeRequestState } from '../changeRequest.types';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
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 {
|
interface IChangeRequestStatusBadgeProps {
|
||||||
state: ChangeRequestState;
|
state: ChangeRequestState;
|
||||||
@ -47,6 +47,12 @@ export const ChangeRequestStatusBadge: VFC<IChangeRequestStatusBadgeProps> = ({
|
|||||||
Rejected
|
Rejected
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
case 'Scheduled':
|
||||||
|
return (
|
||||||
|
<Badge color='warning' icon={<AccessTime fontSize={'small'} />}>
|
||||||
|
Scheduled
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <ReviewRequiredBadge />;
|
return <ReviewRequiredBadge />;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,12 @@ export interface IChangeRequest {
|
|||||||
rejections: IChangeRequestApproval[];
|
rejections: IChangeRequestApproval[];
|
||||||
comments: IChangeRequestComment[];
|
comments: IChangeRequestComment[];
|
||||||
conflict?: string;
|
conflict?: string;
|
||||||
|
schedule?: IChangeRequestSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChangeRequestSchedule {
|
||||||
|
scheduledAt: string;
|
||||||
|
status: 'pending' | 'failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IChangeRequestEnvironmentConfig {
|
export interface IChangeRequestEnvironmentConfig {
|
||||||
@ -67,6 +73,7 @@ export type ChangeRequestState =
|
|||||||
| 'Approved'
|
| 'Approved'
|
||||||
| 'In review'
|
| 'In review'
|
||||||
| 'Applied'
|
| 'Applied'
|
||||||
|
| 'Scheduled'
|
||||||
| 'Cancelled'
|
| 'Cancelled'
|
||||||
| 'Rejected';
|
| 'Rejected';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user