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:
parent
da102a3e98
commit
5dd8616c74
@ -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>
|
||||||
);
|
);
|
@ -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{' '}
|
@ -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
|
@ -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>
|
@ -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 }}
|
@ -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 (
|
@ -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}
|
@ -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}
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
@ -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={() => {
|
@ -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' }}>
|
@ -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'}
|
@ -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'}
|
@ -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
|
@ -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}
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 />}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
};
|
};
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
@ -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,
|
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;
|
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;
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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": {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -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';
|
|
||||||
|
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,
|
anonymiseEventLog: false,
|
||||||
responseTimeWithAppName: true,
|
responseTimeWithAppName: true,
|
||||||
syncSSOGroups: true,
|
syncSSOGroups: true,
|
||||||
suggestChanges: true,
|
changeRequests: true,
|
||||||
cloneEnvironment: true,
|
cloneEnvironment: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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' });
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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"
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user