mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-12 13:48:35 +02:00
feat: schedule changes dialog (#5285)
Closes: # [1-1585](https://linear.app/unleash/issue/1-1585/reschedule-changes-dialog) [1-1582](https://linear.app/unleash/issue/1-1582/change-apply-changes-apply-or-schedule-changes) Manually tested e2e -> Approve -> Schedule -> Reschedule -> Apply/Reject: ui tests verifying it in next pr --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai> Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
This commit is contained in:
parent
b3054c9277
commit
addda5b022
@ -30,6 +30,7 @@ import {
|
|||||||
ChangeRequestApplyScheduledDialogue,
|
ChangeRequestApplyScheduledDialogue,
|
||||||
ChangeRequestRejectScheduledDialogue,
|
ChangeRequestRejectScheduledDialogue,
|
||||||
} from './ChangeRequestScheduledDialogs/changeRequestScheduledDialogs';
|
} from './ChangeRequestScheduledDialogs/changeRequestScheduledDialogs';
|
||||||
|
import { ScheduleChangeRequestDialog } from './ChangeRequestScheduledDialogs/ScheduleChangeRequestDialog';
|
||||||
|
|
||||||
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
||||||
width: '30%',
|
width: '30%',
|
||||||
@ -77,6 +78,8 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
||||||
const [showRejectDialog, setShowRejectDialog] = useState(false);
|
const [showRejectDialog, setShowRejectDialog] = useState(false);
|
||||||
|
const [showScheduleChangesDialog, setShowScheduleChangeDialog] =
|
||||||
|
useState(false);
|
||||||
const [showApplyScheduledDialog, setShowApplyScheduledDialog] =
|
const [showApplyScheduledDialog, setShowApplyScheduledDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [showRejectScheduledDialog, setShowRejectScheduledDialog] =
|
const [showRejectScheduledDialog, setShowRejectScheduledDialog] =
|
||||||
@ -111,6 +114,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
await changeState(projectId, Number(id), {
|
await changeState(projectId, Number(id), {
|
||||||
state: 'Applied',
|
state: 'Applied',
|
||||||
});
|
});
|
||||||
|
setShowApplyScheduledDialog(false);
|
||||||
refetchChangeRequest();
|
refetchChangeRequest();
|
||||||
refetchChangeRequestOpen();
|
refetchChangeRequestOpen();
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -123,6 +127,25 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onScheduleChangeRequest = async (scheduledDate: Date) => {
|
||||||
|
try {
|
||||||
|
await changeState(projectId, Number(id), {
|
||||||
|
state: 'Scheduled',
|
||||||
|
scheduledAt: scheduledDate.toISOString(),
|
||||||
|
});
|
||||||
|
setShowScheduleChangeDialog(false);
|
||||||
|
refetchChangeRequest();
|
||||||
|
refetchChangeRequestOpen();
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Changes scheduled',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onAddComment = async () => {
|
const onAddComment = async () => {
|
||||||
try {
|
try {
|
||||||
await addComment(projectId, id, commentText);
|
await addComment(projectId, id, commentText);
|
||||||
@ -196,6 +219,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
const onCancelAbort = () => setShowCancelDialog(false);
|
const onCancelAbort = () => setShowCancelDialog(false);
|
||||||
const onCancelReject = () => setShowRejectDialog(false);
|
const onCancelReject = () => setShowRejectDialog(false);
|
||||||
const onApplyScheduledAbort = () => setShowApplyScheduledDialog(false);
|
const onApplyScheduledAbort = () => setShowApplyScheduledDialog(false);
|
||||||
|
const onScheduleChangeAbort = () => setShowApplyScheduledDialog(false);
|
||||||
const onRejectScheduledAbort = () => setShowRejectScheduledDialog(false);
|
const onRejectScheduledAbort = () => setShowRejectScheduledDialog(false);
|
||||||
|
|
||||||
const isSelfReview =
|
const isSelfReview =
|
||||||
@ -293,11 +317,11 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
!allowChangeRequestActions ||
|
!allowChangeRequestActions ||
|
||||||
loading
|
loading
|
||||||
}
|
}
|
||||||
onSchedule={() => {
|
onSchedule={() =>
|
||||||
console.log(
|
setShowScheduleChangeDialog(
|
||||||
'I would schedule changes now',
|
true,
|
||||||
);
|
)
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
Apply or schedule changes
|
Apply or schedule changes
|
||||||
</ApplyButton>
|
</ApplyButton>
|
||||||
@ -339,11 +363,9 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
!allowChangeRequestActions ||
|
!allowChangeRequestActions ||
|
||||||
loading
|
loading
|
||||||
}
|
}
|
||||||
onSchedule={() => {
|
onSchedule={() =>
|
||||||
console.log(
|
setShowScheduleChangeDialog(true)
|
||||||
'I would schedule changes now',
|
}
|
||||||
);
|
|
||||||
}}
|
|
||||||
variant={'update'}
|
variant={'update'}
|
||||||
>
|
>
|
||||||
Apply or schedule changes
|
Apply or schedule changes
|
||||||
@ -421,6 +443,28 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
condition={scheduleChangeRequests}
|
condition={scheduleChangeRequests}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
|
<ScheduleChangeRequestDialog
|
||||||
|
open={showScheduleChangesDialog}
|
||||||
|
onConfirm={onScheduleChangeRequest}
|
||||||
|
onClose={onScheduleChangeAbort}
|
||||||
|
disabled={!allowChangeRequestActions || loading}
|
||||||
|
projectId={projectId}
|
||||||
|
environment={changeRequest.environment}
|
||||||
|
primaryButtonText={
|
||||||
|
changeRequest?.schedule?.scheduledAt
|
||||||
|
? 'Update scheduled time'
|
||||||
|
: 'Schedule changes'
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
changeRequest?.schedule?.scheduledAt
|
||||||
|
? 'Update schedule'
|
||||||
|
: 'Schedule changes'
|
||||||
|
}
|
||||||
|
scheduledAt={
|
||||||
|
changeRequest?.schedule?.scheduledAt ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ChangeRequestApplyScheduledDialogue
|
<ChangeRequestApplyScheduledDialogue
|
||||||
open={showApplyScheduledDialog}
|
open={showApplyScheduledDialog}
|
||||||
onConfirm={onApplyChanges}
|
onConfirm={onApplyChanges}
|
||||||
@ -434,7 +478,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
/>
|
/>
|
||||||
<ChangeRequestRejectScheduledDialogue
|
<ChangeRequestRejectScheduledDialogue
|
||||||
open={showRejectScheduledDialog}
|
open={showRejectScheduledDialog}
|
||||||
onConfirm={onCancelChanges}
|
onConfirm={onReject}
|
||||||
onClose={onRejectScheduledAbort}
|
onClose={onRejectScheduledAbort}
|
||||||
scheduledTime={
|
scheduledTime={
|
||||||
changeRequest?.schedule?.scheduledAt
|
changeRequest?.schedule?.scheduledAt
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { Alert, styled, Typography } from '@mui/material';
|
import { Alert, styled, Typography } from '@mui/material';
|
||||||
import { Dialogue } from '../../../common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
|
||||||
export interface ChangeRequestScheduleDialogueProps {
|
export interface ChangeRequestScheduledDialogProps {
|
||||||
title: string;
|
title: string;
|
||||||
primaryButtonText: string;
|
primaryButtonText: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -12,8 +12,6 @@ export interface ChangeRequestScheduleDialogueProps {
|
|||||||
message: string;
|
message: string;
|
||||||
permissionButton?: ReactElement;
|
permissionButton?: ReactElement;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
projectId?: string;
|
|
||||||
environment?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
@ -23,8 +21,8 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
|
|||||||
borderColor: `${theme.palette.neutral.light}!important`,
|
borderColor: `${theme.palette.neutral.light}!important`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ChangeRequestScheduledDialogue: FC<
|
export const ChangeRequestScheduledDialog: FC<
|
||||||
ChangeRequestScheduleDialogueProps
|
ChangeRequestScheduledDialogProps
|
||||||
> = ({
|
> = ({
|
||||||
open,
|
open,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
@ -33,6 +31,7 @@ export const ChangeRequestScheduledDialogue: FC<
|
|||||||
primaryButtonText,
|
primaryButtonText,
|
||||||
message,
|
message,
|
||||||
scheduledTime,
|
scheduledTime,
|
||||||
|
permissionButton,
|
||||||
}) => {
|
}) => {
|
||||||
if (!scheduledTime) return null;
|
if (!scheduledTime) return null;
|
||||||
|
|
||||||
@ -44,6 +43,7 @@ export const ChangeRequestScheduledDialogue: FC<
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onClick={() => onConfirm()}
|
onClick={() => onConfirm()}
|
||||||
|
permissionButton={permissionButton}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
<StyledAlert icon={false}>
|
<StyledAlert icon={false}>
|
@ -0,0 +1,107 @@
|
|||||||
|
import { FC, useState } from 'react';
|
||||||
|
import { Alert, Box, styled, Typography } from '@mui/material';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import { APPLY_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
import { DateTimePicker } from 'component/common/DateTimePicker/DateTimePicker';
|
||||||
|
import { getBrowserTimezoneInHumanReadableUTCOffset } from '../ChangeRequestReviewStatus/utils';
|
||||||
|
|
||||||
|
export interface ScheduleChangeRequestDialogProps {
|
||||||
|
title: string;
|
||||||
|
primaryButtonText: string;
|
||||||
|
open: boolean;
|
||||||
|
onConfirm: (selectedDate: Date) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
scheduledAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ScheduleChangeRequestDialog: FC<ScheduleChangeRequestDialogProps> =
|
||||||
|
({
|
||||||
|
open,
|
||||||
|
onConfirm,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
primaryButtonText,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
disabled,
|
||||||
|
scheduledAt,
|
||||||
|
}) => {
|
||||||
|
const [selectedDate, setSelectedDate] = useState(
|
||||||
|
scheduledAt ? new Date(scheduledAt) : new Date(),
|
||||||
|
);
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const timezone = getBrowserTimezoneInHumanReadableUTCOffset();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
title={title}
|
||||||
|
primaryButtonText={primaryButtonText}
|
||||||
|
secondaryButtonText='Cancel'
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
onClick={() => onConfirm(selectedDate)}
|
||||||
|
permissionButton={
|
||||||
|
<PermissionButton
|
||||||
|
variant='contained'
|
||||||
|
onClick={() => onConfirm(selectedDate)}
|
||||||
|
projectId={projectId}
|
||||||
|
permission={APPLY_CHANGE_REQUEST}
|
||||||
|
environmentId={environment}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{primaryButtonText}
|
||||||
|
</PermissionButton>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
severity={'info'}
|
||||||
|
sx={{ mb: (theme) => theme.spacing(2) }}
|
||||||
|
>
|
||||||
|
The time shown below is based on your browser's time zone.
|
||||||
|
</Alert>
|
||||||
|
<Typography
|
||||||
|
variant={'body1'}
|
||||||
|
sx={{ mb: (theme) => theme.spacing(4) }}
|
||||||
|
>
|
||||||
|
Select the date and time when these changes should be
|
||||||
|
applied. If you change your mind later, you can reschedule
|
||||||
|
the changes or apply the immediately.
|
||||||
|
</Typography>
|
||||||
|
<StyledContainer>
|
||||||
|
<DateTimePicker
|
||||||
|
label='Date'
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={(date) => {
|
||||||
|
setError(undefined);
|
||||||
|
if (date < new Date()) {
|
||||||
|
setError(
|
||||||
|
`The time you provided (${date.toLocaleString()}) is not valid because it's in the past. Please select a time in the future.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setSelectedDate(date);
|
||||||
|
}}
|
||||||
|
min={new Date()}
|
||||||
|
error={Boolean(error)}
|
||||||
|
errorText={error}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Typography variant={'body2'}>
|
||||||
|
Your browser's time zone is {timezone}
|
||||||
|
</Typography>
|
||||||
|
</StyledContainer>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
@ -1,19 +1,16 @@
|
|||||||
import { FC, useState } from 'react';
|
import { FC } from 'react';
|
||||||
import { TextField, Box, Alert, styled, Typography } from '@mui/material';
|
|
||||||
import { Dialogue } from '../../../common/Dialogue/Dialogue';
|
|
||||||
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { APPLY_CHANGE_REQUEST } from '../../../providers/AccessProvider/permissions';
|
import { APPLY_CHANGE_REQUEST } from '../../../providers/AccessProvider/permissions';
|
||||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||||
import {
|
import {
|
||||||
ChangeRequestScheduledDialogue,
|
ChangeRequestScheduledDialog,
|
||||||
ChangeRequestScheduleDialogueProps,
|
ChangeRequestScheduledDialogProps,
|
||||||
} from './ChangeRequestScheduledDialogue';
|
} from './ChangeRequestScheduledDialog';
|
||||||
|
|
||||||
export const ChangeRequestApplyScheduledDialogue: FC<
|
export const ChangeRequestApplyScheduledDialogue: FC<
|
||||||
Omit<
|
Omit<
|
||||||
ChangeRequestScheduleDialogueProps,
|
ChangeRequestScheduledDialogProps,
|
||||||
'message' | 'title' | 'primaryButtonText' | 'permissionButton'
|
'message' | 'title' | 'primaryButtonText' | 'permissionButton'
|
||||||
>
|
> & { projectId: string; environment: string }
|
||||||
> = ({ projectId, environment, disabled, onConfirm, ...rest }) => {
|
> = ({ projectId, environment, disabled, onConfirm, ...rest }) => {
|
||||||
const message =
|
const message =
|
||||||
'Applying the changes now means the scheduled time will be ignored';
|
'Applying the changes now means the scheduled time will be ignored';
|
||||||
@ -21,7 +18,7 @@ export const ChangeRequestApplyScheduledDialogue: FC<
|
|||||||
const primaryButtonText = 'Apply changes now';
|
const primaryButtonText = 'Apply changes now';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangeRequestScheduledDialogue
|
<ChangeRequestScheduledDialog
|
||||||
message={message}
|
message={message}
|
||||||
title={title}
|
title={title}
|
||||||
primaryButtonText={primaryButtonText}
|
primaryButtonText={primaryButtonText}
|
||||||
@ -45,7 +42,7 @@ export const ChangeRequestApplyScheduledDialogue: FC<
|
|||||||
|
|
||||||
export const ChangeRequestRejectScheduledDialogue: FC<
|
export const ChangeRequestRejectScheduledDialogue: FC<
|
||||||
Omit<
|
Omit<
|
||||||
ChangeRequestScheduleDialogueProps,
|
ChangeRequestScheduledDialogProps,
|
||||||
'message' | 'title' | 'primaryButtonText'
|
'message' | 'title' | 'primaryButtonText'
|
||||||
>
|
>
|
||||||
> = ({ ...rest }) => {
|
> = ({ ...rest }) => {
|
||||||
@ -55,7 +52,7 @@ export const ChangeRequestRejectScheduledDialogue: FC<
|
|||||||
const primaryButtonText = 'Reject changes';
|
const primaryButtonText = 'Reject changes';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangeRequestScheduledDialogue
|
<ChangeRequestScheduledDialog
|
||||||
message={message}
|
message={message}
|
||||||
title={title}
|
title={title}
|
||||||
primaryButtonText={primaryButtonText}
|
primaryButtonText={primaryButtonText}
|
||||||
|
@ -58,10 +58,12 @@ export const useChangeRequestApi = () => {
|
|||||||
state:
|
state:
|
||||||
| 'Approved'
|
| 'Approved'
|
||||||
| 'Applied'
|
| 'Applied'
|
||||||
|
| 'Scheduled'
|
||||||
| 'Cancelled'
|
| 'Cancelled'
|
||||||
| 'In review'
|
| 'In review'
|
||||||
| 'Rejected';
|
| 'Rejected';
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
scheduledAt?: string;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
trackEvent('change_request', {
|
trackEvent('change_request', {
|
||||||
|
Loading…
Reference in New Issue
Block a user