mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +02:00
Disable and enable strategies - frontend (#3582)
Signed-off-by: andreas-unleash <andreas@getunleash.ai> Co-authored-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
1e3f652311
commit
3bb09c5ce4
@ -1,6 +1,5 @@
|
|||||||
import { FC, ReactNode } from 'react';
|
import { FC, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
hasNameField,
|
|
||||||
IChange,
|
IChange,
|
||||||
IChangeRequest,
|
IChangeRequest,
|
||||||
IChangeRequestFeature,
|
IChangeRequestFeature,
|
||||||
@ -8,18 +7,8 @@ import {
|
|||||||
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 { Alert, Box, styled } from '@mui/material';
|
import { Alert, Box, styled } from '@mui/material';
|
||||||
|
|
||||||
import {
|
|
||||||
StrategyTooltipLink,
|
|
||||||
StrategyDiff,
|
|
||||||
} from 'component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink';
|
|
||||||
import { StrategyExecution } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
|
||||||
import { ToggleStatusChange } from './ToggleStatusChange';
|
import { ToggleStatusChange } from './ToggleStatusChange';
|
||||||
import {
|
import { StrategyChange } from './StrategyChange';
|
||||||
StrategyAddedChange,
|
|
||||||
StrategyDeletedChange,
|
|
||||||
StrategyEditedChange,
|
|
||||||
} from './StrategyChange';
|
|
||||||
import { VariantPatch } from './VariantPatch/VariantPatch';
|
import { VariantPatch } from './VariantPatch/VariantPatch';
|
||||||
|
|
||||||
const StyledSingleChangeBox = styled(Box, {
|
const StyledSingleChangeBox = styled(Box, {
|
||||||
@ -74,6 +63,7 @@ export const Change: FC<{
|
|||||||
const lastIndex = feature.defaultChange
|
const lastIndex = feature.defaultChange
|
||||||
? feature.changes.length + 1
|
? feature.changes.length + 1
|
||||||
: feature.changes.length;
|
: feature.changes.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSingleChangeBox
|
<StyledSingleChangeBox
|
||||||
key={objectId(change)}
|
key={objectId(change)}
|
||||||
@ -98,50 +88,17 @@ export const Change: FC<{
|
|||||||
discard={discard}
|
discard={discard}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{change.action === 'addStrategy' && (
|
{change.action === 'addStrategy' ||
|
||||||
<>
|
change.action === 'deleteStrategy' ||
|
||||||
<StrategyAddedChange discard={discard}>
|
change.action === 'updateStrategy' ? (
|
||||||
<StrategyTooltipLink change={change}>
|
<StrategyChange
|
||||||
<StrategyDiff
|
discard={discard}
|
||||||
change={change}
|
change={change}
|
||||||
feature={feature.name}
|
featureName={feature.name}
|
||||||
environmentName={changeRequest.environment}
|
environmentName={changeRequest.environment}
|
||||||
project={changeRequest.project}
|
projectId={changeRequest.project}
|
||||||
/>
|
/>
|
||||||
</StrategyTooltipLink>
|
) : null}
|
||||||
</StrategyAddedChange>
|
|
||||||
<StrategyExecution strategy={change.payload} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{change.action === 'deleteStrategy' && (
|
|
||||||
<StrategyDeletedChange discard={discard}>
|
|
||||||
{hasNameField(change.payload) && (
|
|
||||||
<StrategyTooltipLink change={change}>
|
|
||||||
<StrategyDiff
|
|
||||||
change={change}
|
|
||||||
feature={feature.name}
|
|
||||||
environmentName={changeRequest.environment}
|
|
||||||
project={changeRequest.project}
|
|
||||||
/>
|
|
||||||
</StrategyTooltipLink>
|
|
||||||
)}
|
|
||||||
</StrategyDeletedChange>
|
|
||||||
)}
|
|
||||||
{change.action === 'updateStrategy' && (
|
|
||||||
<>
|
|
||||||
<StrategyEditedChange discard={discard}>
|
|
||||||
<StrategyTooltipLink change={change}>
|
|
||||||
<StrategyDiff
|
|
||||||
change={change}
|
|
||||||
feature={feature.name}
|
|
||||||
environmentName={changeRequest.environment}
|
|
||||||
project={changeRequest.project}
|
|
||||||
/>
|
|
||||||
</StrategyTooltipLink>
|
|
||||||
</StrategyEditedChange>
|
|
||||||
<StrategyExecution strategy={change.payload} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{change.action === 'patchVariant' && (
|
{change.action === 'patchVariant' && (
|
||||||
<VariantPatch
|
<VariantPatch
|
||||||
feature={feature.name}
|
feature={feature.name}
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
import { Box, styled, Typography } from '@mui/material';
|
import { VFC, FC, ReactNode } from 'react';
|
||||||
import { FC, ReactNode } from 'react';
|
import { Box, styled, Tooltip, Typography } from '@mui/material';
|
||||||
|
import BlockIcon from '@mui/icons-material/Block';
|
||||||
|
import TrackChangesIcon from '@mui/icons-material/TrackChanges';
|
||||||
|
import {
|
||||||
|
StrategyDiff,
|
||||||
|
StrategyTooltipLink,
|
||||||
|
} from '../../StrategyTooltipLink/StrategyTooltipLink';
|
||||||
|
import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||||
|
import {
|
||||||
|
IChangeRequestAddStrategy,
|
||||||
|
IChangeRequestDeleteStrategy,
|
||||||
|
IChangeRequestUpdateStrategy,
|
||||||
|
} from 'component/changeRequest/changeRequest.types';
|
||||||
|
import { useCurrentStrategy } from './hooks/useCurrentStrategy';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { flexRow } from 'themes/themeStyles';
|
||||||
|
|
||||||
export const ChangeItemWrapper = styled(Box)({
|
export const ChangeItemWrapper = styled(Box)({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -7,7 +23,7 @@ export const ChangeItemWrapper = styled(Box)({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
|
const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -20,55 +36,175 @@ const ChangeItemInfo: FC = styled(Box)(({ theme }) => ({
|
|||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StrategyAddedChange: FC<{ discard?: ReactNode }> = ({
|
const hasNameField = (payload: unknown): payload is { name: string } =>
|
||||||
children,
|
typeof payload === 'object' && payload !== null && 'name' in payload;
|
||||||
discard,
|
|
||||||
}) => {
|
const DisabledEnabledState: VFC<{ disabled: boolean }> = ({ disabled }) => {
|
||||||
|
if (disabled) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title="This strategy will not be taken into account when evaluating feature toggle."
|
||||||
|
arrow
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Badge color="disabled" icon={<BlockIcon />}>
|
||||||
|
Disabled
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangeItemCreateEditWrapper>
|
<Tooltip
|
||||||
<ChangeItemInfo>
|
title="This was disabled before and with this change it will be taken into account when evaluating feature toggle."
|
||||||
<Typography
|
arrow
|
||||||
sx={theme => ({
|
sx={{ cursor: 'pointer' }}
|
||||||
color: theme.palette.success.dark,
|
>
|
||||||
})}
|
<Badge color="success" icon={<TrackChangesIcon />}>
|
||||||
>
|
Enabled
|
||||||
+ Adding strategy:
|
</Badge>
|
||||||
</Typography>
|
</Tooltip>
|
||||||
{children}
|
|
||||||
</ChangeItemInfo>
|
|
||||||
{discard}
|
|
||||||
</ChangeItemCreateEditWrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StrategyEditedChange: FC<{ discard?: ReactNode }> = ({
|
const EditHeader: VFC<{
|
||||||
children,
|
wasDisabled?: boolean;
|
||||||
discard,
|
willBeDisabled?: boolean;
|
||||||
}) => {
|
}> = ({ wasDisabled = false, willBeDisabled = false }) => {
|
||||||
return (
|
if (wasDisabled && willBeDisabled) {
|
||||||
<ChangeItemCreateEditWrapper>
|
return (
|
||||||
<ChangeItemInfo>
|
<Typography color="action.disabled">
|
||||||
<Typography>Editing strategy:</Typography>
|
Editing disabled strategy
|
||||||
{children}
|
</Typography>
|
||||||
</ChangeItemInfo>
|
);
|
||||||
{discard}
|
}
|
||||||
</ChangeItemCreateEditWrapper>
|
|
||||||
);
|
if (!wasDisabled && willBeDisabled) {
|
||||||
|
return <Typography color="error.dark">Editing strategy</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasDisabled && !willBeDisabled) {
|
||||||
|
return <Typography color="success.dark">Editing strategy</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Typography>Editing strategy:</Typography>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StrategyDeletedChange: FC<{ discard?: ReactNode }> = ({
|
export const StrategyChange: VFC<{
|
||||||
discard,
|
discard?: ReactNode;
|
||||||
children,
|
change:
|
||||||
}) => {
|
| IChangeRequestAddStrategy
|
||||||
|
| IChangeRequestDeleteStrategy
|
||||||
|
| IChangeRequestUpdateStrategy;
|
||||||
|
environmentName: string;
|
||||||
|
featureName: string;
|
||||||
|
projectId: string;
|
||||||
|
}> = ({ discard, change, featureName, environmentName, projectId }) => {
|
||||||
|
const currentStrategy = useCurrentStrategy(
|
||||||
|
change,
|
||||||
|
projectId,
|
||||||
|
featureName,
|
||||||
|
environmentName
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangeItemWrapper>
|
<>
|
||||||
<ChangeItemInfo>
|
{change.action === 'addStrategy' && (
|
||||||
<Typography sx={theme => ({ color: theme.palette.error.main })}>
|
<>
|
||||||
- Deleting strategy
|
<ChangeItemCreateEditWrapper>
|
||||||
</Typography>
|
<ChangeItemInfo>
|
||||||
{children}
|
<Typography
|
||||||
</ChangeItemInfo>
|
color={
|
||||||
{discard}
|
change.payload?.disabled
|
||||||
</ChangeItemWrapper>
|
? 'action.disabled'
|
||||||
|
: 'success.dark'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Adding strategy:
|
||||||
|
</Typography>
|
||||||
|
<StrategyTooltipLink change={change}>
|
||||||
|
<StrategyDiff
|
||||||
|
change={change}
|
||||||
|
currentStrategy={currentStrategy}
|
||||||
|
/>
|
||||||
|
</StrategyTooltipLink>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(
|
||||||
|
change.payload?.disabled === true
|
||||||
|
)}
|
||||||
|
show={<DisabledEnabledState disabled={true} />}
|
||||||
|
/>
|
||||||
|
</ChangeItemInfo>
|
||||||
|
{discard}
|
||||||
|
</ChangeItemCreateEditWrapper>
|
||||||
|
<StrategyExecution strategy={change.payload} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{change.action === 'deleteStrategy' && (
|
||||||
|
<ChangeItemWrapper>
|
||||||
|
<ChangeItemInfo>
|
||||||
|
<Typography
|
||||||
|
sx={theme => ({ color: theme.palette.error.main })}
|
||||||
|
>
|
||||||
|
- Deleting strategy
|
||||||
|
</Typography>
|
||||||
|
{hasNameField(change.payload) && (
|
||||||
|
<StrategyTooltipLink change={change}>
|
||||||
|
<StrategyDiff
|
||||||
|
change={change}
|
||||||
|
currentStrategy={currentStrategy}
|
||||||
|
/>
|
||||||
|
</StrategyTooltipLink>
|
||||||
|
)}
|
||||||
|
</ChangeItemInfo>
|
||||||
|
{discard}
|
||||||
|
</ChangeItemWrapper>
|
||||||
|
)}
|
||||||
|
{change.action === 'updateStrategy' && (
|
||||||
|
<>
|
||||||
|
<ChangeItemCreateEditWrapper>
|
||||||
|
<ChangeItemInfo>
|
||||||
|
<EditHeader
|
||||||
|
wasDisabled={currentStrategy?.disabled}
|
||||||
|
willBeDisabled={change.payload?.disabled}
|
||||||
|
/>
|
||||||
|
<StrategyTooltipLink
|
||||||
|
change={change}
|
||||||
|
previousTitle={currentStrategy?.title}
|
||||||
|
>
|
||||||
|
<StrategyDiff
|
||||||
|
change={change}
|
||||||
|
currentStrategy={currentStrategy}
|
||||||
|
/>
|
||||||
|
</StrategyTooltipLink>
|
||||||
|
</ChangeItemInfo>
|
||||||
|
{discard}
|
||||||
|
</ChangeItemCreateEditWrapper>
|
||||||
|
<StrategyExecution strategy={change.payload} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
change.payload?.disabled !==
|
||||||
|
currentStrategy?.disabled
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
marginTop: theme => theme.spacing(2),
|
||||||
|
paddingLeft: theme => theme.spacing(3),
|
||||||
|
paddingRight: theme => theme.spacing(3),
|
||||||
|
...flexRow,
|
||||||
|
gap: theme => theme.spacing(1),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
This strategy will be{' '}
|
||||||
|
<DisabledEnabledState
|
||||||
|
disabled={change.payload?.disabled || false}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
IChangeRequestAddStrategy,
|
||||||
|
IChangeRequestDeleteStrategy,
|
||||||
|
IChangeRequestUpdateStrategy,
|
||||||
|
} from 'component/changeRequest/changeRequest.types';
|
||||||
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
|
|
||||||
|
export const useCurrentStrategy = (
|
||||||
|
change:
|
||||||
|
| IChangeRequestAddStrategy
|
||||||
|
| IChangeRequestUpdateStrategy
|
||||||
|
| IChangeRequestDeleteStrategy,
|
||||||
|
project: string,
|
||||||
|
feature: string,
|
||||||
|
environmentName: string
|
||||||
|
) => {
|
||||||
|
const currentFeature = useFeature(project, feature);
|
||||||
|
const currentStrategy = currentFeature.feature?.environments
|
||||||
|
.find(environment => environment.name === environmentName)
|
||||||
|
?.strategies.find(
|
||||||
|
strategy =>
|
||||||
|
'id' in change.payload && strategy.id === change.payload.id
|
||||||
|
);
|
||||||
|
return currentStrategy;
|
||||||
|
};
|
@ -8,11 +8,13 @@ import {
|
|||||||
formatStrategyName,
|
formatStrategyName,
|
||||||
GetFeatureStrategyIcon,
|
GetFeatureStrategyIcon,
|
||||||
} from 'utils/strategyNames';
|
} from 'utils/strategyNames';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
|
||||||
import EventDiff from 'component/events/EventDiff/EventDiff';
|
import EventDiff from 'component/events/EventDiff/EventDiff';
|
||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||||
import { styled } from '@mui/material';
|
import { Typography, styled } from '@mui/material';
|
||||||
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { textTruncated } from 'themes/themeStyles';
|
||||||
|
|
||||||
const StyledCodeSection = styled('div')(({ theme }) => ({
|
const StyledCodeSection = styled('div')(({ theme }) => ({
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
@ -25,41 +27,13 @@ const StyledCodeSection = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const useCurrentStrategy = (
|
|
||||||
change:
|
|
||||||
| IChangeRequestAddStrategy
|
|
||||||
| IChangeRequestUpdateStrategy
|
|
||||||
| IChangeRequestDeleteStrategy,
|
|
||||||
project: string,
|
|
||||||
feature: string,
|
|
||||||
environmentName: string
|
|
||||||
) => {
|
|
||||||
const currentFeature = useFeature(project, feature);
|
|
||||||
const currentStrategy = currentFeature.feature?.environments
|
|
||||||
.find(environment => environment.name === environmentName)
|
|
||||||
?.strategies.find(
|
|
||||||
strategy =>
|
|
||||||
'id' in change.payload && strategy.id === change.payload.id
|
|
||||||
);
|
|
||||||
return currentStrategy;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StrategyDiff: FC<{
|
export const StrategyDiff: FC<{
|
||||||
change:
|
change:
|
||||||
| IChangeRequestAddStrategy
|
| IChangeRequestAddStrategy
|
||||||
| IChangeRequestUpdateStrategy
|
| IChangeRequestUpdateStrategy
|
||||||
| IChangeRequestDeleteStrategy;
|
| IChangeRequestDeleteStrategy;
|
||||||
project: string;
|
currentStrategy?: IFeatureStrategy;
|
||||||
feature: string;
|
}> = ({ change, currentStrategy }) => {
|
||||||
environmentName: string;
|
|
||||||
}> = ({ change, project, feature, environmentName }) => {
|
|
||||||
const currentStrategy = useCurrentStrategy(
|
|
||||||
change,
|
|
||||||
project,
|
|
||||||
feature,
|
|
||||||
environmentName
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeRequestStrategy =
|
const changeRequestStrategy =
|
||||||
change.action === 'deleteStrategy' ? undefined : change.payload;
|
change.action === 'deleteStrategy' ? undefined : change.payload;
|
||||||
|
|
||||||
@ -79,14 +53,35 @@ interface IStrategyTooltipLinkProps {
|
|||||||
| IChangeRequestAddStrategy
|
| IChangeRequestAddStrategy
|
||||||
| IChangeRequestUpdateStrategy
|
| IChangeRequestUpdateStrategy
|
||||||
| IChangeRequestDeleteStrategy;
|
| IChangeRequestDeleteStrategy;
|
||||||
|
previousTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({
|
export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({
|
||||||
change,
|
change,
|
||||||
|
previousTitle,
|
||||||
children,
|
children,
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
<GetFeatureStrategyIcon strategyName={change.payload.name} />
|
<GetFeatureStrategyIcon strategyName={change.payload.name} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(
|
||||||
|
previousTitle && previousTitle !== change.payload.title
|
||||||
|
)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
component="s"
|
||||||
|
color="action.disabled"
|
||||||
|
sx={{
|
||||||
|
...textTruncated,
|
||||||
|
maxWidth: '100px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{previousTitle}
|
||||||
|
</Typography>{' '}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipLink
|
<TooltipLink
|
||||||
tooltip={children}
|
tooltip={children}
|
||||||
tooltipProps={{
|
tooltipProps={{
|
||||||
@ -94,7 +89,20 @@ export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({
|
|||||||
maxHeight: 600,
|
maxHeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formatStrategyName(change.payload.name)}
|
<Typography
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
...textTruncated,
|
||||||
|
maxWidth:
|
||||||
|
previousTitle === change.payload.title
|
||||||
|
? '300px'
|
||||||
|
: '200px',
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{change.payload.title ||
|
||||||
|
formatStrategyName(change.payload.name)}
|
||||||
|
</Typography>
|
||||||
</TooltipLink>
|
</TooltipLink>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -106,7 +106,7 @@ type ChangeRequestEnabled = { enabled: boolean };
|
|||||||
|
|
||||||
type ChangeRequestAddStrategy = Pick<
|
type ChangeRequestAddStrategy = Pick<
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
'parameters' | 'constraints' | 'segments'
|
'parameters' | 'constraints' | 'segments' | 'title' | 'disabled'
|
||||||
> & { name: string };
|
> & { name: string };
|
||||||
|
|
||||||
type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string };
|
type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string };
|
||||||
@ -114,6 +114,8 @@ type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string };
|
|||||||
type ChangeRequestDeleteStrategy = {
|
type ChangeRequestDeleteStrategy = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChangeRequestAction =
|
export type ChangeRequestAction =
|
||||||
@ -122,6 +124,3 @@ export type ChangeRequestAction =
|
|||||||
| 'updateStrategy'
|
| 'updateStrategy'
|
||||||
| 'deleteStrategy'
|
| 'deleteStrategy'
|
||||||
| 'patchVariant';
|
| 'patchVariant';
|
||||||
|
|
||||||
export const hasNameField = (payload: unknown): payload is { name: string } =>
|
|
||||||
typeof payload === 'object' && payload !== null && 'name' in payload;
|
|
||||||
|
@ -9,7 +9,14 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
type Color = 'info' | 'success' | 'warning' | 'error' | 'secondary' | 'neutral';
|
type Color =
|
||||||
|
| 'info'
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| 'error'
|
||||||
|
| 'secondary'
|
||||||
|
| 'neutral'
|
||||||
|
| 'disabled'; // TODO: refactor theme
|
||||||
|
|
||||||
interface IBadgeProps {
|
interface IBadgeProps {
|
||||||
as?: React.ElementType;
|
as?: React.ElementType;
|
||||||
@ -37,16 +44,27 @@ const StyledBadge = styled('div')<IBadgeProps>(
|
|||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
fontWeight: theme.fontWeight.bold,
|
fontWeight: theme.fontWeight.bold,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
backgroundColor: theme.palette[color].light,
|
...(color === 'disabled'
|
||||||
color: theme.palette[color].contrastText,
|
? {
|
||||||
border: `1px solid ${theme.palette[color].border}`,
|
color: theme.palette.text.secondary,
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
backgroundColor: theme.palette[color].light,
|
||||||
|
color: theme.palette[color].contrastText,
|
||||||
|
border: `1px solid ${theme.palette[color].border}`,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const StyledBadgeIcon = styled('div')<IBadgeIconProps>(
|
const StyledBadgeIcon = styled('div')<IBadgeIconProps>(
|
||||||
({ theme, color = 'neutral', iconRight = false }) => ({
|
({ theme, color = 'neutral', iconRight = false }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
color: theme.palette[color].main,
|
color:
|
||||||
|
color === 'disabled'
|
||||||
|
? theme.palette.action.disabled
|
||||||
|
: theme.palette[color].main,
|
||||||
margin: iconRight
|
margin: iconRight
|
||||||
? theme.spacing(0, 0, 0, 0.5)
|
? theme.spacing(0, 0, 0, 0.5)
|
||||||
: theme.spacing(0, 0.5, 0, 0),
|
: theme.spacing(0, 0.5, 0, 0),
|
||||||
|
@ -29,7 +29,7 @@ interface IConstraintAccordionViewProps {
|
|||||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: 'transparent',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
'&:before': {
|
'&:before': {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DragEventHandler, FC, ReactNode } from 'react';
|
import { DragEventHandler, FC, ReactNode } from 'react';
|
||||||
import { DragIndicator } from '@mui/icons-material';
|
import { DragIndicator } from '@mui/icons-material';
|
||||||
import { styled, IconButton, Box } from '@mui/material';
|
import { styled, IconButton, Box, Chip } from '@mui/material';
|
||||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
import {
|
import {
|
||||||
getFeatureStrategyIcon,
|
getFeatureStrategyIcon,
|
||||||
@ -10,6 +10,7 @@ import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PlaygroundStrategySchema } from 'openapi';
|
import { PlaygroundStrategySchema } from 'openapi';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { Badge } from '../Badge/Badge';
|
||||||
|
|
||||||
interface IStrategyItemContainerProps {
|
interface IStrategyItemContainerProps {
|
||||||
strategy: IFeatureStrategy | PlaygroundStrategySchema;
|
strategy: IFeatureStrategy | PlaygroundStrategySchema;
|
||||||
@ -39,26 +40,35 @@ const StyledIndexLabel = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
const StyledContainer = styled(Box, {
|
||||||
|
shouldForwardProp: prop => prop !== 'disabled',
|
||||||
|
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
'& + &': {
|
'& + &': {
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
},
|
},
|
||||||
background: theme.palette.background.paper,
|
background: disabled
|
||||||
|
? theme.palette.envAccordion.disabled
|
||||||
|
: theme.palette.background.paper,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeader = styled('div', {
|
const StyledHeader = styled('div', {
|
||||||
shouldForwardProp: prop => prop !== 'draggable',
|
shouldForwardProp: prop => prop !== 'draggable' && prop !== 'disabled',
|
||||||
})(({ theme, draggable }) => ({
|
})<{ draggable: boolean; disabled: boolean }>(
|
||||||
padding: theme.spacing(0.5, 2),
|
({ theme, draggable, disabled }) => ({
|
||||||
display: 'flex',
|
padding: theme.spacing(0.5, 2),
|
||||||
gap: theme.spacing(1),
|
display: 'flex',
|
||||||
alignItems: 'center',
|
gap: theme.spacing(1),
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
alignItems: 'center',
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2),
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
}));
|
paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2),
|
||||||
|
color: disabled
|
||||||
|
? theme.palette.action.disabled
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
|
export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
|
||||||
strategy,
|
strategy,
|
||||||
@ -78,8 +88,14 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
|
|||||||
condition={orderNumber !== undefined}
|
condition={orderNumber !== undefined}
|
||||||
show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>}
|
show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>}
|
||||||
/>
|
/>
|
||||||
<StyledContainer style={style}>
|
<StyledContainer
|
||||||
<StyledHeader draggable={Boolean(onDragStart)}>
|
disabled={strategy?.disabled || false}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<StyledHeader
|
||||||
|
draggable={Boolean(onDragStart)}
|
||||||
|
disabled={Boolean(strategy?.disabled)}
|
||||||
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(onDragStart)}
|
condition={Boolean(onDragStart)}
|
||||||
show={() => (
|
show={() => (
|
||||||
@ -113,6 +129,14 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
|
|||||||
: strategy.name
|
: strategy.name
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(strategy?.disabled)}
|
||||||
|
show={() => (
|
||||||
|
<>
|
||||||
|
<Badge color="disabled">Disabled</Badge>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
@ -29,6 +29,52 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh
|
|||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
|
||||||
|
const useTitleTracking = () => {
|
||||||
|
const [previousTitle, setPreviousTitle] = useState<string>('');
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
|
const trackTitle = (title: string = '') => {
|
||||||
|
// don't expose the title, just if it was added, removed, or edited
|
||||||
|
if (title === previousTitle) {
|
||||||
|
trackEvent('strategyTitle', {
|
||||||
|
props: {
|
||||||
|
action: 'none',
|
||||||
|
on: 'edit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (previousTitle === '' && title !== '') {
|
||||||
|
trackEvent('strategyTitle', {
|
||||||
|
props: {
|
||||||
|
action: 'added',
|
||||||
|
on: 'edit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (previousTitle !== '' && title === '') {
|
||||||
|
trackEvent('strategyTitle', {
|
||||||
|
props: {
|
||||||
|
action: 'removed',
|
||||||
|
on: 'edit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (previousTitle !== '' && title !== '' && title !== previousTitle) {
|
||||||
|
trackEvent('strategyTitle', {
|
||||||
|
props: {
|
||||||
|
action: 'edited',
|
||||||
|
on: 'edit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
setPreviousTitle,
|
||||||
|
trackTitle,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const FeatureStrategyEdit = () => {
|
export const FeatureStrategyEdit = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
@ -48,7 +94,7 @@ export const FeatureStrategyEdit = () => {
|
|||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
const { refetch: refetchChangeRequests } =
|
const { refetch: refetchChangeRequests } =
|
||||||
usePendingChangeRequests(projectId);
|
usePendingChangeRequests(projectId);
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { setPreviousTitle, trackTitle } = useTitleTracking();
|
||||||
|
|
||||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
@ -87,6 +133,7 @@ export const FeatureStrategyEdit = () => {
|
|||||||
.flatMap(environment => environment.strategies)
|
.flatMap(environment => environment.strategies)
|
||||||
.find(strategy => strategy.id === strategyId);
|
.find(strategy => strategy.id === strategyId);
|
||||||
setStrategy(prev => ({ ...prev, ...savedStrategy }));
|
setStrategy(prev => ({ ...prev, ...savedStrategy }));
|
||||||
|
setPreviousTitle(savedStrategy?.title || '');
|
||||||
}, [strategyId, data]);
|
}, [strategyId, data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -106,12 +153,10 @@ export const FeatureStrategyEdit = () => {
|
|||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
|
|
||||||
trackEvent('strategyTitle', {
|
if (uiConfig?.flags?.strategyTitle) {
|
||||||
props: {
|
// NOTE: remove tracking when feature flag is removed
|
||||||
hasTitle: Boolean(strategy.title),
|
trackTitle(strategy.title);
|
||||||
on: 'edit',
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetchSavedStrategySegments();
|
await refetchSavedStrategySegments();
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -202,6 +247,7 @@ export const createStrategyPayload = (
|
|||||||
constraints: strategy.constraints ?? [],
|
constraints: strategy.constraints ?? [],
|
||||||
parameters: strategy.parameters ?? {},
|
parameters: strategy.parameters ?? {},
|
||||||
segments: segments.map(segment => segment.id),
|
segments: segments.map(segment => segment.id),
|
||||||
|
disabled: strategy.disabled ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const formatFeaturePath = (
|
export const formatFeaturePath = (
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { FormControlLabel, Switch } from '@mui/material';
|
||||||
|
import { VFC } from 'react';
|
||||||
|
|
||||||
|
interface IFeatureStrategyEnabledDisabledProps {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggleEnabled: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureStrategyEnabledDisabled: VFC<
|
||||||
|
IFeatureStrategyEnabledDisabledProps
|
||||||
|
> = ({ enabled, onToggleEnabled }) => {
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
name="enabled"
|
||||||
|
onChange={onToggleEnabled}
|
||||||
|
checked={enabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enabled – This strategy will be used when evaluating feature toggles."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -30,6 +30,7 @@ import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewW
|
|||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
|
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
|
||||||
import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle';
|
import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle';
|
||||||
|
import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled';
|
||||||
|
|
||||||
interface IFeatureStrategyFormProps {
|
interface IFeatureStrategyFormProps {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
@ -250,6 +251,16 @@ export const FeatureStrategyForm = ({
|
|||||||
hasAccess={access}
|
hasAccess={access}
|
||||||
/>
|
/>
|
||||||
<StyledHr />
|
<StyledHr />
|
||||||
|
<FeatureStrategyEnabledDisabled
|
||||||
|
enabled={!strategy?.disabled}
|
||||||
|
onToggleEnabled={() =>
|
||||||
|
setStrategy(strategyState => ({
|
||||||
|
...strategyState,
|
||||||
|
disabled: !strategyState.disabled,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledHr />
|
||||||
<StyledButtons>
|
<StyledButtons>
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
permission={permission}
|
permission={permission}
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
import { VFC, useState } from 'react';
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
import BlockIcon from '@mui/icons-material/Block';
|
||||||
|
import TrackChangesIcon from '@mui/icons-material/TrackChanges';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { UPDATE_FEATURE_STRATEGY } from '@server/types/permissions';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import { useEnableDisable } from './hooks/useEnableDisable';
|
||||||
|
import { useSuggestEnableDisable } from './hooks/useSuggestEnableDisable';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { FeatureStrategyChangeRequestAlert } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyChangeRequestAlert/FeatureStrategyChangeRequestAlert';
|
||||||
|
import { IDisableEnableStrategyProps } from './IDisableEnableStrategyProps';
|
||||||
|
|
||||||
|
const DisableStrategy: VFC<IDisableEnableStrategyProps> = ({ ...props }) => {
|
||||||
|
const { projectId, environmentId } = props;
|
||||||
|
const [isDialogueOpen, setDialogueOpen] = useState(false);
|
||||||
|
const { onDisable } = useEnableDisable({ ...props });
|
||||||
|
const { onSuggestDisable } = useSuggestEnableDisable({ ...props });
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const isChangeRequest = isChangeRequestConfigured(environmentId);
|
||||||
|
|
||||||
|
const onClick = (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isChangeRequest) {
|
||||||
|
onSuggestDisable();
|
||||||
|
} else {
|
||||||
|
onDisable();
|
||||||
|
}
|
||||||
|
setDialogueOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PermissionIconButton
|
||||||
|
onClick={() => setDialogueOpen(true)}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
permission={UPDATE_FEATURE_STRATEGY}
|
||||||
|
tooltipProps={{
|
||||||
|
title: 'Disable strategy',
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<BlockIcon />
|
||||||
|
</PermissionIconButton>
|
||||||
|
<Dialogue
|
||||||
|
title={
|
||||||
|
isChangeRequest
|
||||||
|
? 'Add disabling strategy to change request?'
|
||||||
|
: 'Are you sure you want to disable this strategy?'
|
||||||
|
}
|
||||||
|
open={isDialogueOpen}
|
||||||
|
primaryButtonText={
|
||||||
|
isChangeRequest ? 'Add to draft' : 'Disable strategy'
|
||||||
|
}
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClick={onClick}
|
||||||
|
onClose={() => setDialogueOpen(false)}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isChangeRequest}
|
||||||
|
show={
|
||||||
|
<FeatureStrategyChangeRequestAlert
|
||||||
|
environment={environmentId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Alert severity="error">
|
||||||
|
Disabling the strategy will change which users
|
||||||
|
receive access to the feature.
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Dialogue>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EnableStrategy: VFC<IDisableEnableStrategyProps> = ({ ...props }) => {
|
||||||
|
const { projectId, environmentId } = props;
|
||||||
|
const [isDialogueOpen, setDialogueOpen] = useState(false);
|
||||||
|
const { onEnable } = useEnableDisable({ ...props });
|
||||||
|
const { onSuggestEnable } = useSuggestEnableDisable({ ...props });
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const isChangeRequest = isChangeRequestConfigured(environmentId);
|
||||||
|
|
||||||
|
const onClick = (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isChangeRequest) {
|
||||||
|
onSuggestEnable();
|
||||||
|
} else {
|
||||||
|
onEnable();
|
||||||
|
}
|
||||||
|
setDialogueOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PermissionIconButton
|
||||||
|
onClick={() => setDialogueOpen(true)}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
permission={UPDATE_FEATURE_STRATEGY}
|
||||||
|
tooltipProps={{
|
||||||
|
title: 'Enable strategy',
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<TrackChangesIcon />
|
||||||
|
</PermissionIconButton>
|
||||||
|
<Dialogue
|
||||||
|
title={
|
||||||
|
isChangeRequest
|
||||||
|
? 'Add enabling strategy to change request?'
|
||||||
|
: 'Are you sure you want to enable this strategy?'
|
||||||
|
}
|
||||||
|
open={isDialogueOpen}
|
||||||
|
primaryButtonText={
|
||||||
|
isChangeRequest ? 'Add to draft' : 'Enable strategy'
|
||||||
|
}
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClick={onClick}
|
||||||
|
onClose={() => setDialogueOpen(false)}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isChangeRequest}
|
||||||
|
show={
|
||||||
|
<FeatureStrategyChangeRequestAlert
|
||||||
|
environment={environmentId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Alert severity="error">
|
||||||
|
Enabling the strategy will change which users
|
||||||
|
receive access to the feature.
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Dialogue>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisableEnableStrategy: VFC<IDisableEnableStrategyProps> = ({
|
||||||
|
...props
|
||||||
|
}) =>
|
||||||
|
props.strategy.disabled ? (
|
||||||
|
<EnableStrategy {...props} />
|
||||||
|
) : (
|
||||||
|
<DisableStrategy {...props} />
|
||||||
|
);
|
@ -0,0 +1,8 @@
|
|||||||
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
|
||||||
|
export interface IDisableEnableStrategyProps {
|
||||||
|
projectId: string;
|
||||||
|
featureId: string;
|
||||||
|
environmentId: string;
|
||||||
|
strategy: IFeatureStrategy;
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { IDisableEnableStrategyProps } from '../IDisableEnableStrategyProps';
|
||||||
|
|
||||||
|
export const useEnableDisable = ({
|
||||||
|
projectId,
|
||||||
|
environmentId,
|
||||||
|
featureId,
|
||||||
|
strategy,
|
||||||
|
}: IDisableEnableStrategyProps) => {
|
||||||
|
const { refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
const { setStrategyDisabledState } = useFeatureStrategyApi();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
|
const onEnableDisable = (enabled: boolean) => async () => {
|
||||||
|
try {
|
||||||
|
await setStrategyDisabledState(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentId,
|
||||||
|
strategy.id,
|
||||||
|
!enabled
|
||||||
|
);
|
||||||
|
setToastData({
|
||||||
|
title: `Strategy ${enabled ? 'enabled' : 'disabled'}`,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
refetchFeature();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDisable: onEnableDisable(false),
|
||||||
|
onEnable: onEnableDisable(true),
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { IDisableEnableStrategyProps } from '../IDisableEnableStrategyProps';
|
||||||
|
|
||||||
|
export const useSuggestEnableDisable = ({
|
||||||
|
projectId,
|
||||||
|
environmentId,
|
||||||
|
featureId,
|
||||||
|
strategy,
|
||||||
|
}: IDisableEnableStrategyProps) => {
|
||||||
|
const { addChange } = useChangeRequestApi();
|
||||||
|
const { refetch: refetchChangeRequests } =
|
||||||
|
usePendingChangeRequests(projectId);
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const onSuggestEnableDisable = (enabled: boolean) => async () => {
|
||||||
|
try {
|
||||||
|
await addChange(projectId, environmentId, {
|
||||||
|
action: 'updateStrategy',
|
||||||
|
feature: featureId,
|
||||||
|
payload: {
|
||||||
|
...strategy,
|
||||||
|
disabled: !enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setToastData({
|
||||||
|
title: 'Changes added to the draft!',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
await refetchChangeRequests();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
onSuggestDisable: onSuggestEnableDisable(false),
|
||||||
|
onSuggestEnable: onSuggestEnableDisable(true),
|
||||||
|
};
|
||||||
|
};
|
@ -12,6 +12,8 @@ import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
|
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
|
||||||
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
|
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
|
||||||
|
import { DisableEnableStrategy } from './DisableEnableStrategy/DisableEnableStrategy';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
interface IStrategyItemProps {
|
interface IStrategyItemProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
@ -32,6 +34,7 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
|
|||||||
orderNumber,
|
orderNumber,
|
||||||
headerChildren,
|
headerChildren,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
|
|
||||||
@ -76,6 +79,17 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
|
|||||||
>
|
>
|
||||||
<Edit />
|
<Edit />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(uiConfig?.flags?.strategyDisable)}
|
||||||
|
show={() => (
|
||||||
|
<DisableEnableStrategy
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
strategy={strategy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FeatureStrategyRemove
|
<FeatureStrategyRemove
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
|
@ -111,7 +111,7 @@ export const ChangeRequestTable: VFC = () => {
|
|||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
label: `${key} ${labelText}`,
|
label: `${key} ${labelText}`,
|
||||||
sx: { 'font-size': theme.fontSizes.smallBody },
|
sx: { fontSize: theme.fontSizes.smallBody },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -71,11 +71,37 @@ const useFeatureStrategyApi = () => {
|
|||||||
await makeRequest(req.caller, req.id);
|
await makeRequest(req.caller, req.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setStrategyDisabledState = async (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string,
|
||||||
|
strategyId: string,
|
||||||
|
disabled: boolean
|
||||||
|
): Promise<void> => {
|
||||||
|
const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`;
|
||||||
|
const req = createRequest(
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify([
|
||||||
|
{
|
||||||
|
path: '/disabled',
|
||||||
|
value: disabled,
|
||||||
|
op: 'replace',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
'setStrategyDisabledState'
|
||||||
|
);
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addStrategyToFeature,
|
addStrategyToFeature,
|
||||||
updateStrategyOnFeature,
|
updateStrategyOnFeature,
|
||||||
deleteStrategyFromFeature,
|
deleteStrategyFromFeature,
|
||||||
setStrategiesSortOrder,
|
setStrategiesSortOrder,
|
||||||
|
setStrategyDisabledState,
|
||||||
loading,
|
loading,
|
||||||
errors,
|
errors,
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ export interface IFeatureStrategy {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
segments?: number[];
|
segments?: number[];
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureStrategyParameters {
|
export interface IFeatureStrategyParameters {
|
||||||
@ -24,6 +25,7 @@ export interface IFeatureStrategyPayload {
|
|||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
parameters: IFeatureStrategyParameters;
|
parameters: IFeatureStrategyParameters;
|
||||||
segments?: number[];
|
segments?: number[];
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStrategy {
|
export interface IStrategy {
|
||||||
|
@ -51,6 +51,7 @@ export interface IFlags {
|
|||||||
demo?: boolean;
|
demo?: boolean;
|
||||||
strategyTitle?: boolean;
|
strategyTitle?: boolean;
|
||||||
groupRootRoles?: boolean;
|
groupRootRoles?: boolean;
|
||||||
|
strategyDisable?: boolean;
|
||||||
googleAuthEnabled?: boolean;
|
googleAuthEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ export interface CreateFeatureStrategySchema {
|
|||||||
name: string;
|
name: string;
|
||||||
/** A descriptive title for the strategy */
|
/** A descriptive title for the strategy */
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
/** A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs */
|
||||||
|
disabled?: boolean | null;
|
||||||
/** The order of the strategy in the list */
|
/** The order of the strategy in the list */
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
/** A list of the constraints attached to the strategy */
|
/** A list of the constraints attached to the strategy */
|
||||||
|
@ -16,6 +16,8 @@ export interface FeatureStrategySchema {
|
|||||||
name: string;
|
name: string;
|
||||||
/** A descriptive title for the strategy */
|
/** A descriptive title for the strategy */
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
/** A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs */
|
||||||
|
disabled?: boolean | null;
|
||||||
/** The name or feature the strategy is attached to */
|
/** The name or feature the strategy is attached to */
|
||||||
featureName?: string;
|
featureName?: string;
|
||||||
/** The order of the strategy in the list */
|
/** The order of the strategy in the list */
|
||||||
|
@ -10,6 +10,8 @@ export interface GroupSchema {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
mappingsSSO?: string[];
|
mappingsSSO?: string[];
|
||||||
|
/** A role id that is used as the root role for all users in this group. This can be either the id of the Editor or Admin role. */
|
||||||
|
rootRole?: number | null;
|
||||||
createdBy?: string | null;
|
createdBy?: string | null;
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
users?: GroupUserModelSchema[];
|
users?: GroupUserModelSchema[];
|
||||||
|
@ -17,6 +17,8 @@ export interface PlaygroundStrategySchema {
|
|||||||
id: string;
|
id: string;
|
||||||
/** The strategy's evaluation result. If the strategy is a custom strategy that Unleash can't evaluate, `evaluationStatus` will be `unknown`. Otherwise, it will be `true` or `false` */
|
/** The strategy's evaluation result. If the strategy is a custom strategy that Unleash can't evaluate, `evaluationStatus` will be `unknown`. Otherwise, it will be `true` or `false` */
|
||||||
result: PlaygroundStrategySchemaResult;
|
result: PlaygroundStrategySchemaResult;
|
||||||
|
/** The strategy's status. Disabled strategies are not evaluated */
|
||||||
|
disabled: boolean | null;
|
||||||
/** The strategy's segments and their evaluation results. */
|
/** The strategy's segments and their evaluation results. */
|
||||||
segments: PlaygroundSegmentSchema[];
|
segments: PlaygroundSegmentSchema[];
|
||||||
/** The strategy's constraints and their evaluation results. */
|
/** The strategy's constraints and their evaluation results. */
|
||||||
|
@ -15,6 +15,10 @@ import type { EnvironmentSchema } from './environmentSchema';
|
|||||||
import type { SegmentSchema } from './segmentSchema';
|
import type { SegmentSchema } from './segmentSchema';
|
||||||
import type { FeatureStrategySegmentSchema } from './featureStrategySegmentSchema';
|
import type { FeatureStrategySegmentSchema } from './featureStrategySegmentSchema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the application used by export/import APIs which are deprecated in favor of the more fine grained /api/admin/export and /api/admin/import APIs
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export interface StateSchema {
|
export interface StateSchema {
|
||||||
version: number;
|
version: number;
|
||||||
features?: FeatureSchema[];
|
features?: FeatureSchema[];
|
||||||
|
@ -10,5 +10,9 @@ export interface UpdateFeatureStrategySchema {
|
|||||||
name?: string;
|
name?: string;
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
constraints?: ConstraintSchema[];
|
constraints?: ConstraintSchema[];
|
||||||
|
/** A descriptive title for the strategy */
|
||||||
|
title?: string | null;
|
||||||
|
/** A toggle to disable the strategy. defaults to true. Disabled strategies are not evaluated or returned to the SDKs */
|
||||||
|
disabled?: boolean | null;
|
||||||
parameters?: ParametersSchema;
|
parameters?: ParametersSchema;
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,7 @@ exports[`should create default config 1`] = `
|
|||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"projectScopedStickiness": false,
|
"projectScopedStickiness": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
|
"strategyDisable": false,
|
||||||
"strategyTitle": false,
|
"strategyTitle": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
},
|
},
|
||||||
@ -114,6 +115,7 @@ exports[`should create default config 1`] = `
|
|||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"projectScopedStickiness": false,
|
"projectScopedStickiness": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
|
"strategyDisable": false,
|
||||||
"strategyTitle": false,
|
"strategyTitle": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
},
|
},
|
||||||
|
@ -80,6 +80,10 @@ const flags = {
|
|||||||
process.env.UNLEASH_STRATEGY_TITLE,
|
process.env.UNLEASH_STRATEGY_TITLE,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
strategyDisable: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_STRATEGY_DISABLE,
|
||||||
|
false,
|
||||||
|
),
|
||||||
googleAuthEnabled: parseEnvVarBoolean(
|
googleAuthEnabled: parseEnvVarBoolean(
|
||||||
process.env.GOOGLE_AUTH_ENABLED,
|
process.env.GOOGLE_AUTH_ENABLED,
|
||||||
false,
|
false,
|
||||||
|
Loading…
Reference in New Issue
Block a user