mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
chore: add schedule option to approved change requests (#5252)
The button doesn't do anything at the moment, but it's there visually. Because this uses the same button as the dual-function button for approve/reject, I extracted that component into a reusable "multi-action" button. I could have copied the code wholesale, but it's a complex component, so I thought this would be a better solution. I'll add the dialog in a follow-up PR. This one already has a lot of changes. Visual: 
This commit is contained in:
parent
95245c4413
commit
9fbb61a1c4
@ -0,0 +1,31 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import CheckBox from '@mui/icons-material/Check';
|
||||||
|
import Today from '@mui/icons-material/Today';
|
||||||
|
import { MultiActionButton } from '../MultiActionButton/MultiActionButton';
|
||||||
|
import { APPLY_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
|
||||||
|
|
||||||
|
export const ApplyButton: FC<{
|
||||||
|
disabled: boolean;
|
||||||
|
onSchedule: () => void;
|
||||||
|
onApply: () => void;
|
||||||
|
}> = ({ disabled, onSchedule, onApply, children }) => (
|
||||||
|
<MultiActionButton
|
||||||
|
permission={APPLY_CHANGE_REQUEST}
|
||||||
|
disabled={disabled}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Apply changes',
|
||||||
|
onSelect: onApply,
|
||||||
|
icon: <CheckBox fontSize='small' />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Schedule changes',
|
||||||
|
onSelect: onSchedule,
|
||||||
|
icon: <Today fontSize='small' />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MultiActionButton>
|
||||||
|
);
|
@ -24,6 +24,9 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
|||||||
import { changesCount } from '../changesCount';
|
import { changesCount } from '../changesCount';
|
||||||
import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers';
|
import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers';
|
||||||
import { ChangeRequestRejectDialogue } from './ChangeRequestRejectDialog/ChangeRequestRejectDialog';
|
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 }) => ({
|
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
||||||
width: '30%',
|
width: '30%',
|
||||||
@ -87,6 +90,8 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scheduleChangeRequests = useUiFlag('scheduledConfigurationChanges');
|
||||||
|
|
||||||
const allowChangeRequestActions = isChangeRequestConfiguredForReview(
|
const allowChangeRequestActions = isChangeRequestConfiguredForReview(
|
||||||
changeRequest.environment,
|
changeRequest.environment,
|
||||||
);
|
);
|
||||||
@ -267,21 +272,44 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={changeRequest.state === 'Approved'}
|
condition={changeRequest.state === 'Approved'}
|
||||||
show={
|
show={
|
||||||
<PermissionButton
|
<ConditionallyRender
|
||||||
variant='contained'
|
condition={scheduleChangeRequests}
|
||||||
onClick={onApplyChanges}
|
show={
|
||||||
projectId={projectId}
|
<ApplyButton
|
||||||
permission={APPLY_CHANGE_REQUEST}
|
onApply={onApplyChanges}
|
||||||
environmentId={
|
disabled={
|
||||||
changeRequest.environment
|
!allowChangeRequestActions ||
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
onSchedule={() => {
|
||||||
|
console.log(
|
||||||
|
'I would schedule changes now',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply or schedule changes
|
||||||
|
</ApplyButton>
|
||||||
}
|
}
|
||||||
disabled={
|
elseShow={
|
||||||
!allowChangeRequestActions ||
|
<PermissionButton
|
||||||
loading
|
variant='contained'
|
||||||
|
onClick={onApplyChanges}
|
||||||
|
projectId={projectId}
|
||||||
|
permission={
|
||||||
|
APPLY_CHANGE_REQUEST
|
||||||
|
}
|
||||||
|
environmentId={
|
||||||
|
changeRequest.environment
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!allowChangeRequestActions ||
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Apply changes
|
||||||
|
</PermissionButton>
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
Apply changes
|
|
||||||
</PermissionButton>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import React, { FC, useContext } from 'react';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClickAwayListener,
|
||||||
|
Grow,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Paper,
|
||||||
|
Popper,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
import AccessContext from 'contexts/AccessContext';
|
||||||
|
|
||||||
|
type Action = {
|
||||||
|
label: string;
|
||||||
|
onSelect: () => void;
|
||||||
|
icon: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultiActionButton: FC<{
|
||||||
|
disabled: boolean;
|
||||||
|
actions: Action[];
|
||||||
|
permission: string;
|
||||||
|
}> = ({ disabled, children, actions, permission }) => {
|
||||||
|
const { isAdmin } = useContext(AccessContext);
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const id = useRequiredPathParam('id');
|
||||||
|
const { user } = useAuthUser();
|
||||||
|
const { data } = useChangeRequest(projectId, id);
|
||||||
|
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const anchorRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const onToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = (event: Event) => {
|
||||||
|
if (anchorRef.current?.contains(event.target as HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
const popperWidth = anchorRef.current
|
||||||
|
? anchorRef.current.offsetWidth
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<PermissionButton
|
||||||
|
variant='contained'
|
||||||
|
disabled={
|
||||||
|
disabled || (data?.createdBy.id === user?.id && !isAdmin)
|
||||||
|
}
|
||||||
|
aria-controls={open ? 'review-options-menu' : undefined}
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
aria-label='review changes'
|
||||||
|
aria-haspopup='menu'
|
||||||
|
onClick={onToggle}
|
||||||
|
ref={anchorRef}
|
||||||
|
endIcon={<ArrowDropDownIcon />}
|
||||||
|
permission={permission}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentId={data?.environment}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PermissionButton>
|
||||||
|
<Popper
|
||||||
|
sx={{
|
||||||
|
zIndex: 1,
|
||||||
|
width: popperWidth,
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
role={undefined}
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
>
|
||||||
|
{({ TransitionProps, placement }) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === 'bottom'
|
||||||
|
? 'center top'
|
||||||
|
: 'center bottom',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper className='dropdown-outline'>
|
||||||
|
<ClickAwayListener onClickAway={onClose}>
|
||||||
|
<MenuList
|
||||||
|
id='review-options-menu'
|
||||||
|
autoFocusItem
|
||||||
|
>
|
||||||
|
{actions.map(
|
||||||
|
({ label, onSelect, icon }) => (
|
||||||
|
<MenuItem onClick={onSelect}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
{label}
|
||||||
|
</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
@ -1,124 +1,31 @@
|
|||||||
import React, { FC, useContext } from 'react';
|
import { FC } from 'react';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
||||||
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ClickAwayListener,
|
|
||||||
Grow,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
Paper,
|
|
||||||
Popper,
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
|
||||||
import { APPROVE_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
|
||||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
|
||||||
import AccessContext from 'contexts/AccessContext';
|
|
||||||
import CheckBox from '@mui/icons-material/Check';
|
import CheckBox from '@mui/icons-material/Check';
|
||||||
import Clear from '@mui/icons-material/Clear';
|
import Clear from '@mui/icons-material/Clear';
|
||||||
|
import { MultiActionButton } from '../MultiActionButton/MultiActionButton';
|
||||||
|
import { APPROVE_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
|
||||||
|
|
||||||
export const ReviewButton: FC<{
|
export const ReviewButton: FC<{
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onReject: () => void;
|
onReject: () => void;
|
||||||
onApprove: () => void;
|
onApprove: () => void;
|
||||||
}> = ({ disabled, onReject, onApprove, children }) => {
|
}> = ({ disabled, onReject, onApprove, children }) => (
|
||||||
const { isAdmin } = useContext(AccessContext);
|
<MultiActionButton
|
||||||
const projectId = useRequiredPathParam('projectId');
|
permission={APPROVE_CHANGE_REQUEST}
|
||||||
const id = useRequiredPathParam('id');
|
disabled={disabled}
|
||||||
const { user } = useAuthUser();
|
actions={[
|
||||||
const { data } = useChangeRequest(projectId, id);
|
{
|
||||||
|
label: 'Approve',
|
||||||
const [open, setOpen] = React.useState(false);
|
onSelect: onApprove,
|
||||||
const anchorRef = React.useRef<HTMLButtonElement>(null);
|
icon: <CheckBox fontSize='small' />,
|
||||||
|
},
|
||||||
const onToggle = () => {
|
{
|
||||||
setOpen((prevOpen) => !prevOpen);
|
label: 'Reject',
|
||||||
};
|
onSelect: onReject,
|
||||||
|
icon: <Clear fontSize='small' />,
|
||||||
const onClose = (event: Event) => {
|
},
|
||||||
if (anchorRef.current?.contains(event.target as HTMLElement)) {
|
]}
|
||||||
return;
|
>
|
||||||
}
|
{children}
|
||||||
|
</MultiActionButton>
|
||||||
setOpen(false);
|
);
|
||||||
};
|
|
||||||
const popperWidth = anchorRef.current
|
|
||||||
? anchorRef.current.offsetWidth
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<PermissionButton
|
|
||||||
variant='contained'
|
|
||||||
disabled={
|
|
||||||
disabled || (data?.createdBy.id === user?.id && !isAdmin)
|
|
||||||
}
|
|
||||||
aria-controls={open ? 'review-options-menu' : undefined}
|
|
||||||
aria-expanded={open ? 'true' : undefined}
|
|
||||||
aria-label='review changes'
|
|
||||||
aria-haspopup='menu'
|
|
||||||
onClick={onToggle}
|
|
||||||
ref={anchorRef}
|
|
||||||
endIcon={<ArrowDropDownIcon />}
|
|
||||||
permission={APPROVE_CHANGE_REQUEST}
|
|
||||||
projectId={projectId}
|
|
||||||
environmentId={data?.environment}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</PermissionButton>
|
|
||||||
<Popper
|
|
||||||
sx={{
|
|
||||||
zIndex: 1,
|
|
||||||
width: popperWidth,
|
|
||||||
}}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorRef.current}
|
|
||||||
role={undefined}
|
|
||||||
transition
|
|
||||||
disablePortal
|
|
||||||
>
|
|
||||||
{({ TransitionProps, placement }) => (
|
|
||||||
<Grow
|
|
||||||
{...TransitionProps}
|
|
||||||
style={{
|
|
||||||
transformOrigin:
|
|
||||||
placement === 'bottom'
|
|
||||||
? 'center top'
|
|
||||||
: 'center bottom',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper className='dropdown-outline'>
|
|
||||||
<ClickAwayListener onClickAway={onClose}>
|
|
||||||
<MenuList
|
|
||||||
id='review-options-menu'
|
|
||||||
autoFocusItem
|
|
||||||
>
|
|
||||||
<MenuItem onClick={onApprove}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<CheckBox fontSize='small' />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>
|
|
||||||
Approve changes
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={onReject}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Clear fontSize='small' />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>
|
|
||||||
Reject changes
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</ClickAwayListener>
|
|
||||||
</Paper>
|
|
||||||
</Grow>
|
|
||||||
)}
|
|
||||||
</Popper>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user