mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-12 01:17:04 +02:00
Frontend - Suggest change copy strategy (#2312)
* Suggest change copy strategy * Fix merge conflicts * Copy strategies from other environment added to draft * Copy strategies from other environment added to draft * Copy strategies from other environment added to draft * Copy strategies from other environment added to draft * fmt * PR comments * PR comments * PR comments * PR comments * Fix: Conditionally hide Change Requests tab
This commit is contained in:
parent
a267f13a7d
commit
c1e0bd83b0
@ -7,7 +7,20 @@ import { ToggleStatusChange } from '../ChangeRequestOverview/ChangeRequestFeatur
|
|||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import type { IChangeRequest } from '../changeRequest.types';
|
import type {
|
||||||
|
IChangeRequest,
|
||||||
|
IChangeRequestAddStrategy,
|
||||||
|
} from '../changeRequest.types';
|
||||||
|
import {
|
||||||
|
StrategyAddedChange,
|
||||||
|
StrategyDeletedChange,
|
||||||
|
StrategyEditedChange,
|
||||||
|
} from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/StrategyChange';
|
||||||
|
import {
|
||||||
|
formatStrategyName,
|
||||||
|
GetFeatureStrategyIcon,
|
||||||
|
} from '../../../utils/strategyNames';
|
||||||
|
import { IChangeRequestEnabled } from '../changeRequest.types';
|
||||||
|
|
||||||
interface IChangeRequestProps {
|
interface IChangeRequestProps {
|
||||||
changeRequest: IChangeRequest;
|
changeRequest: IChangeRequest;
|
||||||
@ -54,21 +67,29 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
|
|||||||
condition={change.action === 'updateEnabled'}
|
condition={change.action === 'updateEnabled'}
|
||||||
show={
|
show={
|
||||||
<ToggleStatusChange
|
<ToggleStatusChange
|
||||||
// @ts-expect-error TODO: fix types
|
enabled={
|
||||||
enabled={change?.payload?.enabled}
|
(change as IChangeRequestEnabled)
|
||||||
onDiscard={onDiscard(change.id)}
|
?.payload?.enabled
|
||||||
|
}
|
||||||
|
onDiscard={onDiscard(change.id!)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* <ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={change.action === 'addStrategy'}
|
condition={change.action === 'addStrategy'}
|
||||||
show={
|
show={
|
||||||
<StrategyAddedChange>
|
<StrategyAddedChange>
|
||||||
<GetFeatureStrategyIcon
|
<GetFeatureStrategyIcon
|
||||||
strategyName={change.payload.name}
|
strategyName={
|
||||||
|
(
|
||||||
|
change as IChangeRequestAddStrategy
|
||||||
|
)?.payload.name!
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{formatStrategyName(
|
{formatStrategyName(
|
||||||
change.payload.name
|
(
|
||||||
|
change as IChangeRequestAddStrategy
|
||||||
|
)?.payload.name!
|
||||||
)}
|
)}
|
||||||
</StrategyAddedChange>
|
</StrategyAddedChange>
|
||||||
}
|
}
|
||||||
@ -80,7 +101,7 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={change.action === 'updateStrategy'}
|
condition={change.action === 'updateStrategy'}
|
||||||
show={<StrategyEditedChange />}
|
show={<StrategyEditedChange />}
|
||||||
/> */}
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</ChangeRequestFeatureToggleChange>
|
</ChangeRequestFeatureToggleChange>
|
||||||
|
@ -6,38 +6,39 @@ interface IChangeRequestDialogueProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
featureName?: string;
|
|
||||||
environment?: string;
|
environment?: string;
|
||||||
enabled?: boolean;
|
showBanner?: boolean;
|
||||||
|
messageComponent: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangeRequestDialogue: FC<IChangeRequestDialogueProps> = ({
|
export const ChangeRequestDialogue: FC<IChangeRequestDialogueProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onClose,
|
onClose,
|
||||||
enabled,
|
showBanner,
|
||||||
featureName,
|
|
||||||
environment,
|
environment,
|
||||||
|
messageComponent,
|
||||||
}) => (
|
}) => (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
primaryButtonText="Add to draft"
|
primaryButtonText="Add suggestion to draft"
|
||||||
secondaryButtonText="Cancel"
|
secondaryButtonText="Cancel"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Request changes"
|
title="Request changes"
|
||||||
|
fullWidth
|
||||||
>
|
>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
{showBanner && (
|
||||||
Change requests is enabled for {environment}. Your changes needs to
|
<Alert severity="info" sx={{ mb: 2 }}>
|
||||||
be approved before they will be live. All the changes you do now
|
Change requests feature is enabled for {environment}. Your
|
||||||
will be added into a draft that you can submit for review.
|
changes needs to be approved before they will be live. All the
|
||||||
</Alert>
|
changes you do now will be added into a draft that you can
|
||||||
|
submit for review.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Change requests:
|
Your suggestion:
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
<strong>{enabled ? 'Disable' : 'Enable'}</strong> feature toggle{' '}
|
|
||||||
<strong>{featureName}</strong> in <strong>{environment}</strong>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{messageComponent}
|
||||||
</Dialogue>
|
</Dialogue>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import { styled, Typography } from '@mui/material';
|
||||||
|
import { formatStrategyName } from '../../../../utils/strategyNames';
|
||||||
|
import { IFeatureStrategy } from '../../../../interfaces/strategy';
|
||||||
|
import { CopyStrategyMsg } from './CopyStrategyMessage';
|
||||||
|
|
||||||
|
const MsgContainer = styled('div')(({ theme }) => ({
|
||||||
|
'&>*:nth-child(n)': {
|
||||||
|
margin: theme.spacing(1, 0),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CopyStrategiesMessage = ({
|
||||||
|
payload,
|
||||||
|
fromEnvironment,
|
||||||
|
environment,
|
||||||
|
}: CopyStrategyMsg) => (
|
||||||
|
<MsgContainer>
|
||||||
|
<Typography>
|
||||||
|
<strong>Copy: </strong>
|
||||||
|
</Typography>
|
||||||
|
{(payload as IFeatureStrategy[])?.map(strategy => (
|
||||||
|
<Typography>
|
||||||
|
<strong>
|
||||||
|
{formatStrategyName((strategy as IFeatureStrategy)?.name)}{' '}
|
||||||
|
strategy{' '}
|
||||||
|
</strong>{' '}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
<Typography>
|
||||||
|
from {fromEnvironment} to {environment}
|
||||||
|
</Typography>
|
||||||
|
</MsgContainer>
|
||||||
|
);
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import { formatStrategyName } from '../../../../utils/strategyNames';
|
||||||
|
import { IFeatureStrategy } from '../../../../interfaces/strategy';
|
||||||
|
|
||||||
|
export interface CopyStrategyMsg {
|
||||||
|
payload: IFeatureStrategy | IFeatureStrategy[];
|
||||||
|
fromEnvironment?: string;
|
||||||
|
environment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopyStrategyMessage = ({
|
||||||
|
payload,
|
||||||
|
fromEnvironment,
|
||||||
|
environment,
|
||||||
|
}: CopyStrategyMsg) => (
|
||||||
|
<Typography>
|
||||||
|
<strong>
|
||||||
|
Copy {formatStrategyName((payload as IFeatureStrategy)?.name)}{' '}
|
||||||
|
strategy{' '}
|
||||||
|
</strong>{' '}
|
||||||
|
from {fromEnvironment} to {environment}
|
||||||
|
</Typography>
|
||||||
|
);
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Typography } from '@mui/material';
|
||||||
|
|
||||||
|
interface UpdateEnabledMsg {
|
||||||
|
enabled: boolean;
|
||||||
|
featureName: string;
|
||||||
|
environment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateEnabledMessage = ({
|
||||||
|
enabled,
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
}: UpdateEnabledMsg) => (
|
||||||
|
<Typography>
|
||||||
|
<strong>{enabled ? 'Disable' : 'Enable'}</strong> feature toggle{' '}
|
||||||
|
<strong>{featureName}</strong> in <strong>{environment}</strong>
|
||||||
|
</Typography>
|
||||||
|
);
|
@ -1,3 +1,31 @@
|
|||||||
|
import { IFeatureStrategy } from '../../interfaces/strategy';
|
||||||
|
import { IUser } from '../../interfaces/user';
|
||||||
|
|
||||||
|
export interface IChangeRequest {
|
||||||
|
id: number;
|
||||||
|
state: ChangeRequestState;
|
||||||
|
project: string;
|
||||||
|
environment: string;
|
||||||
|
createdBy: Pick<IUser, 'id' | 'username' | 'imageUrl'>;
|
||||||
|
createdAt: Date;
|
||||||
|
features: IChangeRequestFeature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChangeRequestFeature {
|
||||||
|
name: string;
|
||||||
|
conflict?: string;
|
||||||
|
changes: IChangeRequestEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChangeRequestBase {
|
||||||
|
id?: number;
|
||||||
|
action: ChangeRequestAction;
|
||||||
|
payload: ChangeRequestPayload;
|
||||||
|
conflict?: string;
|
||||||
|
createdBy?: Pick<IUser, 'id' | 'username' | 'imageUrl'>;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export type ChangeRequestState =
|
export type ChangeRequestState =
|
||||||
| 'Draft'
|
| 'Draft'
|
||||||
| 'Approved'
|
| 'Approved'
|
||||||
@ -5,32 +33,53 @@ export type ChangeRequestState =
|
|||||||
| 'Applied'
|
| 'Applied'
|
||||||
| 'Cancelled';
|
| 'Cancelled';
|
||||||
|
|
||||||
export interface IChangeRequest {
|
type ChangeRequestPayload =
|
||||||
id: number;
|
| ChangeRequestEnabled
|
||||||
environment: string;
|
| ChangeRequestAddStrategy
|
||||||
state: ChangeRequestState;
|
| ChangeRequestEditStrategy
|
||||||
project: string;
|
| ChangeRequestDeleteStrategy;
|
||||||
createdBy: ICreatedBy;
|
|
||||||
createdAt: string;
|
export interface IChangeRequestAddStrategy extends IChangeRequestBase {
|
||||||
features: IChangeRequestFeatures[];
|
action: 'addStrategy';
|
||||||
|
payload: ChangeRequestAddStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICreatedBy {
|
export interface IChangeRequestDeleteStrategy extends IChangeRequestBase {
|
||||||
id: number;
|
action: 'deleteStrategy';
|
||||||
username: string;
|
payload: ChangeRequestDeleteStrategy;
|
||||||
imageUrl: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IChangeRequestFeatures {
|
export interface IChangeRequestUpdateStrategy extends IChangeRequestBase {
|
||||||
name: string;
|
action: 'updateStrategy';
|
||||||
changes: IChangeRequestFeatureChanges[];
|
payload: ChangeRequestEditStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IChangeRequestFeatureChanges {
|
export interface IChangeRequestEnabled extends IChangeRequestBase {
|
||||||
id: number;
|
action: 'updateEnabled';
|
||||||
action: string;
|
payload: ChangeRequestEnabled;
|
||||||
payload: unknown;
|
|
||||||
createdAt: string;
|
|
||||||
createdBy: ICreatedBy;
|
|
||||||
warning?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IChangeRequestEvent =
|
||||||
|
| IChangeRequestAddStrategy
|
||||||
|
| IChangeRequestDeleteStrategy
|
||||||
|
| IChangeRequestUpdateStrategy
|
||||||
|
| IChangeRequestEnabled;
|
||||||
|
|
||||||
|
type ChangeRequestEnabled = { enabled: boolean };
|
||||||
|
|
||||||
|
type ChangeRequestAddStrategy = Pick<
|
||||||
|
IFeatureStrategy,
|
||||||
|
'parameters' | 'constraints'
|
||||||
|
> & { name: string };
|
||||||
|
|
||||||
|
type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string };
|
||||||
|
|
||||||
|
type ChangeRequestDeleteStrategy = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChangeRequestAction =
|
||||||
|
| 'updateEnabled'
|
||||||
|
| 'addStrategy'
|
||||||
|
| 'updateStrategy'
|
||||||
|
| 'deleteStrategy';
|
||||||
|
@ -12,8 +12,10 @@ import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmu
|
|||||||
import { getFeatureStrategyIcon } from 'utils/strategyNames';
|
import { getFeatureStrategyIcon } from 'utils/strategyNames';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { CopyButton } from './CopyButton/CopyButton';
|
import { CopyButton } from './CopyButton/CopyButton';
|
||||||
import { useSegments } from '../../../../hooks/api/getters/useSegments/useSegments';
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { IFeatureStrategyPayload } from '../../../../interfaces/strategy';
|
import { useChangeRequestAddStrategy } from '../../../../hooks/useChangeRequestAddStrategy';
|
||||||
|
import { ChangeRequestDialogue } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||||
|
import { CopyStrategiesMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategiesMessage';
|
||||||
|
|
||||||
interface IFeatureStrategyEmptyProps {
|
interface IFeatureStrategyEmptyProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -42,6 +44,16 @@ export const FeatureStrategyEmpty = ({
|
|||||||
environment.strategies.length > 0
|
environment.strategies.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const changeRequestsEnabled = uiConfig?.flags?.changeRequests;
|
||||||
|
|
||||||
|
const {
|
||||||
|
changeRequestDialogDetails,
|
||||||
|
onChangeRequestAddStrategies,
|
||||||
|
onChangeRequestAddStrategiesConfirm,
|
||||||
|
onChangeRequestAddStrategyClose,
|
||||||
|
} = useChangeRequestAddStrategy(projectId, featureId, 'addStrategy');
|
||||||
|
|
||||||
const onAfterAddStrategy = (multiple = false) => {
|
const onAfterAddStrategy = (multiple = false) => {
|
||||||
refetchFeature();
|
refetchFeature();
|
||||||
refetchFeatureImmutable();
|
refetchFeatureImmutable();
|
||||||
@ -61,6 +73,15 @@ export const FeatureStrategyEmpty = ({
|
|||||||
environment => environment.name === fromEnvironmentName
|
environment => environment.name === fromEnvironmentName
|
||||||
)?.strategies || [];
|
)?.strategies || [];
|
||||||
|
|
||||||
|
if (changeRequestsEnabled) {
|
||||||
|
await onChangeRequestAddStrategies(
|
||||||
|
environmentId,
|
||||||
|
strategies,
|
||||||
|
fromEnvironmentName
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
strategies.map(strategy => {
|
strategies.map(strategy => {
|
||||||
@ -118,77 +139,96 @@ export const FeatureStrategyEmpty = ({
|
|||||||
otherAvailableEnvironments && otherAvailableEnvironments.length > 0;
|
otherAvailableEnvironments && otherAvailableEnvironments.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<>
|
||||||
<div className={styles.title}>
|
<ChangeRequestDialogue
|
||||||
You have not defined any strategies yet.
|
isOpen={changeRequestDialogDetails.isOpen}
|
||||||
|
onClose={onChangeRequestAddStrategyClose}
|
||||||
|
environment={changeRequestDialogDetails?.environment}
|
||||||
|
onConfirm={onChangeRequestAddStrategiesConfirm}
|
||||||
|
messageComponent={
|
||||||
|
<CopyStrategiesMessage
|
||||||
|
fromEnvironment={
|
||||||
|
changeRequestDialogDetails.fromEnvironment!
|
||||||
|
}
|
||||||
|
payload={changeRequestDialogDetails.strategies!}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
You have not defined any strategies yet.
|
||||||
|
</div>
|
||||||
|
<p className={styles.description}>
|
||||||
|
Strategies added in this environment will only be executed
|
||||||
|
if the SDK is using an{' '}
|
||||||
|
<Link to="/admin/api">API key configured</Link> for this
|
||||||
|
environment.
|
||||||
|
</p>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
w: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FeatureStrategyMenu
|
||||||
|
label="Add your first strategy"
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
matchWidth={canCopyFromOtherEnvironment}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={canCopyFromOtherEnvironment}
|
||||||
|
show={
|
||||||
|
<CopyButton
|
||||||
|
environmentId={environmentId}
|
||||||
|
environments={otherAvailableEnvironments.map(
|
||||||
|
environment => environment.name
|
||||||
|
)}
|
||||||
|
onClick={onCopyStrategies}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ width: '100%', mt: 3 }}>
|
||||||
|
<SectionSeparator>
|
||||||
|
Or use a strategy template
|
||||||
|
</SectionSeparator>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
width: '100%',
|
||||||
|
gap: 2,
|
||||||
|
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PresetCard
|
||||||
|
title="Standard strategy"
|
||||||
|
Icon={getFeatureStrategyIcon('default')}
|
||||||
|
onClick={onAddSimpleStrategy}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
>
|
||||||
|
The standard strategy is strictly on/off for your entire
|
||||||
|
userbase.
|
||||||
|
</PresetCard>
|
||||||
|
<PresetCard
|
||||||
|
title="Gradual rollout"
|
||||||
|
Icon={getFeatureStrategyIcon('flexibleRollout')}
|
||||||
|
onClick={onAddGradualRolloutStrategy}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
>
|
||||||
|
Roll out to a percentage of your userbase.
|
||||||
|
</PresetCard>
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.description}>
|
</>
|
||||||
Strategies added in this environment will only be executed if
|
|
||||||
the SDK is using an{' '}
|
|
||||||
<Link to="/admin/api">API key configured</Link> for this
|
|
||||||
environment.
|
|
||||||
</p>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
w: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 2,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FeatureStrategyMenu
|
|
||||||
label="Add your first strategy"
|
|
||||||
projectId={projectId}
|
|
||||||
featureId={featureId}
|
|
||||||
environmentId={environmentId}
|
|
||||||
matchWidth={canCopyFromOtherEnvironment}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={canCopyFromOtherEnvironment}
|
|
||||||
show={
|
|
||||||
<CopyButton
|
|
||||||
environmentId={environmentId}
|
|
||||||
environments={otherAvailableEnvironments.map(
|
|
||||||
environment => environment.name
|
|
||||||
)}
|
|
||||||
onClick={onCopyStrategies}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ width: '100%', mt: 3 }}>
|
|
||||||
<SectionSeparator>Or use a strategy template</SectionSeparator>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'grid',
|
|
||||||
width: '100%',
|
|
||||||
gap: 2,
|
|
||||||
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PresetCard
|
|
||||||
title="Standard strategy"
|
|
||||||
Icon={getFeatureStrategyIcon('default')}
|
|
||||||
onClick={onAddSimpleStrategy}
|
|
||||||
projectId={projectId}
|
|
||||||
environmentId={environmentId}
|
|
||||||
>
|
|
||||||
The standard strategy is strictly on/off for your entire
|
|
||||||
userbase.
|
|
||||||
</PresetCard>
|
|
||||||
<PresetCard
|
|
||||||
title="Gradual rollout"
|
|
||||||
Icon={getFeatureStrategyIcon('flexibleRollout')}
|
|
||||||
onClick={onAddGradualRolloutStrategy}
|
|
||||||
projectId={projectId}
|
|
||||||
environmentId={environmentId}
|
|
||||||
>
|
|
||||||
Roll out to a percentage of your userbase.
|
|
||||||
</PresetCard>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,9 +10,9 @@ import React from 'react';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
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 { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||||
|
import { UpdateEnabledMessage } from '../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
|
||||||
interface IFeatureOverviewEnvSwitchProps {
|
interface IFeatureOverviewEnvSwitchProps {
|
||||||
@ -123,9 +123,15 @@ const FeatureOverviewEnvSwitch = ({
|
|||||||
<ChangeRequestDialogue
|
<ChangeRequestDialogue
|
||||||
isOpen={changeRequestDialogDetails.isOpen}
|
isOpen={changeRequestDialogDetails.isOpen}
|
||||||
onClose={onChangeRequestToggleClose}
|
onClose={onChangeRequestToggleClose}
|
||||||
featureName={featureId}
|
|
||||||
environment={changeRequestDialogDetails?.environment}
|
environment={changeRequestDialogDetails?.environment}
|
||||||
onConfirm={onChangeRequestToggleConfirm}
|
onConfirm={onChangeRequestToggleConfirm}
|
||||||
|
messageComponent={
|
||||||
|
<UpdateEnabledMessage
|
||||||
|
enabled={changeRequestDialogDetails?.enabled!}
|
||||||
|
featureName={changeRequestDialogDetails?.featureName!}
|
||||||
|
environment={changeRequestDialogDetails.environment!}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-material';
|
import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-material';
|
||||||
import { IFeatureStrategy, IFeatureStrategyPayload } from 'interfaces/strategy';
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
@ -19,20 +19,24 @@ import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFe
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
|
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useSegments } from '../../../../../../../../../../hooks/api/getters/useSegments/useSegments';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useChangeRequestAddStrategy } from 'hooks/useChangeRequestAddStrategy';
|
||||||
|
import { ChangeRequestDialogue } from '../../../../../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||||
|
import { CopyStrategyMessage } from '../../../../../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
|
||||||
|
|
||||||
interface ICopyStrategyIconMenuProps {
|
interface ICopyStrategyIconMenuProps {
|
||||||
|
environmentId: string;
|
||||||
environments: IFeatureEnvironment['name'][];
|
environments: IFeatureEnvironment['name'][];
|
||||||
strategy: IFeatureStrategy;
|
strategy: IFeatureStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
||||||
|
environmentId,
|
||||||
environments,
|
environments,
|
||||||
strategy,
|
strategy,
|
||||||
}) => {
|
}) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { segments } = useSegments(strategy.id);
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
@ -47,19 +51,40 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const onClick = async (environmentId: string) => {
|
const { uiConfig } = useUiConfig();
|
||||||
|
const changeRequestsEnabled = uiConfig?.flags?.changeRequests;
|
||||||
|
|
||||||
|
const {
|
||||||
|
changeRequestDialogDetails,
|
||||||
|
onChangeRequestAddStrategyClose,
|
||||||
|
onChangeRequestAddStrategy,
|
||||||
|
onChangeRequestAddStrategyConfirm,
|
||||||
|
} = useChangeRequestAddStrategy(projectId, featureId, 'addStrategy');
|
||||||
|
|
||||||
|
const onCopyStrategy = async (environment: string) => {
|
||||||
const { id, ...strategyCopy } = {
|
const { id, ...strategyCopy } = {
|
||||||
...strategy,
|
...strategy,
|
||||||
environment: environmentId,
|
environment,
|
||||||
copyOf: strategy.id,
|
copyOf: strategy.id,
|
||||||
};
|
};
|
||||||
|
if (changeRequestsEnabled) {
|
||||||
|
await onChangeRequestAddStrategy(
|
||||||
|
environment,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
...strategyCopy,
|
||||||
|
},
|
||||||
|
environmentId
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addStrategyToFeature(
|
await addStrategyToFeature(
|
||||||
projectId,
|
projectId,
|
||||||
featureId,
|
featureId,
|
||||||
environmentId,
|
environmentId,
|
||||||
strategyCopy
|
strategy
|
||||||
);
|
);
|
||||||
refetchFeature();
|
refetchFeature();
|
||||||
refetchFeatureImmutable();
|
refetchFeatureImmutable();
|
||||||
@ -80,6 +105,20 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<ChangeRequestDialogue
|
||||||
|
isOpen={changeRequestDialogDetails.isOpen}
|
||||||
|
onClose={onChangeRequestAddStrategyClose}
|
||||||
|
environment={changeRequestDialogDetails?.environment}
|
||||||
|
onConfirm={onChangeRequestAddStrategyConfirm}
|
||||||
|
messageComponent={
|
||||||
|
<CopyStrategyMessage
|
||||||
|
fromEnvironment={
|
||||||
|
changeRequestDialogDetails.fromEnvironment!
|
||||||
|
}
|
||||||
|
payload={changeRequestDialogDetails.strategy!}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={`Copy to another environment${
|
title={`Copy to another environment${
|
||||||
enabled ? '' : ' (Access denied)'
|
enabled ? '' : ' (Access denied)'
|
||||||
@ -128,7 +167,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => onClick(environment)}
|
onClick={() => onCopyStrategy(environment)}
|
||||||
disabled={!access}
|
disabled={!access}
|
||||||
>
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -54,6 +54,7 @@ export const StrategyItem: VFC<IStrategyItemProps> = ({
|
|||||||
)}
|
)}
|
||||||
show={() => (
|
show={() => (
|
||||||
<CopyStrategyIconMenu
|
<CopyStrategyIconMenu
|
||||||
|
environmentId={environmentId}
|
||||||
environments={otherEnvironments as string[]}
|
environments={otherEnvironments as string[]}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
/>
|
/>
|
||||||
|
@ -38,6 +38,8 @@ import { useMediaQuery } from '@mui/material';
|
|||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||||
|
import { CopyStrategyMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
|
||||||
|
import { UpdateEnabledMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
|
||||||
interface IProjectFeatureTogglesProps {
|
interface IProjectFeatureTogglesProps {
|
||||||
@ -524,9 +526,15 @@ export const ProjectFeatureToggles = ({
|
|||||||
<ChangeRequestDialogue
|
<ChangeRequestDialogue
|
||||||
isOpen={changeRequestDialogDetails.isOpen}
|
isOpen={changeRequestDialogDetails.isOpen}
|
||||||
onClose={onChangeRequestToggleClose}
|
onClose={onChangeRequestToggleClose}
|
||||||
featureName={changeRequestDialogDetails?.featureName}
|
|
||||||
environment={changeRequestDialogDetails?.environment}
|
environment={changeRequestDialogDetails?.environment}
|
||||||
onConfirm={onChangeRequestToggleConfirm}
|
onConfirm={onChangeRequestToggleConfirm}
|
||||||
|
messageComponent={
|
||||||
|
<UpdateEnabledMessage
|
||||||
|
featureName={changeRequestDialogDetails.featureName!}
|
||||||
|
enabled={changeRequestDialogDetails.enabled!}
|
||||||
|
environment={changeRequestDialogDetails?.environment!}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
interface IChangeRequestsSchema {
|
export interface IChangeRequestsSchema {
|
||||||
feature: string;
|
feature: string;
|
||||||
action:
|
action:
|
||||||
| 'updateEnabled'
|
| 'updateEnabled'
|
||||||
|
@ -2,10 +2,7 @@ import useSWR from 'swr';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import {
|
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
|
||||||
ChangeRequestState,
|
|
||||||
IChangeRequest,
|
|
||||||
} from 'component/changeRequest/changeRequest.types';
|
|
||||||
|
|
||||||
const fetcher = (path: string) => {
|
const fetcher = (path: string) => {
|
||||||
return fetch(path)
|
return fetch(path)
|
||||||
|
131
frontend/src/hooks/useChangeRequestAddStrategy.ts
Normal file
131
frontend/src/hooks/useChangeRequestAddStrategy.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import {
|
||||||
|
IFeatureStrategy,
|
||||||
|
IFeatureStrategyPayload,
|
||||||
|
} from '../interfaces/strategy';
|
||||||
|
import { useChangeRequestApi } from './api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
|
import { useChangeRequestOpen } from './api/getters/useChangeRequestOpen/useChangeRequestOpen';
|
||||||
|
|
||||||
|
export type ChangeRequestStrategyAction =
|
||||||
|
| 'addStrategy'
|
||||||
|
| 'updateStrategy'
|
||||||
|
| 'deleteStrategy';
|
||||||
|
|
||||||
|
export const useChangeRequestAddStrategy = (
|
||||||
|
project: string,
|
||||||
|
featureName: string,
|
||||||
|
action: ChangeRequestStrategyAction
|
||||||
|
) => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { addChangeRequest } = useChangeRequestApi();
|
||||||
|
const { refetch } = useChangeRequestOpen(project);
|
||||||
|
|
||||||
|
const [changeRequestDialogDetails, setChangeRequestDialogDetails] =
|
||||||
|
useState<{
|
||||||
|
strategy?: IFeatureStrategy;
|
||||||
|
strategies?: IFeatureStrategy[];
|
||||||
|
featureName?: string;
|
||||||
|
environment?: string;
|
||||||
|
fromEnvironment?: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
}>({ isOpen: false });
|
||||||
|
|
||||||
|
const onChangeRequestAddStrategy = useCallback(
|
||||||
|
(
|
||||||
|
environment: string,
|
||||||
|
strategy: IFeatureStrategy,
|
||||||
|
fromEnvironment?: string
|
||||||
|
) => {
|
||||||
|
setChangeRequestDialogDetails({
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
fromEnvironment,
|
||||||
|
strategy,
|
||||||
|
isOpen: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeRequestAddStrategies = useCallback(
|
||||||
|
(
|
||||||
|
environment: string,
|
||||||
|
strategies: IFeatureStrategy[],
|
||||||
|
fromEnvironment: string
|
||||||
|
) => {
|
||||||
|
setChangeRequestDialogDetails({
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
fromEnvironment,
|
||||||
|
strategies,
|
||||||
|
isOpen: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeRequestAddStrategyClose = useCallback(() => {
|
||||||
|
setChangeRequestDialogDetails({ isOpen: false });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onChangeRequestAddStrategyConfirm = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await addChangeRequest(
|
||||||
|
project,
|
||||||
|
changeRequestDialogDetails.environment!,
|
||||||
|
{
|
||||||
|
feature: changeRequestDialogDetails.featureName!,
|
||||||
|
action: action,
|
||||||
|
payload: changeRequestDialogDetails.strategy!,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
setChangeRequestDialogDetails({ isOpen: false });
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Changes added to the draft!',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
setChangeRequestDialogDetails({ isOpen: false });
|
||||||
|
}
|
||||||
|
}, [addChangeRequest]);
|
||||||
|
|
||||||
|
const onChangeRequestAddStrategiesConfirm = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
changeRequestDialogDetails.strategies!.map(strategy => {
|
||||||
|
return addChangeRequest(
|
||||||
|
project,
|
||||||
|
changeRequestDialogDetails.environment!,
|
||||||
|
{
|
||||||
|
feature: changeRequestDialogDetails.featureName!,
|
||||||
|
action: action,
|
||||||
|
payload: strategy,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
setChangeRequestDialogDetails({ isOpen: false });
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Changes added to the draft!',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
setChangeRequestDialogDetails({ isOpen: false });
|
||||||
|
}
|
||||||
|
}, [addChangeRequest]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onChangeRequestAddStrategy,
|
||||||
|
onChangeRequestAddStrategies,
|
||||||
|
onChangeRequestAddStrategyClose,
|
||||||
|
onChangeRequestAddStrategyConfirm,
|
||||||
|
onChangeRequestAddStrategiesConfirm,
|
||||||
|
changeRequestDialogDetails,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user