1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

Rename suggest changes to change request (#2311)

* Rename change request

* Merge with review status

* Move events and permissions
This commit is contained in:
sjaanus 2022-11-02 07:34:14 +01:00 committed by GitHub
parent da102a3e98
commit 5dd8616c74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 328 additions and 414 deletions

View File

@ -1,45 +1,36 @@
import { useCallback, VFC } from 'react'; import { VFC } from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { SuggestedFeatureToggleChange } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/SuggestedFeatureToggleChange'; import { ChangeRequestFeatureToggleChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ChangeRequestFeatureToggleChange';
import { objectId } from 'utils/objectId'; import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ToggleStatusChange } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/ToggleStatusChange'; import { ToggleStatusChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ToggleStatusChange';
// import { import type { IChangeRequestResponse } from 'hooks/api/getters/useChangeRequestDraft/useChangeRequestDraft';
// StrategyAddedChange, import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
// StrategyDeletedChange,
// StrategyEditedChange,
// } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/StrategyChange';
// import {
// formatStrategyName,
// GetFeatureStrategyIcon,
// } from 'utils/strategyNames';
import type { ISuggestChangesResponse } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
import { useSuggestChangeApi } from 'hooks/api/actions/useSuggestChangeApi/useSuggestChangeApi';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
interface ISuggestedChangeset { interface IChangeRequest {
suggestedChange: ISuggestChangesResponse; changeRequest: IChangeRequestResponse;
onRefetch?: () => void; onRefetch?: () => void;
onNavigate?: () => void; onNavigate?: () => void;
} }
export const SuggestedChangeset: VFC<ISuggestedChangeset> = ({ export const ChangeRequest: VFC<IChangeRequest> = ({
suggestedChange, changeRequest,
onRefetch, onRefetch,
onNavigate, onNavigate,
}) => { }) => {
const { discardSuggestions } = useSuggestChangeApi(); const { discardChangeRequestEvent } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const onDiscard = (id: number) => async () => { const onDiscard = (id: number) => async () => {
try { try {
await discardSuggestions( await discardChangeRequestEvent(
suggestedChange.project, changeRequest.project,
suggestedChange.id, changeRequest.id,
id id
); );
setToastData({ setToastData({
title: 'Change discarded from suggestion draft.', title: 'Change discarded from change request draft.',
type: 'success', type: 'success',
}); });
onRefetch?.(); onRefetch?.();
@ -51,11 +42,11 @@ export const SuggestedChangeset: VFC<ISuggestedChangeset> = ({
return ( return (
<Box> <Box>
Changes Changes
{suggestedChange.features?.map(featureToggleChange => ( {changeRequest.features?.map(featureToggleChange => (
<SuggestedFeatureToggleChange <ChangeRequestFeatureToggleChange
key={featureToggleChange.name} key={featureToggleChange.name}
featureName={featureToggleChange.name} featureName={featureToggleChange.name}
projectId={suggestedChange.project} projectId={changeRequest.project}
onNavigate={onNavigate} onNavigate={onNavigate}
> >
{featureToggleChange.changes.map(change => ( {featureToggleChange.changes.map(change => (
@ -92,7 +83,7 @@ export const SuggestedChangeset: VFC<ISuggestedChangeset> = ({
/> */} /> */}
</Box> </Box>
))} ))}
</SuggestedFeatureToggleChange> </ChangeRequestFeatureToggleChange>
))} ))}
</Box> </Box>
); );

View File

@ -2,7 +2,7 @@ import { FC } from 'react';
import { Alert, Typography } from '@mui/material'; import { Alert, Typography } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
interface ISuggestChangesDialogueProps { interface IChangeRequestDialogueProps {
isOpen: boolean; isOpen: boolean;
onConfirm: () => void; onConfirm: () => void;
onClose: () => void; onClose: () => void;
@ -11,7 +11,7 @@ interface ISuggestChangesDialogueProps {
enabled?: boolean; enabled?: boolean;
} }
export const SuggestChangesDialogue: FC<ISuggestChangesDialogueProps> = ({ export const ChangeRequestDialogue: FC<IChangeRequestDialogueProps> = ({
isOpen, isOpen,
onConfirm, onConfirm,
onClose, onClose,
@ -25,15 +25,15 @@ export const SuggestChangesDialogue: FC<ISuggestChangesDialogueProps> = ({
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
onClick={onConfirm} onClick={onConfirm}
onClose={onClose} onClose={onClose}
title="Suggest changes" title="Request changes"
> >
<Alert severity="info" sx={{ mb: 2 }}> <Alert severity="info" sx={{ mb: 2 }}>
Suggest changes is enabled for {environment}. Your changes needs to Change requests is enabled for {environment}. Your changes needs to
be approved before they will be live. All the changes you do now be approved before they will be live. All the changes you do now
will be added into a draft that you can submit for review. will be added into a draft that you can submit for review.
</Alert> </Alert>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Suggested changes: Change requests:
</Typography> </Typography>
<Typography> <Typography>
<strong>{enabled ? 'Disable' : 'Enable'}</strong> feature toggle{' '} <strong>{enabled ? 'Disable' : 'Enable'}</strong> feature toggle{' '}

View File

@ -3,14 +3,14 @@ import { Link } from 'react-router-dom';
import { Box, Card, Typography } from '@mui/material'; import { Box, Card, Typography } from '@mui/material';
import ToggleOnIcon from '@mui/icons-material/ToggleOn'; import ToggleOnIcon from '@mui/icons-material/ToggleOn';
interface ISuggestedFeatureToggleChange { interface IChangeRequestToggleChange {
featureName: string; featureName: string;
projectId: string; projectId: string;
onNavigate?: () => void; onNavigate?: () => void;
} }
export const SuggestedFeatureToggleChange: FC< export const ChangeRequestFeatureToggleChange: FC<
ISuggestedFeatureToggleChange IChangeRequestToggleChange
> = ({ featureName, projectId, onNavigate, children }) => { > = ({ featureName, projectId, onNavigate, children }) => {
return ( return (
<Card <Card

View File

@ -4,8 +4,8 @@ import { PlaygroundResultChip } from 'component/playground/Playground/Playground
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg'; import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({ export const ChangeRequestHeader: FC<{ changeRequest: any }> = ({
suggestedChange, changeRequest,
}) => { }) => {
return ( return (
<Paper <Paper
@ -30,9 +30,9 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
}} }}
variant="h1" variant="h1"
> >
Suggestion Change request
<Typography variant="h1" component="p"> <Typography variant="h1" component="p">
#{suggestedChange.id} #{changeRequest.id}
</Typography> </Typography>
</Typography> </Typography>
<PlaygroundResultChip <PlaygroundResultChip
@ -43,10 +43,10 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
</Box> </Box>
<Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}> <Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}>
<Typography sx={{ margin: 'auto 0' }}> <Typography sx={{ margin: 'auto 0' }}>
Created{' '} Created <TimeAgo date={new Date(changeRequest.createdAt)} />{' '}
<TimeAgo date={new Date(suggestedChange.createdAt)} /> by by
</Typography> </Typography>
<Avatar src={suggestedChange?.createdBy?.avatar} /> <Avatar src={changeRequest?.createdBy?.avatar} />
<Card <Card
variant="outlined" variant="outlined"
sx={theme => ({ sx={theme => ({
@ -56,11 +56,11 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
> >
Environment:{' '} Environment:{' '}
<Typography display="inline" fontWeight="bold"> <Typography display="inline" fontWeight="bold">
{suggestedChange?.environment} {changeRequest?.environment}
</Typography>{' '} </Typography>{' '}
| Updates:{' '} | Updates:{' '}
<Typography display="inline" fontWeight="bold"> <Typography display="inline" fontWeight="bold">
{suggestedChange?.features.length} feature toggles {changeRequest?.features.length} feature toggles
</Typography> </Typography>
</Card> </Card>
</Box> </Box>

View File

@ -1,24 +1,24 @@
import { FC } from 'react'; import { FC } from 'react';
import { Box, Button, Paper } from '@mui/material'; import { Box, Button, Paper } from '@mui/material';
import { useSuggestedChange } from 'hooks/api/getters/useSuggestChange/useSuggestedChange'; import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
import { SuggestedChangeHeader } from './SuggestedChangeHeader/SuggestedChangeHeader'; import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
import { SuggestedChangeTimeline } from './SuggestedChangeTimeline/SuggestedChangeTimeline'; import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline';
import { SuggestedChangeReviewers } from './SuggestedChangeReviewers/SuggestedChangeReviewers'; import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers';
import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset'; import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useSuggestChangeApi } from 'hooks/api/actions/useSuggestChangeApi/useSuggestChangeApi'; import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { SuggestedChangeReviewStatus } from './SuggestedChangeReviewStatus/SuggestedChangeReviewStatus'; import { ChangeRequestReviewStatus } from './ChangeRequestReviewStatus/ChangeRequestReviewStatus';
export const SuggestedChangeOverview: FC = () => { export const ChangeRequestOverview: FC = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const id = useRequiredPathParam('id'); const id = useRequiredPathParam('id');
const { data: suggestedChange } = useSuggestedChange(projectId, id); const { data: changeRequest } = useChangeRequest(projectId, id);
const { applyChanges } = useSuggestChangeApi(); const { applyChanges } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
if (!suggestedChange) { if (!changeRequest) {
return null; return null;
} }
@ -37,7 +37,7 @@ export const SuggestedChangeOverview: FC = () => {
return ( return (
<> <>
<SuggestedChangeHeader suggestedChange={suggestedChange} /> <ChangeRequestHeader changeRequest={changeRequest} />
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Box <Box
sx={{ sx={{
@ -46,8 +46,8 @@ export const SuggestedChangeOverview: FC = () => {
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
<SuggestedChangeTimeline /> <ChangeRequestTimeline />
<SuggestedChangeReviewers /> <ChangeRequestReviewers />
</Box> </Box>
<Paper <Paper
elevation={0} elevation={0}
@ -65,8 +65,8 @@ export const SuggestedChangeOverview: FC = () => {
padding: theme.spacing(2), padding: theme.spacing(2),
})} })}
> >
<SuggestedChangeset suggestedChange={suggestedChange} /> <ChangeRequest changeRequest={changeRequest} />
<SuggestedChangeReviewStatus approved={true} /> <ChangeRequestReviewStatus approved={true} />
<Button <Button
variant="contained" variant="contained"
sx={{ marginTop: 2 }} sx={{ marginTop: 2 }}

View File

@ -11,13 +11,13 @@ import {
StyledErrorIcon, StyledErrorIcon,
StyledReviewTitle, StyledReviewTitle,
StyledDivider, StyledDivider,
} from './SuggestChangeReviewStatus.styles'; } from './ChangeRequestReviewStatus.styles';
interface ISuggestChangeReviewsStatusProps { interface ISuggestChangeReviewsStatusProps {
approved: boolean; approved: boolean;
} }
export const SuggestedChangeReviewStatus: FC< export const ChangeRequestReviewStatus: FC<
ISuggestChangeReviewsStatusProps ISuggestChangeReviewsStatusProps
> = ({ approved }) => { > = ({ approved }) => {
return ( return (

View File

@ -1,6 +1,6 @@
import { Box, Paper } from '@mui/material'; import { Box, Paper } from '@mui/material';
export const SuggestedChangeReviewers = () => { export const ChangeRequestReviewers = () => {
return ( return (
<Paper <Paper
elevation={0} elevation={0}

View File

@ -7,7 +7,7 @@ import TimelineDot from '@mui/lab/TimelineDot';
import TimelineConnector from '@mui/lab/TimelineConnector'; import TimelineConnector from '@mui/lab/TimelineConnector';
import TimelineContent from '@mui/lab/TimelineContent'; import TimelineContent from '@mui/lab/TimelineContent';
export const SuggestedChangeTimeline: FC = () => { export const ChangeRequestTimeline: FC = () => {
return ( return (
<Paper <Paper
elevation={0} elevation={0}

View File

@ -5,11 +5,11 @@ import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { HelpOutline } from '@mui/icons-material'; import { HelpOutline } from '@mui/icons-material';
import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset'; import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft'; import { useChangeRequestDraft } from 'hooks/api/getters/useChangeRequestDraft/useChangeRequestDraft';
import { useSuggestChangeApi } from 'hooks/api/actions/useSuggestChangeApi/useSuggestChangeApi'; import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
interface ISuggestedChangesSidebarProps { interface IChangeRequestSidebarProps {
open: boolean; open: boolean;
project: string; project: string;
onClose: () => void; onClose: () => void;
@ -41,7 +41,7 @@ const StyledHeaderHint = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({ export const ChangeRequestSidebar: VFC<IChangeRequestSidebarProps> = ({
open, open,
project, project,
onClose, onClose,
@ -49,14 +49,14 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
const { const {
draft, draft,
loading, loading,
refetch: refetchSuggestedChanges, refetch: refetchChangeRequest,
} = useSuggestedChangesDraft(project); } = useChangeRequestDraft(project);
const { changeState } = useSuggestChangeApi(); const { changeState } = useChangeRequestApi();
const onReview = async (draftId: number) => { const onReview = async (draftId: number) => {
try { try {
await changeState(project, draftId, { state: 'In review' }); await changeState(project, draftId, { state: 'In review' });
refetchSuggestedChanges(); refetchChangeRequest();
} catch (e) { } catch (e) {
console.log('something went wrong'); console.log('something went wrong');
} }
@ -108,16 +108,16 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
</Tooltip> </Tooltip>
<StyledHeaderHint> <StyledHeaderHint>
Make sure you are sending the right changes Make sure you are sending the right changes
suggestions to be reviewed to be reviewed
</StyledHeaderHint> </StyledHeaderHint>
</> </>
} }
></PageHeader> ></PageHeader>
} }
> >
{draft?.map(environmentChangeset => ( {draft?.map(environmentChangeRequest => (
<Box <Box
key={environmentChangeset.id} key={environmentChangeRequest.id}
sx={{ sx={{
padding: 2, padding: 2,
border: '2px solid', border: '2px solid',
@ -127,35 +127,37 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
}} }}
> >
<Typography> <Typography>
env: {environmentChangeset?.environment} env: {environmentChangeRequest?.environment}
</Typography> </Typography>
<Typography> <Typography>
state: {environmentChangeset?.state} state: {environmentChangeRequest?.state}
</Typography> </Typography>
<hr /> <hr />
<SuggestedChangeset <ChangeRequest
suggestedChange={environmentChangeset} changeRequest={environmentChangeRequest}
onNavigate={() => { onNavigate={() => {
onClose(); onClose();
}} }}
onRefetch={refetchSuggestedChanges} onRefetch={refetchChangeRequest}
/> />
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<ConditionallyRender <ConditionallyRender
condition={ condition={
environmentChangeset?.state === 'APPROVED' environmentChangeRequest?.state ===
'APPROVED'
} }
show={<Typography>Applied</Typography>} show={<Typography>Applied</Typography>}
/> />
<ConditionallyRender <ConditionallyRender
condition={ condition={
environmentChangeset?.state === 'CLOSED' environmentChangeRequest?.state === 'CLOSED'
} }
show={<Typography>Applied</Typography>} show={<Typography>Applied</Typography>}
/> />
<ConditionallyRender <ConditionallyRender
condition={ condition={
environmentChangeset?.state === 'APPROVED' environmentChangeRequest?.state ===
'APPROVED'
} }
show={ show={
<> <>
@ -171,7 +173,7 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
/> />
<ConditionallyRender <ConditionallyRender
condition={ condition={
environmentChangeset?.state === 'Draft' environmentChangeRequest?.state === 'Draft'
} }
show={ show={
<> <>
@ -180,7 +182,7 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
variant="contained" variant="contained"
onClick={() => onClick={() =>
onReview( onReview(
environmentChangeset.id environmentChangeRequest.id
) )
} }
> >

View File

@ -3,8 +3,8 @@ import { Box, Button, Typography } from '@mui/material';
import { useStyles as useAppStyles } from 'component/App.styles'; import { useStyles as useAppStyles } from 'component/App.styles';
import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SuggestedChangesSidebar } from '../SuggestedChangesSidebar/SuggestedChangesSidebar'; import { ChangeRequestSidebar } from '../ChangeRequestSidebar/ChangeRequestSidebar';
import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft'; import { useChangeRequestDraft } from 'hooks/api/getters/useChangeRequestDraft/useChangeRequestDraft';
interface IDraftBannerProps { interface IDraftBannerProps {
project: string; project: string;
@ -13,7 +13,7 @@ interface IDraftBannerProps {
export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => { export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => {
const { classes } = useAppStyles(); const { classes } = useAppStyles();
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const { draft, loading } = useSuggestedChangesDraft(project); const { draft, loading } = useChangeRequestDraft(project);
const environment = ''; const environment = '';
if ((!loading && !draft) || draft?.length === 0) { if ((!loading && !draft) || draft?.length === 0) {
@ -69,7 +69,7 @@ export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => {
</Button> </Button>
</Box> </Box>
</Box> </Box>
<SuggestedChangesSidebar <ChangeRequestSidebar
project={project} project={project}
open={isSidebarOpen} open={isSidebarOpen}
onClose={() => { onClose={() => {

View File

@ -2,7 +2,7 @@ import { ArrowRight } from '@mui/icons-material';
import { useTheme } from '@mui/system'; import { useTheme } from '@mui/system';
import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell'; import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell';
export const ChangesetActionCell = () => { export const ChangeRequestActionCell = () => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<TextCell sx={{ textAlign: 'right' }}> <TextCell sx={{ textAlign: 'right' }}>

View File

@ -4,11 +4,11 @@ import { colors } from 'themes/colors';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Check, CircleOutlined, Close } from '@mui/icons-material'; import { Check, CircleOutlined, Close } from '@mui/icons-material';
interface IChangesetStatusCellProps { interface IChangeRequestStatusCellProps {
value?: string | null; value?: string | null;
} }
export enum SuggestChangesetState { export enum ChangeRequestState {
DRAFT = 'Draft', DRAFT = 'Draft',
APPROVED = 'Approved', APPROVED = 'Approved',
IN_REVIEW = 'In review', IN_REVIEW = 'In review',
@ -62,40 +62,40 @@ export const StyledReviewChip = styled(StyledChip)(({ theme }) => ({
}, },
})); }));
export const ChangesetStatusCell: VFC<IChangesetStatusCellProps> = ({ export const ChangeRequestStatusCell: VFC<IChangeRequestStatusCellProps> = ({
value, value,
}) => { }) => {
const renderState = (state: string) => { const renderState = (state: string) => {
switch (state) { switch (state) {
case SuggestChangesetState.IN_REVIEW: case ChangeRequestState.IN_REVIEW:
return ( return (
<StyledReviewChip <StyledReviewChip
label={'Review required'} label={'Review required'}
icon={<CircleOutlined fontSize={'small'} />} icon={<CircleOutlined fontSize={'small'} />}
/> />
); );
case SuggestChangesetState.APPROVED: case ChangeRequestState.APPROVED:
return ( return (
<StyledApprovedChip <StyledApprovedChip
label={'Approved'} label={'Approved'}
icon={<Check fontSize={'small'} />} icon={<Check fontSize={'small'} />}
/> />
); );
case SuggestChangesetState.APPLIED: case ChangeRequestState.APPLIED:
return ( return (
<StyledApprovedChip <StyledApprovedChip
label={'Applied'} label={'Applied'}
icon={<Check fontSize={'small'} />} icon={<Check fontSize={'small'} />}
/> />
); );
case SuggestChangesetState.CANCELLED: case ChangeRequestState.CANCELLED:
return ( return (
<StyledRejectedChip <StyledRejectedChip
label={'Cancelled'} label={'Cancelled'}
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />} icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
/> />
); );
case SuggestChangesetState.REJECTED: case ChangeRequestState.REJECTED:
return ( return (
<StyledRejectedChip <StyledRejectedChip
label={'Rejected'} label={'Rejected'}

View File

@ -4,7 +4,7 @@ import { Link as RouterLink } from 'react-router-dom';
import { useTheme } from '@mui/system'; import { useTheme } from '@mui/system';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
interface IChangesetTitleCellProps { interface IChangeRequestTitleCellProps {
value?: any; value?: any;
row: { original: any }; row: { original: any };
} }
@ -15,14 +15,14 @@ export const StyledLink = styled('div')(({ theme }) => ({
margin: 0, margin: 0,
})); }));
export const ChangesetTitleCell = ({ export const ChangeRequestTitleCell = ({
value, value,
row: { original }, row: { original },
}: IChangesetTitleCellProps) => { }: IChangeRequestTitleCellProps) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { id, features: changes } = original; const { id, features: changes } = original;
const theme = useTheme(); const theme = useTheme();
const path = `/projects/${projectId}/suggest-changes/${id}`; const path = `/projects/${projectId}/change-requests/${id}`;
if (!value) { if (!value) {
return <TextCell />; return <TextCell />;
@ -37,7 +37,7 @@ export const ChangesetTitleCell = ({
to={path} to={path}
sx={{ pt: 0.2 }} sx={{ pt: 0.2 }}
> >
Suggestion Change request
</Link> </Link>
<Typography <Typography
component={'span'} component={'span'}

View File

@ -19,15 +19,15 @@ import { useSearch } from 'hooks/useSearch';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { TimeAgoCell } from '../../../common/Table/cells/TimeAgoCell/TimeAgoCell'; import { TimeAgoCell } from '../../../common/Table/cells/TimeAgoCell/TimeAgoCell';
import { TextCell } from '../../../common/Table/cells/TextCell/TextCell'; import { TextCell } from '../../../common/Table/cells/TextCell/TextCell';
import { ChangesetStatusCell } from './ChangesetStatusCell/ChangesetStatusCell'; import { ChangeRequestStatusCell } from './ChangeRequestStatusCell/ChangeRequestStatusCell';
import { ChangesetActionCell } from './ChangesetActionCell/ChangesetActionCell'; import { ChangeRequestActionCell } from './ChangeRequestActionCell/ChangeRequestActionCell';
import { AvatarCell } from './AvatarCell/AvatarCell'; import { AvatarCell } from './AvatarCell/AvatarCell';
import { ChangesetTitleCell } from './ChangesetTitleCell/ChangesetTitleCell'; import { ChangeRequestTitleCell } from './ChangeRequestTitleCell/ChangeRequestTitleCell';
import { TableBody, TableRow } from '../../../common/Table'; import { TableBody, TableRow } from '../../../common/Table';
import { useStyles } from './SuggestionsTabs.styles'; import { useStyles } from './ChangeRequestsTabs.styles';
export interface IChangeSetTableProps { export interface IChangeRequestTableProps {
changesets: any[]; changeRequests: any[];
loading: boolean; loading: boolean;
storedParams: SortingRule<string>; storedParams: SortingRule<string>;
setStoredParams: ( setStoredParams: (
@ -38,13 +38,13 @@ export interface IChangeSetTableProps {
projectId: string; projectId: string;
} }
export const SuggestionsTabs = ({ export const ChangeRequestsTabs = ({
changesets = [], changeRequests = [],
loading, loading,
storedParams, storedParams,
setStoredParams, setStoredParams,
projectId, projectId,
}: IChangeSetTableProps) => { }: IChangeRequestTableProps) => {
const { classes } = useStyles(); const { classes } = useStyles();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -53,27 +53,29 @@ export const SuggestionsTabs = ({
searchParams.get('search') || '' searchParams.get('search') || ''
); );
const [openChangesets, closedChangesets] = useMemo(() => { const [openChangeRequests, closedChangeRequests] = useMemo(() => {
const open = changesets.filter( const open = changeRequests.filter(
changeset => changeRequest =>
changeset.state !== 'Cancelled' && changeset.state !== 'Applied' changeRequest.state !== 'Cancelled' &&
changeRequest.state !== 'Applied'
); );
const closed = changesets.filter( const closed = changeRequests.filter(
changeset => changeRequest =>
changeset.state === 'Cancelled' || changeset.state === 'Applied' changeRequest.state === 'Cancelled' ||
changeRequest.state === 'Applied'
); );
return [open, closed]; return [open, closed];
}, [changesets]); }, [changeRequests]);
const tabs = [ const tabs = [
{ {
title: 'Suggestions', title: 'Change requests',
data: openChangesets, data: openChangeRequests,
}, },
{ {
title: 'Closed', title: 'Closed',
data: closedChangesets, data: closedChangeRequests,
}, },
]; ];
@ -87,7 +89,7 @@ export const SuggestionsTabs = ({
width: 100, width: 100,
canSort: true, canSort: true,
accessor: 'id', accessor: 'id',
Cell: ChangesetTitleCell, Cell: ChangeRequestTitleCell,
}, },
{ {
Header: 'By', Header: 'By',
@ -117,7 +119,7 @@ export const SuggestionsTabs = ({
accessor: 'state', accessor: 'state',
minWidth: 150, minWidth: 150,
width: 150, width: 150,
Cell: ChangesetStatusCell, Cell: ChangeRequestStatusCell,
sortType: 'text', sortType: 'text',
}, },
{ {
@ -126,7 +128,7 @@ export const SuggestionsTabs = ({
minWidth: 50, minWidth: 50,
width: 50, width: 50,
canSort: false, canSort: false,
Cell: ChangesetActionCell, Cell: ChangeRequestActionCell,
}, },
], ],
//eslint-disable-next-line //eslint-disable-next-line

View File

@ -2,28 +2,28 @@ import { usePageTitle } from 'hooks/usePageTitle';
import { createLocalStorage } from 'utils/createLocalStorage'; import { createLocalStorage } from 'utils/createLocalStorage';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import { SuggestionsTabs } from './SuggestionsTabs/SuggestionsTabs'; import { ChangeRequestsTabs } from './ChangeRequestsTabs/ChangeRequestsTabs';
import { SortingRule } from 'react-table'; import { SortingRule } from 'react-table';
import { useProjectSuggestedChanges } from 'hooks/api/getters/useProjectSuggestedChanges/useProjectSuggestedChanges'; import { useProjectChangeRequests } from 'hooks/api/getters/useProjectChangeRequests/useProjectChangeRequests';
const defaultSort: SortingRule<string> = { id: 'updatedAt', desc: true }; const defaultSort: SortingRule<string> = { id: 'updatedAt', desc: true };
export const ProjectSuggestedChanges = () => { export const ProjectChangeRequests = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const projectName = useProjectNameOrId(projectId); const projectName = useProjectNameOrId(projectId);
usePageTitle(`Change requests ${projectName}`); usePageTitle(`Change requests ${projectName}`);
const { changesets, loading } = useProjectSuggestedChanges(projectId); const { changeRequests, loading } = useProjectChangeRequests(projectId);
const { value, setValue } = createLocalStorage( const { value, setValue } = createLocalStorage(
`${projectId}:ProjectSuggestedChanges`, `${projectId}:ProjectChangeRequest`,
defaultSort defaultSort
); );
return ( return (
<SuggestionsTabs <ChangeRequestsTabs
changesets={changesets} changeRequests={changeRequests}
storedParams={value} storedParams={value}
setStoredParams={setValue} setStoredParams={setValue}
projectId={projectId} projectId={projectId}

View File

@ -11,8 +11,8 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { useStyles } from './FeatureOverviewEnvSwitch.styles'; import { useStyles } from './FeatureOverviewEnvSwitch.styles';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useSuggestToggle } from 'hooks/useSuggestToggle'; import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { SuggestChangesDialogue } from 'component/suggestChanges/SuggestChangeConfirmDialog/SuggestChangeConfirmDialog'; import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
interface IFeatureOverviewEnvSwitchProps { interface IFeatureOverviewEnvSwitchProps {
env: IFeatureEnvironment; env: IFeatureEnvironment;
@ -36,11 +36,11 @@ const FeatureOverviewEnvSwitch = ({
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { const {
onSuggestToggle, onChangeRequestToggle,
onSuggestToggleClose, onChangeRequestToggleClose,
onSuggestToggleConfirm, onChangeRequestToggleConfirm,
suggestChangesDialogDetails, changeRequestDialogDetails,
} = useSuggestToggle(projectId); } = useChangeRequestToggle(projectId);
const handleToggleEnvironmentOn = async () => { const handleToggleEnvironmentOn = async () => {
try { try {
@ -84,9 +84,9 @@ const FeatureOverviewEnvSwitch = ({
}; };
const toggleEnvironment = async (e: React.ChangeEvent) => { const toggleEnvironment = async (e: React.ChangeEvent) => {
if (uiConfig?.flags?.suggestChanges && env.name === 'production') { if (uiConfig?.flags?.changeRequests && env.name === 'production') {
e.preventDefault(); e.preventDefault();
onSuggestToggle(featureId, env.name, env.enabled); onChangeRequestToggle(featureId, env.name, env.enabled);
return; return;
} }
if (env.enabled) { if (env.enabled) {
@ -119,12 +119,12 @@ const FeatureOverviewEnvSwitch = ({
/> />
{content} {content}
</label> </label>
<SuggestChangesDialogue <ChangeRequestDialogue
isOpen={suggestChangesDialogDetails.isOpen} isOpen={changeRequestDialogDetails.isOpen}
onClose={onSuggestToggleClose} onClose={onChangeRequestToggleClose}
featureName={featureId} featureName={featureId}
environment={suggestChangesDialogDetails?.environment} environment={changeRequestDialogDetails?.environment}
onConfirm={onSuggestToggleConfirm} onConfirm={onChangeRequestToggleConfirm}
/> />
</div> </div>
); );

View File

@ -30,7 +30,7 @@ import StatusChip from 'component/common/StatusChip/StatusChip';
import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound'; import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureArchiveDialog } from '../../common/FeatureArchiveDialog/FeatureArchiveDialog'; import { FeatureArchiveDialog } from '../../common/FeatureArchiveDialog/FeatureArchiveDialog';
import { DraftBanner } from 'component/suggestChanges/DraftBanner/DraftBanner'; import { DraftBanner } from 'component/changeRequest/DraftBanner/DraftBanner';
import { MainLayout } from 'component/layout/MainLayout/MainLayout'; import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -88,7 +88,7 @@ export const FeatureView = () => {
<MainLayout <MainLayout
ref={ref} ref={ref}
subheader={ subheader={
uiConfig?.flags?.suggestChanges ? ( uiConfig?.flags?.changeRequests ? (
<DraftBanner project={projectId} /> <DraftBanner project={projectId} />
) : null ) : null
} }

View File

@ -24,10 +24,10 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Routes, Route, useLocation } from 'react-router-dom'; import { Routes, Route, useLocation } from 'react-router-dom';
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue'; import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
import { ProjectLog } from './ProjectLog/ProjectLog'; import { ProjectLog } from './ProjectLog/ProjectLog';
import { SuggestedChangeOverview } from 'component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview'; import { ChangeRequestOverview } from 'component/changeRequest/ChangeRequestOverview/ChangeRequestOverview';
import { DraftBanner } from 'component/suggestChanges/DraftBanner/DraftBanner'; import { DraftBanner } from 'component/changeRequest/DraftBanner/DraftBanner';
import { MainLayout } from 'component/layout/MainLayout/MainLayout'; import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import { ProjectSuggestedChanges } from '../../suggest-changes/ProjectSuggestions/ProjectSuggestedChanges'; import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests';
const StyledDiv = styled('div')(() => ({ const StyledDiv = styled('div')(() => ({
display: 'flex', display: 'flex',
@ -91,8 +91,8 @@ const Project = () => {
}, },
{ {
title: 'Change requests', title: 'Change requests',
path: `${basePath}/suggest-changes`, path: `${basePath}/change-requests`,
name: 'suggest-changes' + '', name: 'change-request' + '',
}, },
{ {
title: 'Event log', title: 'Event log',
@ -124,7 +124,7 @@ const Project = () => {
<MainLayout <MainLayout
ref={ref} ref={ref}
subheader={ subheader={
uiConfig?.flags?.suggestChanges ? ( !uiConfig?.flags?.changeRequests ? (
<DraftBanner project={projectId} /> <DraftBanner project={projectId} />
) : null ) : null
} }
@ -235,20 +235,20 @@ const Project = () => {
<Route path="archive" element={<ProjectFeaturesArchive />} /> <Route path="archive" element={<ProjectFeaturesArchive />} />
<Route path="logs" element={<ProjectLog />} /> <Route path="logs" element={<ProjectLog />} />
<Route <Route
path="suggest-changes" path="change-requests"
element={ element={
<ConditionallyRender <ConditionallyRender
condition={Boolean(uiConfig?.flags?.suggestChanges)} condition={Boolean(uiConfig?.flags?.changeRequests)}
show={<ProjectSuggestedChanges />} show={<ProjectChangeRequests />}
/> />
} }
/> />
<Route <Route
path="suggest-changes/:id" path="change-requests/:id"
element={ element={
<ConditionallyRender <ConditionallyRender
condition={Boolean(uiConfig?.flags?.suggestChanges)} condition={Boolean(uiConfig?.flags?.changeRequests)}
show={<SuggestedChangeOverview />} show={<ChangeRequestOverview />}
/> />
} }
/> />

View File

@ -36,8 +36,8 @@ import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/Feat
import { useSearch } from 'hooks/useSearch'; import { useSearch } from 'hooks/useSearch';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
import { useSuggestToggle } from 'hooks/useSuggestToggle'; import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { SuggestChangesDialogue } from 'component/suggestChanges/SuggestChangeConfirmDialog/SuggestChangeConfirmDialog'; import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
interface IProjectFeatureTogglesProps { interface IProjectFeatureTogglesProps {
features: IProject['features']; features: IProject['features'];
@ -102,11 +102,11 @@ export const ProjectFeatureToggles = ({
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } = const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
useFeatureApi(); useFeatureApi();
const { const {
onSuggestToggle, onChangeRequestToggle,
onSuggestToggleClose, onChangeRequestToggleClose,
onSuggestToggleConfirm, onChangeRequestToggleConfirm,
suggestChangesDialogDetails, changeRequestDialogDetails,
} = useSuggestToggle(projectId); } = useChangeRequestToggle(projectId);
const onToggle = useCallback( const onToggle = useCallback(
async ( async (
@ -116,10 +116,10 @@ export const ProjectFeatureToggles = ({
enabled: boolean enabled: boolean
) => { ) => {
if ( if (
uiConfig?.flags?.suggestChanges && uiConfig?.flags?.changeRequests &&
environment === 'production' environment === 'production'
) { ) {
onSuggestToggle(featureName, environment, enabled); onChangeRequestToggle(featureName, environment, enabled);
throw new Error('Additional approval required'); throw new Error('Additional approval required');
} }
try { try {
@ -517,12 +517,12 @@ export const ProjectFeatureToggles = ({
featureId={featureArchiveState || ''} featureId={featureArchiveState || ''}
projectId={projectId} projectId={projectId}
/>{' '} />{' '}
<SuggestChangesDialogue <ChangeRequestDialogue
isOpen={suggestChangesDialogDetails.isOpen} isOpen={changeRequestDialogDetails.isOpen}
onClose={onSuggestToggleClose} onClose={onChangeRequestToggleClose}
featureName={suggestChangesDialogDetails?.featureName} featureName={changeRequestDialogDetails?.featureName}
environment={suggestChangesDialogDetails?.environment} environment={changeRequestDialogDetails?.environment}
onConfirm={onSuggestToggleConfirm} onConfirm={onChangeRequestToggleConfirm}
/> />
</PageContent> </PageContent>
); );

View File

@ -1,6 +1,6 @@
import useAPI from '../useApi/useApi'; import useAPI from '../useApi/useApi';
interface ISuggestChangeSchema { interface IChangeRequestsSchema {
feature: string; feature: string;
action: action:
| 'updateEnabled' | 'updateEnabled'
@ -10,17 +10,17 @@ interface ISuggestChangeSchema {
payload: string | boolean | object | number; payload: string | boolean | object | number;
} }
export const useSuggestChangeApi = () => { export const useChangeRequestApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true, propagateErrors: true,
}); });
const addSuggestion = async ( const addChangeRequest = async (
project: string, project: string,
environment: string, environment: string,
payload: ISuggestChangeSchema payload: IChangeRequestsSchema
) => { ) => {
const path = `api/admin/projects/${project}/environments/${environment}/suggest-changes`; const path = `api/admin/projects/${project}/environments/${environment}/change-requests`;
const req = createRequest(path, { const req = createRequest(path, {
method: 'POST', method: 'POST',
body: JSON.stringify(payload), body: JSON.stringify(payload),
@ -35,10 +35,10 @@ export const useSuggestChangeApi = () => {
const changeState = async ( const changeState = async (
project: string, project: string,
suggestChangeId: number, changeRequestId: number,
payload: any payload: any
) => { ) => {
const path = `api/admin/projects/${project}/suggest-changes/${suggestChangeId}/state`; const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/state`;
const req = createRequest(path, { const req = createRequest(path, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(payload), body: JSON.stringify(payload),
@ -51,8 +51,8 @@ export const useSuggestChangeApi = () => {
} }
}; };
const applyChanges = async (project: string, suggestChangeId: string) => { const applyChanges = async (project: string, changeRequestId: string) => {
const path = `api/admin/projects/${project}/suggest-changes/${suggestChangeId}/apply`; const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/apply`;
const req = createRequest(path, { const req = createRequest(path, {
method: 'PUT', method: 'PUT',
}); });
@ -64,12 +64,12 @@ export const useSuggestChangeApi = () => {
} }
}; };
const discardSuggestions = async ( const discardChangeRequestEvent = async (
project: string, project: string,
changesetId: number, changeRequestId: number,
changeId: number changeRequestEventId: number
) => { ) => {
const path = `api/admin/projects/${project}/suggest-changes/${changesetId}/changes/${changeId}`; const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/changes/${changeRequestEventId}`;
const req = createRequest(path, { const req = createRequest(path, {
method: 'DELETE', method: 'DELETE',
}); });
@ -81,10 +81,10 @@ export const useSuggestChangeApi = () => {
}; };
return { return {
addSuggestion, addChangeRequest,
applyChanges, applyChanges,
changeState, changeState,
discardSuggestions, discardChangeRequestEvent,
errors, errors,
loading, loading,
}; };

View File

@ -2,16 +2,16 @@ import useSWR from 'swr';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
export const useSuggestedChange = (projectId: string, id: string) => { export const useChangeRequest = (projectId: string, id: string) => {
const { data, error, mutate } = useSWR( const { data, error, mutate } = useSWR(
formatApiPath(`api/admin/projects/${projectId}/suggest-changes/${id}`), formatApiPath(`api/admin/projects/${projectId}/change-requests/${id}`),
fetcher fetcher
); );
return { return {
data, data,
loading: !error && !data, loading: !error && !data,
refetchSuggestedChange: () => mutate(), refetchChangeRequest: () => mutate(),
error, error,
}; };
}; };

View File

@ -17,7 +17,7 @@ interface IChange {
}; };
} }
export interface ISuggestChangesResponse { export interface IChangeRequestResponse {
id: number; id: number;
environment: string; environment: string;
state: string; state: string;
@ -36,13 +36,13 @@ export interface ISuggestChangesResponse {
const fetcher = (path: string) => { const fetcher = (path: string) => {
return fetch(path) return fetch(path)
.then(handleErrorResponses('SuggestedChanges')) .then(handleErrorResponses('ChangeRequest'))
.then(res => res.json()); .then(res => res.json());
}; };
export const useSuggestedChangesDraft = (project: string) => { export const useChangeRequestDraft = (project: string) => {
const { data, error, mutate } = useSWR<ISuggestChangesResponse[]>( const { data, error, mutate } = useSWR<IChangeRequestResponse[]>(
formatApiPath(`api/admin/projects/${project}/suggest-changes/draft`), formatApiPath(`api/admin/projects/${project}/change-requests/draft`),
fetcher fetcher
); );

View File

@ -5,19 +5,19 @@ import handleErrorResponses from '../httpErrorResponseHandler';
const fetcher = (path: string) => { const fetcher = (path: string) => {
return fetch(path) return fetch(path)
.then(handleErrorResponses('SuggestedChanges')) .then(handleErrorResponses('ChangeRequest'))
.then(res => res.json()); .then(res => res.json());
}; };
export const useProjectSuggestedChanges = (project: string) => { export const useProjectChangeRequests = (project: string) => {
const { data, error, mutate } = useSWR( const { data, error, mutate } = useSWR(
formatApiPath(`api/admin/projects/${project}/suggest-changes`), formatApiPath(`api/admin/projects/${project}/change-requests`),
fetcher fetcher
); );
return useMemo( return useMemo(
() => ({ () => ({
changesets: data, changeRequests: data,
loading: !error && !data, loading: !error && !data,
refetch: () => mutate(), refetch: () => mutate(),
error, error,

View File

@ -0,0 +1,67 @@
import { useCallback, useState } from 'react';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useChangeRequestApi } from './api/actions/useChangeRequestApi/useChangeRequestApi';
import { useChangeRequestDraft } from './api/getters/useChangeRequestDraft/useChangeRequestDraft';
export const useChangeRequestToggle = (project: string) => {
const { setToastData, setToastApiError } = useToast();
const { addChangeRequest } = useChangeRequestApi();
const { refetch: refetchChangeRequests } = useChangeRequestDraft(project);
const [changeRequestDialogDetails, setChangeRequestDialogDetails] =
useState<{
enabled?: boolean;
featureName?: string;
environment?: string;
isOpen: boolean;
}>({ isOpen: false });
const onChangeRequestToggle = useCallback(
(featureName: string, environment: string, enabled: boolean) => {
setChangeRequestDialogDetails({
featureName,
environment,
enabled,
isOpen: true,
});
},
[]
);
const onChangeRequestToggleClose = useCallback(() => {
setChangeRequestDialogDetails({ isOpen: false });
}, []);
const onChangeRequestToggleConfirm = useCallback(async () => {
try {
await addChangeRequest(
project,
changeRequestDialogDetails.environment!,
{
feature: changeRequestDialogDetails.featureName!,
action: 'updateEnabled',
payload: {
enabled: Boolean(changeRequestDialogDetails.enabled),
},
}
);
refetchChangeRequests();
setChangeRequestDialogDetails({ isOpen: false });
setToastData({
type: 'success',
title: 'Changes added to the draft!',
});
} catch (error) {
setToastApiError(formatUnknownError(error));
setChangeRequestDialogDetails({ isOpen: false });
}
}, [addChangeRequest]);
return {
onChangeRequestToggle,
onChangeRequestToggleClose,
onChangeRequestToggleConfirm,
changeRequestDialogDetails,
};
};

View File

@ -1,68 +0,0 @@
import { useCallback, useState } from 'react';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useSuggestChangeApi } from './api/actions/useSuggestChangeApi/useSuggestChangeApi';
import { useSuggestedChangesDraft } from './api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
export const useSuggestToggle = (project: string) => {
const { setToastData, setToastApiError } = useToast();
const { addSuggestion } = useSuggestChangeApi();
const { refetch: refetchSuggestedChange } =
useSuggestedChangesDraft(project);
const [suggestChangesDialogDetails, setSuggestChangesDialogDetails] =
useState<{
enabled?: boolean;
featureName?: string;
environment?: string;
isOpen: boolean;
}>({ isOpen: false });
const onSuggestToggle = useCallback(
(featureName: string, environment: string, enabled: boolean) => {
setSuggestChangesDialogDetails({
featureName,
environment,
enabled,
isOpen: true,
});
},
[]
);
const onSuggestToggleClose = useCallback(() => {
setSuggestChangesDialogDetails({ isOpen: false });
}, []);
const onSuggestToggleConfirm = useCallback(async () => {
try {
await addSuggestion(
project,
suggestChangesDialogDetails.environment!,
{
feature: suggestChangesDialogDetails.featureName!,
action: 'updateEnabled',
payload: {
enabled: Boolean(suggestChangesDialogDetails.enabled),
},
}
);
refetchSuggestedChange();
setSuggestChangesDialogDetails({ isOpen: false });
setToastData({
type: 'success',
title: 'Changes added to the draft!',
});
} catch (error) {
setToastApiError(formatUnknownError(error));
setSuggestChangesDialogDetails({ isOpen: false });
}
}, [addSuggestion]);
return {
onSuggestToggle,
onSuggestToggleClose,
onSuggestToggleConfirm,
suggestChangesDialogDetails,
};
};

View File

@ -1,4 +1,4 @@
export interface ISuggestChangeset { export interface IChangeRequest {
id: number; id: number;
state: state:
| 'CREATED' | 'CREATED'
@ -11,11 +11,10 @@ export interface ISuggestChangeset {
environment: string; environment: string;
createdBy?: string; createdBy?: string;
createdAt?: Date; createdAt?: Date;
changes?: ISuggestChange[]; changes?: IChangeRequestEvent[];
events?: ISuggestChangeEvent[];
} }
export interface ISuggestChange { export interface IChangeRequestEvent {
id: number; id: number;
action: action:
| 'updateEnabled' | 'updateEnabled'
@ -28,7 +27,7 @@ export interface ISuggestChange {
createdAt?: Date; createdAt?: Date;
} }
export enum SuggestChangesetEvent { export enum ChangeRequestEvent {
CREATED = 'CREATED', CREATED = 'CREATED',
UPDATED = 'UPDATED', UPDATED = 'UPDATED',
SUBMITTED = 'SUBMITTED', SUBMITTED = 'SUBMITTED',
@ -36,23 +35,3 @@ export enum SuggestChangesetEvent {
REJECTED = 'REJECTED', REJECTED = 'REJECTED',
CLOSED = 'CLOSED', CLOSED = 'CLOSED',
} }
export enum SuggestChangeEvent {
UPDATE_ENABLED = 'updateFeatureEnabledEvent',
ADD_STRATEGY = 'addStrategyEvent',
UPDATE_STRATEGY = 'updateStrategyEvent',
DELETE_STRATEGY = 'deleteStrategyEvent',
}
export interface ISuggestChangeEvent {
id: number;
event: SuggestChangesetEvent;
data: ISuggestChangeEventData;
createdBy?: string;
createdAt?: Date;
}
export interface ISuggestChangeEventData {
feature: string;
data: unknown;
}

View File

@ -42,7 +42,7 @@ export interface IFlags {
embedProxyFrontend?: boolean; embedProxyFrontend?: boolean;
publicSignup?: boolean; publicSignup?: boolean;
syncSSOGroups?: boolean; syncSSOGroups?: boolean;
suggestChanges?: boolean; changeRequests?: boolean;
cloneEnvironment?: boolean; cloneEnvironment?: boolean;
} }

View File

@ -69,12 +69,12 @@ exports[`should create default config 1`] = `
"ENABLE_DARK_MODE_SUPPORT": false, "ENABLE_DARK_MODE_SUPPORT": false,
"anonymiseEventLog": false, "anonymiseEventLog": false,
"batchMetrics": false, "batchMetrics": false,
"changeRequests": false,
"cloneEnvironment": false, "cloneEnvironment": false,
"embedProxy": false, "embedProxy": false,
"embedProxyFrontend": false, "embedProxyFrontend": false,
"publicSignup": false, "publicSignup": false,
"responseTimeWithAppName": false, "responseTimeWithAppName": false,
"suggestChanges": false,
"syncSSOGroups": false, "syncSSOGroups": false,
}, },
}, },
@ -83,12 +83,12 @@ exports[`should create default config 1`] = `
"ENABLE_DARK_MODE_SUPPORT": false, "ENABLE_DARK_MODE_SUPPORT": false,
"anonymiseEventLog": false, "anonymiseEventLog": false,
"batchMetrics": false, "batchMetrics": false,
"changeRequests": false,
"cloneEnvironment": false, "cloneEnvironment": false,
"embedProxy": false, "embedProxy": false,
"embedProxyFrontend": false, "embedProxyFrontend": false,
"publicSignup": false, "publicSignup": false,
"responseTimeWithAppName": false, "responseTimeWithAppName": false,
"suggestChanges": false,
"syncSSOGroups": false, "syncSSOGroups": false,
}, },
"externalResolver": { "externalResolver": {

View File

@ -82,8 +82,6 @@ export const PUBLIC_SIGNUP_TOKEN_CREATED = 'public-signup-token-created';
export const PUBLIC_SIGNUP_TOKEN_USER_ADDED = 'public-signup-token-user-added'; export const PUBLIC_SIGNUP_TOKEN_USER_ADDED = 'public-signup-token-user-added';
export const PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED = 'public-signup-token-updated'; export const PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED = 'public-signup-token-updated';
export const SUGGEST_CHANGE_CREATED = 'suggest-change-created';
export interface IBaseEvent { export interface IBaseEvent {
type: string; type: string;
createdBy: string; createdBy: string;

View File

@ -10,8 +10,8 @@ export const defaultExperimentalOptions = {
process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY, process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY,
false, false,
), ),
suggestChanges: parseEnvVarBoolean( changeRequests: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_SUGGEST_CHANGES, process.env.UNLEASH_EXPERIMENTAL_CHANGE_REQUESTS,
false, false,
), ),
syncSSOGroups: parseEnvVarBoolean( syncSSOGroups: parseEnvVarBoolean(
@ -51,7 +51,7 @@ export interface IExperimentalOptions {
batchMetrics?: boolean; batchMetrics?: boolean;
anonymiseEventLog?: boolean; anonymiseEventLog?: boolean;
syncSSOGroups?: boolean; syncSSOGroups?: boolean;
suggestChanges?: boolean; changeRequests?: boolean;
cloneEnvironment?: boolean; cloneEnvironment?: boolean;
}; };
externalResolver: IExternalFlagResolver; externalResolver: IExternalFlagResolver;

View File

@ -1,7 +1,7 @@
import { ITagType } from './stores/tag-type-store'; import { ITagType } from './stores/tag-type-store';
import { LogProvider } from '../logger'; import { LogProvider } from '../logger';
import { IRole } from './stores/access-store'; import { IRole } from './stores/access-store';
import User, { IUser } from './user'; import { IUser } from './user';
import { ALL_OPERATORS } from '../util/constants'; import { ALL_OPERATORS } from '../util/constants';
export type Operator = typeof ALL_OPERATORS[number]; export type Operator = typeof ALL_OPERATORS[number];
@ -372,99 +372,3 @@ export interface IFeatureStrategySegment {
featureStrategyId: string; featureStrategyId: string;
segmentId: number; segmentId: number;
} }
export interface ISuggestChangeset {
id: number;
state: string;
project: string;
environment: string;
createdBy: Pick<User, 'id' | 'username' | 'imageUrl'>;
createdAt: Date;
features: ISuggestChangeFeature[];
}
export interface ISuggestChangeFeature {
name: string;
changes: ISuggestChange[];
}
export interface ISuggestChangeBase {
id?: number;
action: SuggestChangeAction;
payload: SuggestChangePayload;
createdBy?: Pick<User, 'id' | 'username' | 'imageUrl'>;
createdAt?: Date;
}
export enum SuggestChangesetState {
DRAFT = 'Draft',
APPROVED = 'Approved',
IN_REVIEW = 'In review',
APPLIED = 'Applied',
CANCELLED = 'Cancelled',
}
type SuggestChangePayload =
| SuggestChangeEnabled
| SuggestChangeAddStrategy
| SuggestChangeEditStrategy
| SuggestChangeDeleteStrategy;
export interface ISuggestChangeAddStrategy extends ISuggestChangeBase {
action: 'addStrategy';
payload: SuggestChangeAddStrategy;
}
export interface ISuggestChangeDeleteStrategy extends ISuggestChangeBase {
action: 'deleteStrategy';
payload: SuggestChangeDeleteStrategy;
}
export interface ISuggestChangeUpdateStrategy extends ISuggestChangeBase {
action: 'updateStrategy';
payload: SuggestChangeEditStrategy;
}
export interface ISuggestChangeEnabled extends ISuggestChangeBase {
action: 'updateEnabled';
payload: SuggestChangeEnabled;
}
export type ISuggestChange =
| ISuggestChangeAddStrategy
| ISuggestChangeDeleteStrategy
| ISuggestChangeUpdateStrategy
| ISuggestChangeEnabled;
type SuggestChangeEnabled = { enabled: boolean };
type SuggestChangeAddStrategy = Pick<
IFeatureStrategy,
'parameters' | 'constraints'
> & { name: string };
type SuggestChangeEditStrategy = SuggestChangeAddStrategy & { id: string };
type SuggestChangeDeleteStrategy = {
deleteId: string;
};
export enum SuggestChangesetEvent {
CREATED = 'CREATED',
UPDATED = 'UPDATED',
SUBMITTED = 'SUBMITTED',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
CLOSED = 'CLOSED',
}
export type SuggestChangeAction =
| 'updateEnabled'
| 'addStrategy'
| 'updateStrategy'
| 'deleteStrategy';
export interface ISuggestChangeEventData {
feature: string;
data: unknown;
}

View File

@ -37,4 +37,3 @@ export const MOVE_FEATURE_TOGGLE = 'MOVE_FEATURE_TOGGLE';
export const CREATE_SEGMENT = 'CREATE_SEGMENT'; export const CREATE_SEGMENT = 'CREATE_SEGMENT';
export const UPDATE_SEGMENT = 'UPDATE_SEGMENT'; export const UPDATE_SEGMENT = 'UPDATE_SEGMENT';
export const DELETE_SEGMENT = 'DELETE_SEGMENT'; export const DELETE_SEGMENT = 'DELETE_SEGMENT';
export const SUGGEST_CHANGE = 'SUGGEST_CHANGE';

View File

@ -0,0 +1,40 @@
'use strict';
exports.up = function (db, callback) {
db.runSql(
`
DROP TABLE IF EXISTS suggest_change;
DROP TABLE IF EXISTS suggest_change_set;
CREATE TABLE IF NOT EXISTS change_requests (
id serial primary key,
environment varchar(100) REFERENCES environments(name) ON DELETE CASCADE,
state varchar(255) NOT NULL,
project varchar(255) REFERENCES projects(id) ON DELETE CASCADE,
created_by integer not null references users (id) ON DELETE CASCADE,
created_at timestamp default now()
);
CREATE TABLE IF NOT EXISTS change_request_events (
id serial primary key,
feature varchar(255) NOT NULL references features(name) on delete cascade,
action varchar(255) NOT NULL,
payload jsonb not null default '[]'::jsonb,
created_by integer not null references users (id) ON DELETE CASCADE,
created_at timestamp default now(),
change_request_id integer NOT NULL REFERENCES change_requests(id) ON DELETE CASCADE,
UNIQUE (feature, action, change_request_id)
);
`,
callback,
);
};
exports.down = function (db, callback) {
db.runSql(
`
DROP TABLE IF EXISTS change_request_events;
DROP TABLE IF EXISTS change_requests;
`,
callback,
);
};

View File

@ -39,7 +39,7 @@ process.nextTick(async () => {
anonymiseEventLog: false, anonymiseEventLog: false,
responseTimeWithAppName: true, responseTimeWithAppName: true,
syncSSOGroups: true, syncSSOGroups: true,
suggestChanges: true, changeRequests: true,
cloneEnvironment: true, cloneEnvironment: true,
}, },
}, },

View File

@ -28,7 +28,7 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
embedProxyFrontend: true, embedProxyFrontend: true,
batchMetrics: true, batchMetrics: true,
syncSSOGroups: true, syncSSOGroups: true,
suggestChanges: true, changeRequests: true,
cloneEnvironment: true, cloneEnvironment: true,
}, },
}, },

View File

@ -11,7 +11,7 @@ test('disabled middleware should not block paths that use the same path', async
conditionalMiddleware( conditionalMiddleware(
() => false, () => false,
(req, res) => { (req, res) => {
res.send({ suggestChanges: 'hello' }); res.send({ changeRequest: 'hello' });
}, },
), ),
); );
@ -30,11 +30,11 @@ test('should return 404 when path is not enabled', async () => {
const path = '/api/admin/projects'; const path = '/api/admin/projects';
app.use( app.use(
`${path}/suggest-changes`, `${path}/change-requests`,
conditionalMiddleware( conditionalMiddleware(
() => false, () => false,
(req, res) => { (req, res) => {
res.send({ suggestChanges: 'hello' }); res.send({ changeRequest: 'hello' });
}, },
), ),
); );
@ -43,7 +43,7 @@ test('should return 404 when path is not enabled', async () => {
res.json({ projects: [] }); res.json({ projects: [] });
}); });
await supertest(app).get('/api/admin/projects/suggest-changes').expect(404); await supertest(app).get('/api/admin/projects/change-requests').expect(404);
}); });
test('should respect ordering of endpoints', async () => { test('should respect ordering of endpoints', async () => {
@ -55,7 +55,7 @@ test('should respect ordering of endpoints', async () => {
conditionalMiddleware( conditionalMiddleware(
() => true, () => true,
(req, res) => { (req, res) => {
res.json({ name: 'Suggest changes' }); res.json({ name: 'Request changes' });
}, },
), ),
); );
@ -66,7 +66,7 @@ test('should respect ordering of endpoints', async () => {
await supertest(app) await supertest(app)
.get('/api/admin/projects') .get('/api/admin/projects')
.expect(200, { name: 'Suggest changes' }); .expect(200, { name: 'Request changes' });
}); });
test('disabled middleware should not block paths that use the same basepath', async () => { test('disabled middleware should not block paths that use the same basepath', async () => {
@ -74,11 +74,11 @@ test('disabled middleware should not block paths that use the same basepath', as
const path = '/api/admin/projects'; const path = '/api/admin/projects';
app.use( app.use(
`${path}/suggest-changes`, `${path}/change-requests`,
conditionalMiddleware( conditionalMiddleware(
() => false, () => false,
(req, res) => { (req, res) => {
res.json({ name: 'Suggest changes' }); res.json({ name: 'Request changes' });
}, },
), ),
); );

View File

@ -1562,7 +1562,7 @@ This event fires when you delete a segment.
### 'suggest-change-created' ### 'suggest-change-created'
This event fires when you create a a suggest-changes draft. This event fires when you create a a change-request draft.
```json title="example event: suggest-change-created" ```json title="example event: suggest-change-created"
{ {