Skip to content

Commit

Permalink
chore: add schedule option to approved change requests (#5252)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
thomasheartman committed Nov 3, 2023
1 parent 95245c4 commit 9fbb61a
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 129 deletions.
@@ -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>
);
Expand Up @@ -24,6 +24,9 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { changesCount } from '../changesCount';
import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers';
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%',
Expand Down Expand Up @@ -87,6 +90,8 @@ export const ChangeRequestOverview: FC = () => {
return null;
}

const scheduleChangeRequests = useUiFlag('scheduledConfigurationChanges');

const allowChangeRequestActions = isChangeRequestConfiguredForReview(
changeRequest.environment,
);
Expand Down Expand Up @@ -267,21 +272,44 @@ export const ChangeRequestOverview: FC = () => {
<ConditionallyRender
condition={changeRequest.state === 'Approved'}
show={
<PermissionButton
variant='contained'
onClick={onApplyChanges}
projectId={projectId}
permission={APPLY_CHANGE_REQUEST}
environmentId={
changeRequest.environment
<ConditionallyRender
condition={scheduleChangeRequests}
show={
<ApplyButton
onApply={onApplyChanges}
disabled={
!allowChangeRequestActions ||
loading
}
onSchedule={() => {
console.log(
'I would schedule changes now',
);
}}
>
Apply or schedule changes
</ApplyButton>
}
disabled={
!allowChangeRequestActions ||
loading
elseShow={
<PermissionButton
variant='contained'
onClick={onApplyChanges}
projectId={projectId}
permission={
APPLY_CHANGE_REQUEST
}
environmentId={
changeRequest.environment
}
disabled={
!allowChangeRequestActions ||
loading
}
>
Apply changes
</PermissionButton>
}
>
Apply changes
</PermissionButton>
/>
}
/>
<ConditionallyRender
Expand Down
@@ -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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
import { FC } from 'react';

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 Clear from '@mui/icons-material/Clear';
import { MultiActionButton } from '../MultiActionButton/MultiActionButton';
import { APPROVE_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';

export const ReviewButton: FC<{
disabled: boolean;
onReject: () => void;
onApprove: () => void;
}> = ({ disabled, onReject, onApprove, children }) => {
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={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>
);
};
}> = ({ disabled, onReject, onApprove, children }) => (
<MultiActionButton
permission={APPROVE_CHANGE_REQUEST}
disabled={disabled}
actions={[
{
label: 'Approve',
onSelect: onApprove,
icon: <CheckBox fontSize='small' />,
},
{
label: 'Reject',
onSelect: onReject,
icon: <Clear fontSize='small' />,
},
]}
>
{children}
</MultiActionButton>
);

1 comment on commit 9fbb61a

@vercel
Copy link

@vercel vercel bot commented on 9fbb61a Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.