mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +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:
parent
da102a3e98
commit
5dd8616c74
@ -1,45 +1,36 @@
|
||||
import { useCallback, VFC } from 'react';
|
||||
import { VFC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { SuggestedFeatureToggleChange } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/SuggestedFeatureToggleChange';
|
||||
import { ChangeRequestFeatureToggleChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ChangeRequestFeatureToggleChange';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ToggleStatusChange } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/ToggleStatusChange';
|
||||
// import {
|
||||
// StrategyAddedChange,
|
||||
// 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 { ToggleStatusChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ToggleStatusChange';
|
||||
import type { IChangeRequestResponse } from 'hooks/api/getters/useChangeRequestDraft/useChangeRequestDraft';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import useToast from 'hooks/useToast';
|
||||
|
||||
interface ISuggestedChangeset {
|
||||
suggestedChange: ISuggestChangesResponse;
|
||||
interface IChangeRequest {
|
||||
changeRequest: IChangeRequestResponse;
|
||||
onRefetch?: () => void;
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
export const SuggestedChangeset: VFC<ISuggestedChangeset> = ({
|
||||
suggestedChange,
|
||||
export const ChangeRequest: VFC<IChangeRequest> = ({
|
||||
changeRequest,
|
||||
onRefetch,
|
||||
onNavigate,
|
||||
}) => {
|
||||
const { discardSuggestions } = useSuggestChangeApi();
|
||||
const { discardChangeRequestEvent } = useChangeRequestApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const onDiscard = (id: number) => async () => {
|
||||
try {
|
||||
await discardSuggestions(
|
||||
suggestedChange.project,
|
||||
suggestedChange.id,
|
||||
await discardChangeRequestEvent(
|
||||
changeRequest.project,
|
||||
changeRequest.id,
|
||||
id
|
||||
);
|
||||
setToastData({
|
||||
title: 'Change discarded from suggestion draft.',
|
||||
title: 'Change discarded from change request draft.',
|
||||
type: 'success',
|
||||
});
|
||||
onRefetch?.();
|
||||
@ -51,11 +42,11 @@ export const SuggestedChangeset: VFC<ISuggestedChangeset> = ({
|
||||
return (
|
||||
<Box>
|
||||
Changes
|
||||
{suggestedChange.features?.map(featureToggleChange => (
|
||||
<SuggestedFeatureToggleChange
|
||||
{changeRequest.features?.map(featureToggleChange => (
|
||||
<ChangeRequestFeatureToggleChange
|
||||
key={featureToggleChange.name}
|
||||
featureName={featureToggleChange.name}
|
||||
projectId={suggestedChange.project}
|
||||
projectId={changeRequest.project}
|
||||
onNavigate={onNavigate}
|
||||
>
|
||||
{featureToggleChange.changes.map(change => (
|
||||
@ -92,7 +83,7 @@ export const SuggestedChangeset: VFC<ISuggestedChangeset> = ({
|
||||
/> */}
|
||||
</Box>
|
||||
))}
|
||||
</SuggestedFeatureToggleChange>
|
||||
</ChangeRequestFeatureToggleChange>
|
||||
))}
|
||||
</Box>
|
||||
);
|
@ -2,7 +2,7 @@ import { FC } from 'react';
|
||||
import { Alert, Typography } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
|
||||
interface ISuggestChangesDialogueProps {
|
||||
interface IChangeRequestDialogueProps {
|
||||
isOpen: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
@ -11,7 +11,7 @@ interface ISuggestChangesDialogueProps {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const SuggestChangesDialogue: FC<ISuggestChangesDialogueProps> = ({
|
||||
export const ChangeRequestDialogue: FC<IChangeRequestDialogueProps> = ({
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onClose,
|
||||
@ -25,15 +25,15 @@ export const SuggestChangesDialogue: FC<ISuggestChangesDialogueProps> = ({
|
||||
secondaryButtonText="Cancel"
|
||||
onClick={onConfirm}
|
||||
onClose={onClose}
|
||||
title="Suggest changes"
|
||||
title="Request changes"
|
||||
>
|
||||
<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
|
||||
will be added into a draft that you can submit for review.
|
||||
</Alert>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Suggested changes:
|
||||
Change requests:
|
||||
</Typography>
|
||||
<Typography>
|
||||
<strong>{enabled ? 'Disable' : 'Enable'}</strong> feature toggle{' '}
|
@ -3,14 +3,14 @@ import { Link } from 'react-router-dom';
|
||||
import { Box, Card, Typography } from '@mui/material';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
|
||||
interface ISuggestedFeatureToggleChange {
|
||||
interface IChangeRequestToggleChange {
|
||||
featureName: string;
|
||||
projectId: string;
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
export const SuggestedFeatureToggleChange: FC<
|
||||
ISuggestedFeatureToggleChange
|
||||
export const ChangeRequestFeatureToggleChange: FC<
|
||||
IChangeRequestToggleChange
|
||||
> = ({ featureName, projectId, onNavigate, children }) => {
|
||||
return (
|
||||
<Card
|
@ -4,8 +4,8 @@ import { PlaygroundResultChip } from 'component/playground/Playground/Playground
|
||||
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
||||
suggestedChange,
|
||||
export const ChangeRequestHeader: FC<{ changeRequest: any }> = ({
|
||||
changeRequest,
|
||||
}) => {
|
||||
return (
|
||||
<Paper
|
||||
@ -30,9 +30,9 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
||||
}}
|
||||
variant="h1"
|
||||
>
|
||||
Suggestion
|
||||
Change request
|
||||
<Typography variant="h1" component="p">
|
||||
#{suggestedChange.id}
|
||||
#{changeRequest.id}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<PlaygroundResultChip
|
||||
@ -43,10 +43,10 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}>
|
||||
<Typography sx={{ margin: 'auto 0' }}>
|
||||
Created{' '}
|
||||
<TimeAgo date={new Date(suggestedChange.createdAt)} /> by
|
||||
Created <TimeAgo date={new Date(changeRequest.createdAt)} />{' '}
|
||||
by
|
||||
</Typography>
|
||||
<Avatar src={suggestedChange?.createdBy?.avatar} />
|
||||
<Avatar src={changeRequest?.createdBy?.avatar} />
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={theme => ({
|
||||
@ -56,11 +56,11 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
||||
>
|
||||
Environment:{' '}
|
||||
<Typography display="inline" fontWeight="bold">
|
||||
{suggestedChange?.environment}
|
||||
{changeRequest?.environment}
|
||||
</Typography>{' '}
|
||||
| Updates:{' '}
|
||||
<Typography display="inline" fontWeight="bold">
|
||||
{suggestedChange?.features.length} feature toggles
|
||||
{changeRequest?.features.length} feature toggles
|
||||
</Typography>
|
||||
</Card>
|
||||
</Box>
|
@ -1,24 +1,24 @@
|
||||
import { FC } from 'react';
|
||||
import { Box, Button, Paper } from '@mui/material';
|
||||
import { useSuggestedChange } from 'hooks/api/getters/useSuggestChange/useSuggestedChange';
|
||||
import { SuggestedChangeHeader } from './SuggestedChangeHeader/SuggestedChangeHeader';
|
||||
import { SuggestedChangeTimeline } from './SuggestedChangeTimeline/SuggestedChangeTimeline';
|
||||
import { SuggestedChangeReviewers } from './SuggestedChangeReviewers/SuggestedChangeReviewers';
|
||||
import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset';
|
||||
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
||||
import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
|
||||
import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline';
|
||||
import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers';
|
||||
import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
|
||||
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 { 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 id = useRequiredPathParam('id');
|
||||
const { data: suggestedChange } = useSuggestedChange(projectId, id);
|
||||
const { applyChanges } = useSuggestChangeApi();
|
||||
const { data: changeRequest } = useChangeRequest(projectId, id);
|
||||
const { applyChanges } = useChangeRequestApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
if (!suggestedChange) {
|
||||
if (!changeRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export const SuggestedChangeOverview: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SuggestedChangeHeader suggestedChange={suggestedChange} />
|
||||
<ChangeRequestHeader changeRequest={changeRequest} />
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box
|
||||
sx={{
|
||||
@ -46,8 +46,8 @@ export const SuggestedChangeOverview: FC = () => {
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<SuggestedChangeTimeline />
|
||||
<SuggestedChangeReviewers />
|
||||
<ChangeRequestTimeline />
|
||||
<ChangeRequestReviewers />
|
||||
</Box>
|
||||
<Paper
|
||||
elevation={0}
|
||||
@ -65,8 +65,8 @@ export const SuggestedChangeOverview: FC = () => {
|
||||
padding: theme.spacing(2),
|
||||
})}
|
||||
>
|
||||
<SuggestedChangeset suggestedChange={suggestedChange} />
|
||||
<SuggestedChangeReviewStatus approved={true} />
|
||||
<ChangeRequest changeRequest={changeRequest} />
|
||||
<ChangeRequestReviewStatus approved={true} />
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ marginTop: 2 }}
|
@ -11,13 +11,13 @@ import {
|
||||
StyledErrorIcon,
|
||||
StyledReviewTitle,
|
||||
StyledDivider,
|
||||
} from './SuggestChangeReviewStatus.styles';
|
||||
} from './ChangeRequestReviewStatus.styles';
|
||||
|
||||
interface ISuggestChangeReviewsStatusProps {
|
||||
approved: boolean;
|
||||
}
|
||||
|
||||
export const SuggestedChangeReviewStatus: FC<
|
||||
export const ChangeRequestReviewStatus: FC<
|
||||
ISuggestChangeReviewsStatusProps
|
||||
> = ({ approved }) => {
|
||||
return (
|
@ -1,6 +1,6 @@
|
||||
import { Box, Paper } from '@mui/material';
|
||||
|
||||
export const SuggestedChangeReviewers = () => {
|
||||
export const ChangeRequestReviewers = () => {
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
@ -7,7 +7,7 @@ import TimelineDot from '@mui/lab/TimelineDot';
|
||||
import TimelineConnector from '@mui/lab/TimelineConnector';
|
||||
import TimelineContent from '@mui/lab/TimelineContent';
|
||||
|
||||
export const SuggestedChangeTimeline: FC = () => {
|
||||
export const ChangeRequestTimeline: FC = () => {
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
@ -5,11 +5,11 @@ import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { HelpOutline } from '@mui/icons-material';
|
||||
import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset';
|
||||
import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
|
||||
import { useSuggestChangeApi } from 'hooks/api/actions/useSuggestChangeApi/useSuggestChangeApi';
|
||||
import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
|
||||
import { useChangeRequestDraft } from 'hooks/api/getters/useChangeRequestDraft/useChangeRequestDraft';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
|
||||
interface ISuggestedChangesSidebarProps {
|
||||
interface IChangeRequestSidebarProps {
|
||||
open: boolean;
|
||||
project: string;
|
||||
onClose: () => void;
|
||||
@ -41,7 +41,7 @@ const StyledHeaderHint = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
|
||||
export const ChangeRequestSidebar: VFC<IChangeRequestSidebarProps> = ({
|
||||
open,
|
||||
project,
|
||||
onClose,
|
||||
@ -49,14 +49,14 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
|
||||
const {
|
||||
draft,
|
||||
loading,
|
||||
refetch: refetchSuggestedChanges,
|
||||
} = useSuggestedChangesDraft(project);
|
||||
const { changeState } = useSuggestChangeApi();
|
||||
refetch: refetchChangeRequest,
|
||||
} = useChangeRequestDraft(project);
|
||||
const { changeState } = useChangeRequestApi();
|
||||
|
||||
const onReview = async (draftId: number) => {
|
||||
try {
|
||||
await changeState(project, draftId, { state: 'In review' });
|
||||
refetchSuggestedChanges();
|
||||
refetchChangeRequest();
|
||||
} catch (e) {
|
||||
console.log('something went wrong');
|
||||
}
|
||||
@ -108,16 +108,16 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
|
||||
</Tooltip>
|
||||
<StyledHeaderHint>
|
||||
Make sure you are sending the right changes
|
||||
suggestions to be reviewed
|
||||
to be reviewed
|
||||
</StyledHeaderHint>
|
||||
</>
|
||||
}
|
||||
></PageHeader>
|
||||
}
|
||||
>
|
||||
{draft?.map(environmentChangeset => (
|
||||
{draft?.map(environmentChangeRequest => (
|
||||
<Box
|
||||
key={environmentChangeset.id}
|
||||
key={environmentChangeRequest.id}
|
||||
sx={{
|
||||
padding: 2,
|
||||
border: '2px solid',
|
||||
@ -127,35 +127,37 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
env: {environmentChangeset?.environment}
|
||||
env: {environmentChangeRequest?.environment}
|
||||
</Typography>
|
||||
<Typography>
|
||||
state: {environmentChangeset?.state}
|
||||
state: {environmentChangeRequest?.state}
|
||||
</Typography>
|
||||
<hr />
|
||||
<SuggestedChangeset
|
||||
suggestedChange={environmentChangeset}
|
||||
<ChangeRequest
|
||||
changeRequest={environmentChangeRequest}
|
||||
onNavigate={() => {
|
||||
onClose();
|
||||
}}
|
||||
onRefetch={refetchSuggestedChanges}
|
||||
onRefetch={refetchChangeRequest}
|
||||
/>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
environmentChangeset?.state === 'APPROVED'
|
||||
environmentChangeRequest?.state ===
|
||||
'APPROVED'
|
||||
}
|
||||
show={<Typography>Applied</Typography>}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
environmentChangeset?.state === 'CLOSED'
|
||||
environmentChangeRequest?.state === 'CLOSED'
|
||||
}
|
||||
show={<Typography>Applied</Typography>}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
environmentChangeset?.state === 'APPROVED'
|
||||
environmentChangeRequest?.state ===
|
||||
'APPROVED'
|
||||
}
|
||||
show={
|
||||
<>
|
||||
@ -171,7 +173,7 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
environmentChangeset?.state === 'Draft'
|
||||
environmentChangeRequest?.state === 'Draft'
|
||||
}
|
||||
show={
|
||||
<>
|
||||
@ -180,7 +182,7 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
onReview(
|
||||
environmentChangeset.id
|
||||
environmentChangeRequest.id
|
||||
)
|
||||
}
|
||||
>
|
@ -3,8 +3,8 @@ import { Box, Button, Typography } from '@mui/material';
|
||||
import { useStyles as useAppStyles } from 'component/App.styles';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { SuggestedChangesSidebar } from '../SuggestedChangesSidebar/SuggestedChangesSidebar';
|
||||
import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
|
||||
import { ChangeRequestSidebar } from '../ChangeRequestSidebar/ChangeRequestSidebar';
|
||||
import { useChangeRequestDraft } from 'hooks/api/getters/useChangeRequestDraft/useChangeRequestDraft';
|
||||
|
||||
interface IDraftBannerProps {
|
||||
project: string;
|
||||
@ -13,7 +13,7 @@ interface IDraftBannerProps {
|
||||
export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => {
|
||||
const { classes } = useAppStyles();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const { draft, loading } = useSuggestedChangesDraft(project);
|
||||
const { draft, loading } = useChangeRequestDraft(project);
|
||||
const environment = '';
|
||||
|
||||
if ((!loading && !draft) || draft?.length === 0) {
|
||||
@ -69,7 +69,7 @@ export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<SuggestedChangesSidebar
|
||||
<ChangeRequestSidebar
|
||||
project={project}
|
||||
open={isSidebarOpen}
|
||||
onClose={() => {
|
@ -2,7 +2,7 @@ import { ArrowRight } from '@mui/icons-material';
|
||||
import { useTheme } from '@mui/system';
|
||||
import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell';
|
||||
|
||||
export const ChangesetActionCell = () => {
|
||||
export const ChangeRequestActionCell = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TextCell sx={{ textAlign: 'right' }}>
|
@ -4,11 +4,11 @@ import { colors } from 'themes/colors';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { Check, CircleOutlined, Close } from '@mui/icons-material';
|
||||
|
||||
interface IChangesetStatusCellProps {
|
||||
interface IChangeRequestStatusCellProps {
|
||||
value?: string | null;
|
||||
}
|
||||
|
||||
export enum SuggestChangesetState {
|
||||
export enum ChangeRequestState {
|
||||
DRAFT = 'Draft',
|
||||
APPROVED = 'Approved',
|
||||
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,
|
||||
}) => {
|
||||
const renderState = (state: string) => {
|
||||
switch (state) {
|
||||
case SuggestChangesetState.IN_REVIEW:
|
||||
case ChangeRequestState.IN_REVIEW:
|
||||
return (
|
||||
<StyledReviewChip
|
||||
label={'Review required'}
|
||||
icon={<CircleOutlined fontSize={'small'} />}
|
||||
/>
|
||||
);
|
||||
case SuggestChangesetState.APPROVED:
|
||||
case ChangeRequestState.APPROVED:
|
||||
return (
|
||||
<StyledApprovedChip
|
||||
label={'Approved'}
|
||||
icon={<Check fontSize={'small'} />}
|
||||
/>
|
||||
);
|
||||
case SuggestChangesetState.APPLIED:
|
||||
case ChangeRequestState.APPLIED:
|
||||
return (
|
||||
<StyledApprovedChip
|
||||
label={'Applied'}
|
||||
icon={<Check fontSize={'small'} />}
|
||||
/>
|
||||
);
|
||||
case SuggestChangesetState.CANCELLED:
|
||||
case ChangeRequestState.CANCELLED:
|
||||
return (
|
||||
<StyledRejectedChip
|
||||
label={'Cancelled'}
|
||||
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
|
||||
/>
|
||||
);
|
||||
case SuggestChangesetState.REJECTED:
|
||||
case ChangeRequestState.REJECTED:
|
||||
return (
|
||||
<StyledRejectedChip
|
||||
label={'Rejected'}
|
@ -4,7 +4,7 @@ import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/system';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
|
||||
interface IChangesetTitleCellProps {
|
||||
interface IChangeRequestTitleCellProps {
|
||||
value?: any;
|
||||
row: { original: any };
|
||||
}
|
||||
@ -15,14 +15,14 @@ export const StyledLink = styled('div')(({ theme }) => ({
|
||||
margin: 0,
|
||||
}));
|
||||
|
||||
export const ChangesetTitleCell = ({
|
||||
export const ChangeRequestTitleCell = ({
|
||||
value,
|
||||
row: { original },
|
||||
}: IChangesetTitleCellProps) => {
|
||||
}: IChangeRequestTitleCellProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { id, features: changes } = original;
|
||||
const theme = useTheme();
|
||||
const path = `/projects/${projectId}/suggest-changes/${id}`;
|
||||
const path = `/projects/${projectId}/change-requests/${id}`;
|
||||
|
||||
if (!value) {
|
||||
return <TextCell />;
|
||||
@ -37,7 +37,7 @@ export const ChangesetTitleCell = ({
|
||||
to={path}
|
||||
sx={{ pt: 0.2 }}
|
||||
>
|
||||
Suggestion
|
||||
Change request
|
||||
</Link>
|
||||
<Typography
|
||||
component={'span'}
|
@ -19,15 +19,15 @@ import { useSearch } from 'hooks/useSearch';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { TimeAgoCell } from '../../../common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||
import { TextCell } from '../../../common/Table/cells/TextCell/TextCell';
|
||||
import { ChangesetStatusCell } from './ChangesetStatusCell/ChangesetStatusCell';
|
||||
import { ChangesetActionCell } from './ChangesetActionCell/ChangesetActionCell';
|
||||
import { ChangeRequestStatusCell } from './ChangeRequestStatusCell/ChangeRequestStatusCell';
|
||||
import { ChangeRequestActionCell } from './ChangeRequestActionCell/ChangeRequestActionCell';
|
||||
import { AvatarCell } from './AvatarCell/AvatarCell';
|
||||
import { ChangesetTitleCell } from './ChangesetTitleCell/ChangesetTitleCell';
|
||||
import { ChangeRequestTitleCell } from './ChangeRequestTitleCell/ChangeRequestTitleCell';
|
||||
import { TableBody, TableRow } from '../../../common/Table';
|
||||
import { useStyles } from './SuggestionsTabs.styles';
|
||||
import { useStyles } from './ChangeRequestsTabs.styles';
|
||||
|
||||
export interface IChangeSetTableProps {
|
||||
changesets: any[];
|
||||
export interface IChangeRequestTableProps {
|
||||
changeRequests: any[];
|
||||
loading: boolean;
|
||||
storedParams: SortingRule<string>;
|
||||
setStoredParams: (
|
||||
@ -38,13 +38,13 @@ export interface IChangeSetTableProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const SuggestionsTabs = ({
|
||||
changesets = [],
|
||||
export const ChangeRequestsTabs = ({
|
||||
changeRequests = [],
|
||||
loading,
|
||||
storedParams,
|
||||
setStoredParams,
|
||||
projectId,
|
||||
}: IChangeSetTableProps) => {
|
||||
}: IChangeRequestTableProps) => {
|
||||
const { classes } = useStyles();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@ -53,27 +53,29 @@ export const SuggestionsTabs = ({
|
||||
searchParams.get('search') || ''
|
||||
);
|
||||
|
||||
const [openChangesets, closedChangesets] = useMemo(() => {
|
||||
const open = changesets.filter(
|
||||
changeset =>
|
||||
changeset.state !== 'Cancelled' && changeset.state !== 'Applied'
|
||||
const [openChangeRequests, closedChangeRequests] = useMemo(() => {
|
||||
const open = changeRequests.filter(
|
||||
changeRequest =>
|
||||
changeRequest.state !== 'Cancelled' &&
|
||||
changeRequest.state !== 'Applied'
|
||||
);
|
||||
const closed = changesets.filter(
|
||||
changeset =>
|
||||
changeset.state === 'Cancelled' || changeset.state === 'Applied'
|
||||
const closed = changeRequests.filter(
|
||||
changeRequest =>
|
||||
changeRequest.state === 'Cancelled' ||
|
||||
changeRequest.state === 'Applied'
|
||||
);
|
||||
|
||||
return [open, closed];
|
||||
}, [changesets]);
|
||||
}, [changeRequests]);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: 'Suggestions',
|
||||
data: openChangesets,
|
||||
title: 'Change requests',
|
||||
data: openChangeRequests,
|
||||
},
|
||||
{
|
||||
title: 'Closed',
|
||||
data: closedChangesets,
|
||||
data: closedChangeRequests,
|
||||
},
|
||||
];
|
||||
|
||||
@ -87,7 +89,7 @@ export const SuggestionsTabs = ({
|
||||
width: 100,
|
||||
canSort: true,
|
||||
accessor: 'id',
|
||||
Cell: ChangesetTitleCell,
|
||||
Cell: ChangeRequestTitleCell,
|
||||
},
|
||||
{
|
||||
Header: 'By',
|
||||
@ -117,7 +119,7 @@ export const SuggestionsTabs = ({
|
||||
accessor: 'state',
|
||||
minWidth: 150,
|
||||
width: 150,
|
||||
Cell: ChangesetStatusCell,
|
||||
Cell: ChangeRequestStatusCell,
|
||||
sortType: 'text',
|
||||
},
|
||||
{
|
||||
@ -126,7 +128,7 @@ export const SuggestionsTabs = ({
|
||||
minWidth: 50,
|
||||
width: 50,
|
||||
canSort: false,
|
||||
Cell: ChangesetActionCell,
|
||||
Cell: ChangeRequestActionCell,
|
||||
},
|
||||
],
|
||||
//eslint-disable-next-line
|
@ -2,28 +2,28 @@ import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
|
||||
import { SuggestionsTabs } from './SuggestionsTabs/SuggestionsTabs';
|
||||
import { ChangeRequestsTabs } from './ChangeRequestsTabs/ChangeRequestsTabs';
|
||||
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 };
|
||||
|
||||
export const ProjectSuggestedChanges = () => {
|
||||
export const ProjectChangeRequests = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const projectName = useProjectNameOrId(projectId);
|
||||
|
||||
usePageTitle(`Change requests – ${projectName}`);
|
||||
|
||||
const { changesets, loading } = useProjectSuggestedChanges(projectId);
|
||||
const { changeRequests, loading } = useProjectChangeRequests(projectId);
|
||||
|
||||
const { value, setValue } = createLocalStorage(
|
||||
`${projectId}:ProjectSuggestedChanges`,
|
||||
`${projectId}:ProjectChangeRequest`,
|
||||
defaultSort
|
||||
);
|
||||
|
||||
return (
|
||||
<SuggestionsTabs
|
||||
changesets={changesets}
|
||||
<ChangeRequestsTabs
|
||||
changeRequests={changeRequests}
|
||||
storedParams={value}
|
||||
setStoredParams={setValue}
|
||||
projectId={projectId}
|
@ -11,8 +11,8 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useStyles } from './FeatureOverviewEnvSwitch.styles';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useSuggestToggle } from 'hooks/useSuggestToggle';
|
||||
import { SuggestChangesDialogue } from 'component/suggestChanges/SuggestChangeConfirmDialog/SuggestChangeConfirmDialog';
|
||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||
|
||||
interface IFeatureOverviewEnvSwitchProps {
|
||||
env: IFeatureEnvironment;
|
||||
@ -36,11 +36,11 @@ const FeatureOverviewEnvSwitch = ({
|
||||
const { classes: styles } = useStyles();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const {
|
||||
onSuggestToggle,
|
||||
onSuggestToggleClose,
|
||||
onSuggestToggleConfirm,
|
||||
suggestChangesDialogDetails,
|
||||
} = useSuggestToggle(projectId);
|
||||
onChangeRequestToggle,
|
||||
onChangeRequestToggleClose,
|
||||
onChangeRequestToggleConfirm,
|
||||
changeRequestDialogDetails,
|
||||
} = useChangeRequestToggle(projectId);
|
||||
|
||||
const handleToggleEnvironmentOn = async () => {
|
||||
try {
|
||||
@ -84,9 +84,9 @@ const FeatureOverviewEnvSwitch = ({
|
||||
};
|
||||
|
||||
const toggleEnvironment = async (e: React.ChangeEvent) => {
|
||||
if (uiConfig?.flags?.suggestChanges && env.name === 'production') {
|
||||
if (uiConfig?.flags?.changeRequests && env.name === 'production') {
|
||||
e.preventDefault();
|
||||
onSuggestToggle(featureId, env.name, env.enabled);
|
||||
onChangeRequestToggle(featureId, env.name, env.enabled);
|
||||
return;
|
||||
}
|
||||
if (env.enabled) {
|
||||
@ -119,12 +119,12 @@ const FeatureOverviewEnvSwitch = ({
|
||||
/>
|
||||
{content}
|
||||
</label>
|
||||
<SuggestChangesDialogue
|
||||
isOpen={suggestChangesDialogDetails.isOpen}
|
||||
onClose={onSuggestToggleClose}
|
||||
<ChangeRequestDialogue
|
||||
isOpen={changeRequestDialogDetails.isOpen}
|
||||
onClose={onChangeRequestToggleClose}
|
||||
featureName={featureId}
|
||||
environment={suggestChangesDialogDetails?.environment}
|
||||
onConfirm={onSuggestToggleConfirm}
|
||||
environment={changeRequestDialogDetails?.environment}
|
||||
onConfirm={onChangeRequestToggleConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -30,7 +30,7 @@ import StatusChip from 'component/common/StatusChip/StatusChip';
|
||||
import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
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 useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
@ -88,7 +88,7 @@ export const FeatureView = () => {
|
||||
<MainLayout
|
||||
ref={ref}
|
||||
subheader={
|
||||
uiConfig?.flags?.suggestChanges ? (
|
||||
uiConfig?.flags?.changeRequests ? (
|
||||
<DraftBanner project={projectId} />
|
||||
) : null
|
||||
}
|
||||
|
@ -24,10 +24,10 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
|
||||
import { ProjectLog } from './ProjectLog/ProjectLog';
|
||||
import { SuggestedChangeOverview } from 'component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview';
|
||||
import { DraftBanner } from 'component/suggestChanges/DraftBanner/DraftBanner';
|
||||
import { ChangeRequestOverview } from 'component/changeRequest/ChangeRequestOverview/ChangeRequestOverview';
|
||||
import { DraftBanner } from 'component/changeRequest/DraftBanner/DraftBanner';
|
||||
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
|
||||
import { ProjectSuggestedChanges } from '../../suggest-changes/ProjectSuggestions/ProjectSuggestedChanges';
|
||||
import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests';
|
||||
|
||||
const StyledDiv = styled('div')(() => ({
|
||||
display: 'flex',
|
||||
@ -91,8 +91,8 @@ const Project = () => {
|
||||
},
|
||||
{
|
||||
title: 'Change requests',
|
||||
path: `${basePath}/suggest-changes`,
|
||||
name: 'suggest-changes' + '',
|
||||
path: `${basePath}/change-requests`,
|
||||
name: 'change-request' + '',
|
||||
},
|
||||
{
|
||||
title: 'Event log',
|
||||
@ -124,7 +124,7 @@ const Project = () => {
|
||||
<MainLayout
|
||||
ref={ref}
|
||||
subheader={
|
||||
uiConfig?.flags?.suggestChanges ? (
|
||||
!uiConfig?.flags?.changeRequests ? (
|
||||
<DraftBanner project={projectId} />
|
||||
) : null
|
||||
}
|
||||
@ -235,20 +235,20 @@ const Project = () => {
|
||||
<Route path="archive" element={<ProjectFeaturesArchive />} />
|
||||
<Route path="logs" element={<ProjectLog />} />
|
||||
<Route
|
||||
path="suggest-changes"
|
||||
path="change-requests"
|
||||
element={
|
||||
<ConditionallyRender
|
||||
condition={Boolean(uiConfig?.flags?.suggestChanges)}
|
||||
show={<ProjectSuggestedChanges />}
|
||||
condition={Boolean(uiConfig?.flags?.changeRequests)}
|
||||
show={<ProjectChangeRequests />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="suggest-changes/:id"
|
||||
path="change-requests/:id"
|
||||
element={
|
||||
<ConditionallyRender
|
||||
condition={Boolean(uiConfig?.flags?.suggestChanges)}
|
||||
show={<SuggestedChangeOverview />}
|
||||
condition={Boolean(uiConfig?.flags?.changeRequests)}
|
||||
show={<ChangeRequestOverview />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -36,8 +36,8 @@ import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/Feat
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { useSuggestToggle } from 'hooks/useSuggestToggle';
|
||||
import { SuggestChangesDialogue } from 'component/suggestChanges/SuggestChangeConfirmDialog/SuggestChangeConfirmDialog';
|
||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||
|
||||
interface IProjectFeatureTogglesProps {
|
||||
features: IProject['features'];
|
||||
@ -102,11 +102,11 @@ export const ProjectFeatureToggles = ({
|
||||
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||
useFeatureApi();
|
||||
const {
|
||||
onSuggestToggle,
|
||||
onSuggestToggleClose,
|
||||
onSuggestToggleConfirm,
|
||||
suggestChangesDialogDetails,
|
||||
} = useSuggestToggle(projectId);
|
||||
onChangeRequestToggle,
|
||||
onChangeRequestToggleClose,
|
||||
onChangeRequestToggleConfirm,
|
||||
changeRequestDialogDetails,
|
||||
} = useChangeRequestToggle(projectId);
|
||||
|
||||
const onToggle = useCallback(
|
||||
async (
|
||||
@ -116,10 +116,10 @@ export const ProjectFeatureToggles = ({
|
||||
enabled: boolean
|
||||
) => {
|
||||
if (
|
||||
uiConfig?.flags?.suggestChanges &&
|
||||
uiConfig?.flags?.changeRequests &&
|
||||
environment === 'production'
|
||||
) {
|
||||
onSuggestToggle(featureName, environment, enabled);
|
||||
onChangeRequestToggle(featureName, environment, enabled);
|
||||
throw new Error('Additional approval required');
|
||||
}
|
||||
try {
|
||||
@ -517,12 +517,12 @@ export const ProjectFeatureToggles = ({
|
||||
featureId={featureArchiveState || ''}
|
||||
projectId={projectId}
|
||||
/>{' '}
|
||||
<SuggestChangesDialogue
|
||||
isOpen={suggestChangesDialogDetails.isOpen}
|
||||
onClose={onSuggestToggleClose}
|
||||
featureName={suggestChangesDialogDetails?.featureName}
|
||||
environment={suggestChangesDialogDetails?.environment}
|
||||
onConfirm={onSuggestToggleConfirm}
|
||||
<ChangeRequestDialogue
|
||||
isOpen={changeRequestDialogDetails.isOpen}
|
||||
onClose={onChangeRequestToggleClose}
|
||||
featureName={changeRequestDialogDetails?.featureName}
|
||||
environment={changeRequestDialogDetails?.environment}
|
||||
onConfirm={onChangeRequestToggleConfirm}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
interface ISuggestChangeSchema {
|
||||
interface IChangeRequestsSchema {
|
||||
feature: string;
|
||||
action:
|
||||
| 'updateEnabled'
|
||||
@ -10,17 +10,17 @@ interface ISuggestChangeSchema {
|
||||
payload: string | boolean | object | number;
|
||||
}
|
||||
|
||||
export const useSuggestChangeApi = () => {
|
||||
export const useChangeRequestApi = () => {
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const addSuggestion = async (
|
||||
const addChangeRequest = async (
|
||||
project: 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, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
@ -35,10 +35,10 @@ export const useSuggestChangeApi = () => {
|
||||
|
||||
const changeState = async (
|
||||
project: string,
|
||||
suggestChangeId: number,
|
||||
changeRequestId: number,
|
||||
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, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
@ -51,8 +51,8 @@ export const useSuggestChangeApi = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const applyChanges = async (project: string, suggestChangeId: string) => {
|
||||
const path = `api/admin/projects/${project}/suggest-changes/${suggestChangeId}/apply`;
|
||||
const applyChanges = async (project: string, changeRequestId: string) => {
|
||||
const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/apply`;
|
||||
const req = createRequest(path, {
|
||||
method: 'PUT',
|
||||
});
|
||||
@ -64,12 +64,12 @@ export const useSuggestChangeApi = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const discardSuggestions = async (
|
||||
const discardChangeRequestEvent = async (
|
||||
project: string,
|
||||
changesetId: number,
|
||||
changeId: number
|
||||
changeRequestId: 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, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
@ -81,10 +81,10 @@ export const useSuggestChangeApi = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
addSuggestion,
|
||||
addChangeRequest,
|
||||
applyChanges,
|
||||
changeState,
|
||||
discardSuggestions,
|
||||
discardChangeRequestEvent,
|
||||
errors,
|
||||
loading,
|
||||
};
|
@ -2,16 +2,16 @@ import useSWR from 'swr';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
export const useSuggestedChange = (projectId: string, id: string) => {
|
||||
export const useChangeRequest = (projectId: string, id: string) => {
|
||||
const { data, error, mutate } = useSWR(
|
||||
formatApiPath(`api/admin/projects/${projectId}/suggest-changes/${id}`),
|
||||
formatApiPath(`api/admin/projects/${projectId}/change-requests/${id}`),
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading: !error && !data,
|
||||
refetchSuggestedChange: () => mutate(),
|
||||
refetchChangeRequest: () => mutate(),
|
||||
error,
|
||||
};
|
||||
};
|
@ -17,7 +17,7 @@ interface IChange {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISuggestChangesResponse {
|
||||
export interface IChangeRequestResponse {
|
||||
id: number;
|
||||
environment: string;
|
||||
state: string;
|
||||
@ -36,13 +36,13 @@ export interface ISuggestChangesResponse {
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('SuggestedChanges'))
|
||||
.then(handleErrorResponses('ChangeRequest'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
export const useSuggestedChangesDraft = (project: string) => {
|
||||
const { data, error, mutate } = useSWR<ISuggestChangesResponse[]>(
|
||||
formatApiPath(`api/admin/projects/${project}/suggest-changes/draft`),
|
||||
export const useChangeRequestDraft = (project: string) => {
|
||||
const { data, error, mutate } = useSWR<IChangeRequestResponse[]>(
|
||||
formatApiPath(`api/admin/projects/${project}/change-requests/draft`),
|
||||
fetcher
|
||||
);
|
||||
|
@ -5,19 +5,19 @@ import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('SuggestedChanges'))
|
||||
.then(handleErrorResponses('ChangeRequest'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
export const useProjectSuggestedChanges = (project: string) => {
|
||||
export const useProjectChangeRequests = (project: string) => {
|
||||
const { data, error, mutate } = useSWR(
|
||||
formatApiPath(`api/admin/projects/${project}/suggest-changes`),
|
||||
formatApiPath(`api/admin/projects/${project}/change-requests`),
|
||||
fetcher
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
changesets: data,
|
||||
changeRequests: data,
|
||||
loading: !error && !data,
|
||||
refetch: () => mutate(),
|
||||
error,
|
67
frontend/src/hooks/useChangeRequestToggle.ts
Normal file
67
frontend/src/hooks/useChangeRequestToggle.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
export interface ISuggestChangeset {
|
||||
export interface IChangeRequest {
|
||||
id: number;
|
||||
state:
|
||||
| 'CREATED'
|
||||
@ -11,11 +11,10 @@ export interface ISuggestChangeset {
|
||||
environment: string;
|
||||
createdBy?: string;
|
||||
createdAt?: Date;
|
||||
changes?: ISuggestChange[];
|
||||
events?: ISuggestChangeEvent[];
|
||||
changes?: IChangeRequestEvent[];
|
||||
}
|
||||
|
||||
export interface ISuggestChange {
|
||||
export interface IChangeRequestEvent {
|
||||
id: number;
|
||||
action:
|
||||
| 'updateEnabled'
|
||||
@ -28,7 +27,7 @@ export interface ISuggestChange {
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export enum SuggestChangesetEvent {
|
||||
export enum ChangeRequestEvent {
|
||||
CREATED = 'CREATED',
|
||||
UPDATED = 'UPDATED',
|
||||
SUBMITTED = 'SUBMITTED',
|
||||
@ -36,23 +35,3 @@ export enum SuggestChangesetEvent {
|
||||
REJECTED = 'REJECTED',
|
||||
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;
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ export interface IFlags {
|
||||
embedProxyFrontend?: boolean;
|
||||
publicSignup?: boolean;
|
||||
syncSSOGroups?: boolean;
|
||||
suggestChanges?: boolean;
|
||||
changeRequests?: boolean;
|
||||
cloneEnvironment?: boolean;
|
||||
}
|
||||
|
||||
|
@ -69,12 +69,12 @@ exports[`should create default config 1`] = `
|
||||
"ENABLE_DARK_MODE_SUPPORT": false,
|
||||
"anonymiseEventLog": false,
|
||||
"batchMetrics": false,
|
||||
"changeRequests": false,
|
||||
"cloneEnvironment": false,
|
||||
"embedProxy": false,
|
||||
"embedProxyFrontend": false,
|
||||
"publicSignup": false,
|
||||
"responseTimeWithAppName": false,
|
||||
"suggestChanges": false,
|
||||
"syncSSOGroups": false,
|
||||
},
|
||||
},
|
||||
@ -83,12 +83,12 @@ exports[`should create default config 1`] = `
|
||||
"ENABLE_DARK_MODE_SUPPORT": false,
|
||||
"anonymiseEventLog": false,
|
||||
"batchMetrics": false,
|
||||
"changeRequests": false,
|
||||
"cloneEnvironment": false,
|
||||
"embedProxy": false,
|
||||
"embedProxyFrontend": false,
|
||||
"publicSignup": false,
|
||||
"responseTimeWithAppName": false,
|
||||
"suggestChanges": false,
|
||||
"syncSSOGroups": false,
|
||||
},
|
||||
"externalResolver": {
|
||||
|
@ -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_TOKEN_UPDATED = 'public-signup-token-updated';
|
||||
|
||||
export const SUGGEST_CHANGE_CREATED = 'suggest-change-created';
|
||||
|
||||
export interface IBaseEvent {
|
||||
type: string;
|
||||
createdBy: string;
|
||||
|
@ -10,8 +10,8 @@ export const defaultExperimentalOptions = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY,
|
||||
false,
|
||||
),
|
||||
suggestChanges: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_SUGGEST_CHANGES,
|
||||
changeRequests: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_CHANGE_REQUESTS,
|
||||
false,
|
||||
),
|
||||
syncSSOGroups: parseEnvVarBoolean(
|
||||
@ -51,7 +51,7 @@ export interface IExperimentalOptions {
|
||||
batchMetrics?: boolean;
|
||||
anonymiseEventLog?: boolean;
|
||||
syncSSOGroups?: boolean;
|
||||
suggestChanges?: boolean;
|
||||
changeRequests?: boolean;
|
||||
cloneEnvironment?: boolean;
|
||||
};
|
||||
externalResolver: IExternalFlagResolver;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ITagType } from './stores/tag-type-store';
|
||||
import { LogProvider } from '../logger';
|
||||
import { IRole } from './stores/access-store';
|
||||
import User, { IUser } from './user';
|
||||
import { IUser } from './user';
|
||||
import { ALL_OPERATORS } from '../util/constants';
|
||||
|
||||
export type Operator = typeof ALL_OPERATORS[number];
|
||||
@ -372,99 +372,3 @@ export interface IFeatureStrategySegment {
|
||||
featureStrategyId: string;
|
||||
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;
|
||||
}
|
||||
|
@ -37,4 +37,3 @@ export const MOVE_FEATURE_TOGGLE = 'MOVE_FEATURE_TOGGLE';
|
||||
export const CREATE_SEGMENT = 'CREATE_SEGMENT';
|
||||
export const UPDATE_SEGMENT = 'UPDATE_SEGMENT';
|
||||
export const DELETE_SEGMENT = 'DELETE_SEGMENT';
|
||||
export const SUGGEST_CHANGE = 'SUGGEST_CHANGE';
|
||||
|
40
src/migrations/20221901130645-add-change-requests-table.js
Normal file
40
src/migrations/20221901130645-add-change-requests-table.js
Normal 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,
|
||||
);
|
||||
};
|
@ -39,7 +39,7 @@ process.nextTick(async () => {
|
||||
anonymiseEventLog: false,
|
||||
responseTimeWithAppName: true,
|
||||
syncSSOGroups: true,
|
||||
suggestChanges: true,
|
||||
changeRequests: true,
|
||||
cloneEnvironment: true,
|
||||
},
|
||||
},
|
||||
|
@ -28,7 +28,7 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
|
||||
embedProxyFrontend: true,
|
||||
batchMetrics: true,
|
||||
syncSSOGroups: true,
|
||||
suggestChanges: true,
|
||||
changeRequests: true,
|
||||
cloneEnvironment: true,
|
||||
},
|
||||
},
|
||||
|
@ -11,7 +11,7 @@ test('disabled middleware should not block paths that use the same path', async
|
||||
conditionalMiddleware(
|
||||
() => false,
|
||||
(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';
|
||||
|
||||
app.use(
|
||||
`${path}/suggest-changes`,
|
||||
`${path}/change-requests`,
|
||||
conditionalMiddleware(
|
||||
() => false,
|
||||
(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: [] });
|
||||
});
|
||||
|
||||
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 () => {
|
||||
@ -55,7 +55,7 @@ test('should respect ordering of endpoints', async () => {
|
||||
conditionalMiddleware(
|
||||
() => true,
|
||||
(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)
|
||||
.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 () => {
|
||||
@ -74,11 +74,11 @@ test('disabled middleware should not block paths that use the same basepath', as
|
||||
const path = '/api/admin/projects';
|
||||
|
||||
app.use(
|
||||
`${path}/suggest-changes`,
|
||||
`${path}/change-requests`,
|
||||
conditionalMiddleware(
|
||||
() => false,
|
||||
(req, res) => {
|
||||
res.json({ name: 'Suggest changes' });
|
||||
res.json({ name: 'Request changes' });
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1562,7 +1562,7 @@ This event fires when you delete a segment.
|
||||
|
||||
### '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"
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user