1
0
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:


![image](https://github.com/Unleash/unleash/assets/17786332/9a9bee77-4925-4054-9ef6-ef8ddbb61fae)
This commit is contained in:
Thomas Heartman 2023-11-03 08:46:06 +01:00 committed by GitHub
parent 95245c4413
commit 9fbb61a1c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 218 additions and 129 deletions

View File

@ -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>
);

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>
);
};