diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx
index 2de63edfdc..850bcefee8 100644
--- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx
@@ -1,6 +1,5 @@
import { FC, ReactNode } from 'react';
import {
- hasNameField,
IChange,
IChangeRequest,
IChangeRequestFeature,
@@ -8,18 +7,8 @@ import {
import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
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 {
- StrategyAddedChange,
- StrategyDeletedChange,
- StrategyEditedChange,
-} from './StrategyChange';
+import { StrategyChange } from './StrategyChange';
import { VariantPatch } from './VariantPatch/VariantPatch';
const StyledSingleChangeBox = styled(Box, {
@@ -74,6 +63,7 @@ export const Change: FC<{
const lastIndex = feature.defaultChange
? feature.changes.length + 1
: feature.changes.length;
+
return (
)}
- {change.action === 'addStrategy' && (
- <>
-
-
-
-
-
-
- >
- )}
- {change.action === 'deleteStrategy' && (
-
- {hasNameField(change.payload) && (
-
-
-
- )}
-
- )}
- {change.action === 'updateStrategy' && (
- <>
-
-
-
-
-
-
- >
- )}
+ {change.action === 'addStrategy' ||
+ change.action === 'deleteStrategy' ||
+ change.action === 'updateStrategy' ? (
+
+ ) : null}
{change.action === 'patchVariant' && (
({
+const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
@@ -20,55 +36,175 @@ const ChangeItemInfo: FC = styled(Box)(({ theme }) => ({
gap: theme.spacing(1),
}));
-export const StrategyAddedChange: FC<{ discard?: ReactNode }> = ({
- children,
- discard,
-}) => {
+const hasNameField = (payload: unknown): payload is { name: string } =>
+ typeof payload === 'object' && payload !== null && 'name' in payload;
+
+const DisabledEnabledState: VFC<{ disabled: boolean }> = ({ disabled }) => {
+ if (disabled) {
+ return (
+
+ }>
+ Disabled
+
+
+ );
+ }
+
return (
-
-
- ({
- color: theme.palette.success.dark,
- })}
- >
- + Adding strategy:
-
- {children}
-
- {discard}
-
+
+ }>
+ Enabled
+
+
);
};
-export const StrategyEditedChange: FC<{ discard?: ReactNode }> = ({
- children,
- discard,
-}) => {
- return (
-
-
- Editing strategy:
- {children}
-
- {discard}
-
- );
+const EditHeader: VFC<{
+ wasDisabled?: boolean;
+ willBeDisabled?: boolean;
+}> = ({ wasDisabled = false, willBeDisabled = false }) => {
+ if (wasDisabled && willBeDisabled) {
+ return (
+
+ Editing disabled strategy
+
+ );
+ }
+
+ if (!wasDisabled && willBeDisabled) {
+ return Editing strategy;
+ }
+
+ if (wasDisabled && !willBeDisabled) {
+ return Editing strategy;
+ }
+
+ return Editing strategy:;
};
-export const StrategyDeletedChange: FC<{ discard?: ReactNode }> = ({
- discard,
- children,
-}) => {
+export const StrategyChange: VFC<{
+ discard?: ReactNode;
+ change:
+ | IChangeRequestAddStrategy
+ | IChangeRequestDeleteStrategy
+ | IChangeRequestUpdateStrategy;
+ environmentName: string;
+ featureName: string;
+ projectId: string;
+}> = ({ discard, change, featureName, environmentName, projectId }) => {
+ const currentStrategy = useCurrentStrategy(
+ change,
+ projectId,
+ featureName,
+ environmentName
+ );
+
return (
-
-
- ({ color: theme.palette.error.main })}>
- - Deleting strategy
-
- {children}
-
- {discard}
-
+ <>
+ {change.action === 'addStrategy' && (
+ <>
+
+
+
+ + Adding strategy:
+
+
+
+
+ }
+ />
+
+ {discard}
+
+
+ >
+ )}
+ {change.action === 'deleteStrategy' && (
+
+
+ ({ color: theme.palette.error.main })}
+ >
+ - Deleting strategy
+
+ {hasNameField(change.payload) && (
+
+
+
+ )}
+
+ {discard}
+
+ )}
+ {change.action === 'updateStrategy' && (
+ <>
+
+
+
+
+
+
+
+ {discard}
+
+
+ theme.spacing(2),
+ paddingLeft: theme => theme.spacing(3),
+ paddingRight: theme => theme.spacing(3),
+ ...flexRow,
+ gap: theme => theme.spacing(1),
+ }}
+ >
+ This strategy will be{' '}
+
+
+ }
+ />
+ >
+ )}
+ >
);
};
diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/hooks/useCurrentStrategy.ts b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/hooks/useCurrentStrategy.ts
new file mode 100644
index 0000000000..bfb389c901
--- /dev/null
+++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/hooks/useCurrentStrategy.ts
@@ -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;
+};
diff --git a/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx b/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx
index abd73211f9..5c162d7a47 100644
--- a/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx
@@ -8,11 +8,13 @@ import {
formatStrategyName,
GetFeatureStrategyIcon,
} from 'utils/strategyNames';
-import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import EventDiff from 'component/events/EventDiff/EventDiff';
import omit from 'lodash.omit';
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 }) => ({
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<{
change:
| IChangeRequestAddStrategy
| IChangeRequestUpdateStrategy
| IChangeRequestDeleteStrategy;
- project: string;
- feature: string;
- environmentName: string;
-}> = ({ change, project, feature, environmentName }) => {
- const currentStrategy = useCurrentStrategy(
- change,
- project,
- feature,
- environmentName
- );
-
+ currentStrategy?: IFeatureStrategy;
+}> = ({ change, currentStrategy }) => {
const changeRequestStrategy =
change.action === 'deleteStrategy' ? undefined : change.payload;
@@ -79,14 +53,35 @@ interface IStrategyTooltipLinkProps {
| IChangeRequestAddStrategy
| IChangeRequestUpdateStrategy
| IChangeRequestDeleteStrategy;
+ previousTitle?: string;
}
export const StrategyTooltipLink: FC = ({
change,
+ previousTitle,
children,
}) => (
<>
+
+
+ {previousTitle}
+ {' '}
+ >
+ }
+ />
= ({
maxHeight: 600,
}}
>
- {formatStrategyName(change.payload.name)}
+
+ {change.payload.title ||
+ formatStrategyName(change.payload.name)}
+
>
);
diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts
index 0592725028..50d7779eab 100644
--- a/frontend/src/component/changeRequest/changeRequest.types.ts
+++ b/frontend/src/component/changeRequest/changeRequest.types.ts
@@ -106,7 +106,7 @@ type ChangeRequestEnabled = { enabled: boolean };
type ChangeRequestAddStrategy = Pick<
IFeatureStrategy,
- 'parameters' | 'constraints' | 'segments'
+ 'parameters' | 'constraints' | 'segments' | 'title' | 'disabled'
> & { name: string };
type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string };
@@ -114,6 +114,8 @@ type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string };
type ChangeRequestDeleteStrategy = {
id: string;
name: string;
+ title?: string;
+ disabled?: boolean;
};
export type ChangeRequestAction =
@@ -122,6 +124,3 @@ export type ChangeRequestAction =
| 'updateStrategy'
| 'deleteStrategy'
| 'patchVariant';
-
-export const hasNameField = (payload: unknown): payload is { name: string } =>
- typeof payload === 'object' && payload !== null && 'name' in payload;
diff --git a/frontend/src/component/common/Badge/Badge.tsx b/frontend/src/component/common/Badge/Badge.tsx
index a6a16a9da2..5cceb30c46 100644
--- a/frontend/src/component/common/Badge/Badge.tsx
+++ b/frontend/src/component/common/Badge/Badge.tsx
@@ -9,7 +9,14 @@ import React, {
} from 'react';
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 {
as?: React.ElementType;
@@ -37,16 +44,27 @@ const StyledBadge = styled('div')(
fontSize: theme.fontSizes.smallerBody,
fontWeight: theme.fontWeight.bold,
lineHeight: 1,
- backgroundColor: theme.palette[color].light,
- color: theme.palette[color].contrastText,
- border: `1px solid ${theme.palette[color].border}`,
+ ...(color === 'disabled'
+ ? {
+ 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')(
({ theme, color = 'neutral', iconRight = false }) => ({
display: 'flex',
- color: theme.palette[color].main,
+ color:
+ color === 'disabled'
+ ? theme.palette.action.disabled
+ : theme.palette[color].main,
margin: iconRight
? theme.spacing(0, 0, 0, 0.5)
: theme.spacing(0, 0.5, 0, 0),
diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx
index 0c2426021b..e08bf2eae8 100644
--- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx
+++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx
@@ -29,7 +29,7 @@ interface IConstraintAccordionViewProps {
const StyledAccordion = styled(Accordion)(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium,
- backgroundColor: theme.palette.background.paper,
+ backgroundColor: 'transparent',
boxShadow: 'none',
margin: 0,
'&:before': {
diff --git a/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.tsx b/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.tsx
index 280e2e8f71..749c0bcba4 100644
--- a/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.tsx
+++ b/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.tsx
@@ -1,6 +1,6 @@
import { DragEventHandler, FC, ReactNode } from 'react';
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 {
getFeatureStrategyIcon,
@@ -10,6 +10,7 @@ import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PlaygroundStrategySchema } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import { Badge } from '../Badge/Badge';
interface IStrategyItemContainerProps {
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,
border: `1px solid ${theme.palette.divider}`,
'& + &': {
marginTop: theme.spacing(2),
},
- background: theme.palette.background.paper,
+ background: disabled
+ ? theme.palette.envAccordion.disabled
+ : theme.palette.background.paper,
}));
const StyledHeader = styled('div', {
- shouldForwardProp: prop => prop !== 'draggable',
-})(({ theme, draggable }) => ({
- padding: theme.spacing(0.5, 2),
- display: 'flex',
- gap: theme.spacing(1),
- alignItems: 'center',
- borderBottom: `1px solid ${theme.palette.divider}`,
- fontWeight: theme.typography.fontWeightMedium,
- paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2),
-}));
+ shouldForwardProp: prop => prop !== 'draggable' && prop !== 'disabled',
+})<{ draggable: boolean; disabled: boolean }>(
+ ({ theme, draggable, disabled }) => ({
+ padding: theme.spacing(0.5, 2),
+ display: 'flex',
+ gap: theme.spacing(1),
+ alignItems: 'center',
+ borderBottom: `1px solid ${theme.palette.divider}`,
+ 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 = ({
strategy,
@@ -78,8 +88,14 @@ export const StrategyItemContainer: FC = ({
condition={orderNumber !== undefined}
show={{orderNumber}}
/>
-
-
+
+
(
@@ -113,6 +129,14 @@ export const StrategyItemContainer: FC = ({
: strategy.name
)}
/>
+ (
+ <>
+ Disabled
+ >
+ )}
+ />
{
+ const [previousTitle, setPreviousTitle] = useState('');
+ 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 = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@@ -48,7 +94,7 @@ export const FeatureStrategyEdit = () => {
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
- const { trackEvent } = usePlausibleTracker();
+ const { setPreviousTitle, trackTitle } = useTitleTracking();
const { feature, refetchFeature } = useFeature(projectId, featureId);
@@ -87,6 +133,7 @@ export const FeatureStrategyEdit = () => {
.flatMap(environment => environment.strategies)
.find(strategy => strategy.id === strategyId);
setStrategy(prev => ({ ...prev, ...savedStrategy }));
+ setPreviousTitle(savedStrategy?.title || '');
}, [strategyId, data]);
useEffect(() => {
@@ -106,12 +153,10 @@ export const FeatureStrategyEdit = () => {
payload
);
- trackEvent('strategyTitle', {
- props: {
- hasTitle: Boolean(strategy.title),
- on: 'edit',
- },
- });
+ if (uiConfig?.flags?.strategyTitle) {
+ // NOTE: remove tracking when feature flag is removed
+ trackTitle(strategy.title);
+ }
await refetchSavedStrategySegments();
setToastData({
@@ -202,6 +247,7 @@ export const createStrategyPayload = (
constraints: strategy.constraints ?? [],
parameters: strategy.parameters ?? {},
segments: segments.map(segment => segment.id),
+ disabled: strategy.disabled ?? false,
});
export const formatFeaturePath = (
diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled.tsx
new file mode 100644
index 0000000000..7eeefca248
--- /dev/null
+++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled.tsx
@@ -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 (
+
+ }
+ label="Enabled – This strategy will be used when evaluating feature toggles."
+ />
+ );
+};
diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx
index 5d0a7fab1a..96de854ad9 100644
--- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx
+++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx
@@ -30,6 +30,7 @@ import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewW
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle';
+import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled';
interface IFeatureStrategyFormProps {
feature: IFeatureToggle;
@@ -250,6 +251,16 @@ export const FeatureStrategyForm = ({
hasAccess={access}
/>
+
+ setStrategy(strategyState => ({
+ ...strategyState,
+ disabled: !strategyState.disabled,
+ }))
+ }
+ />
+
= ({ ...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 (
+ <>
+ setDialogueOpen(true)}
+ projectId={projectId}
+ environmentId={environmentId}
+ permission={UPDATE_FEATURE_STRATEGY}
+ tooltipProps={{
+ title: 'Disable strategy',
+ }}
+ type="button"
+ >
+
+
+ setDialogueOpen(false)}
+ >
+
+ }
+ elseShow={
+
+ Disabling the strategy will change which users
+ receive access to the feature.
+
+ }
+ />
+
+ >
+ );
+};
+
+const EnableStrategy: VFC = ({ ...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 (
+ <>
+ setDialogueOpen(true)}
+ projectId={projectId}
+ environmentId={environmentId}
+ permission={UPDATE_FEATURE_STRATEGY}
+ tooltipProps={{
+ title: 'Enable strategy',
+ }}
+ type="button"
+ >
+
+
+ setDialogueOpen(false)}
+ >
+
+ }
+ elseShow={
+
+ Enabling the strategy will change which users
+ receive access to the feature.
+
+ }
+ />
+
+ >
+ );
+};
+
+export const DisableEnableStrategy: VFC = ({
+ ...props
+}) =>
+ props.strategy.disabled ? (
+
+ ) : (
+
+ );
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/IDisableEnableStrategyProps.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/IDisableEnableStrategyProps.ts
new file mode 100644
index 0000000000..da21c953f4
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/IDisableEnableStrategyProps.ts
@@ -0,0 +1,8 @@
+import { IFeatureStrategy } from 'interfaces/strategy';
+
+export interface IDisableEnableStrategyProps {
+ projectId: string;
+ featureId: string;
+ environmentId: string;
+ strategy: IFeatureStrategy;
+}
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/hooks/useEnableDisable.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/hooks/useEnableDisable.ts
new file mode 100644
index 0000000000..aa6b9f55e2
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/hooks/useEnableDisable.ts
@@ -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),
+ };
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/hooks/useSuggestEnableDisable.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/hooks/useSuggestEnableDisable.ts
new file mode 100644
index 0000000000..5e6738a513
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/DisableEnableStrategy/hooks/useSuggestEnableDisable.ts
@@ -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),
+ };
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx
index d7dea23e63..a4cbb30a65 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx
@@ -12,6 +12,8 @@ import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
+import { DisableEnableStrategy } from './DisableEnableStrategy/DisableEnableStrategy';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IStrategyItemProps {
environmentId: string;
@@ -32,6 +34,7 @@ export const StrategyItem: FC = ({
orderNumber,
headerChildren,
}) => {
+ const { uiConfig } = useUiConfig();
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@@ -76,6 +79,17 @@ export const StrategyItem: FC = ({
>
+ (
+
+ )}
+ />
{
return {
key,
label: `${key} ${labelText}`,
- sx: { 'font-size': theme.fontSizes.smallBody },
+ sx: { fontSize: theme.fontSizes.smallBody },
};
});
diff --git a/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts
index f22a3a3220..03cff34862 100644
--- a/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts
+++ b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts
@@ -71,11 +71,37 @@ const useFeatureStrategyApi = () => {
await makeRequest(req.caller, req.id);
};
+ const setStrategyDisabledState = async (
+ projectId: string,
+ featureId: string,
+ environmentId: string,
+ strategyId: string,
+ disabled: boolean
+ ): Promise => {
+ 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 {
addStrategyToFeature,
updateStrategyOnFeature,
deleteStrategyFromFeature,
setStrategiesSortOrder,
+ setStrategyDisabledState,
loading,
errors,
};
diff --git a/frontend/src/interfaces/strategy.ts b/frontend/src/interfaces/strategy.ts
index 08b828263f..3109783efe 100644
--- a/frontend/src/interfaces/strategy.ts
+++ b/frontend/src/interfaces/strategy.ts
@@ -11,6 +11,7 @@ export interface IFeatureStrategy {
projectId?: string;
environment?: string;
segments?: number[];
+ disabled?: boolean;
}
export interface IFeatureStrategyParameters {
@@ -24,6 +25,7 @@ export interface IFeatureStrategyPayload {
constraints: IConstraint[];
parameters: IFeatureStrategyParameters;
segments?: number[];
+ disabled?: boolean;
}
export interface IStrategy {
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index e1c21447f8..e4c534557d 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -51,6 +51,7 @@ export interface IFlags {
demo?: boolean;
strategyTitle?: boolean;
groupRootRoles?: boolean;
+ strategyDisable?: boolean;
googleAuthEnabled?: boolean;
}
diff --git a/frontend/src/openapi/models/createFeatureStrategySchema.ts b/frontend/src/openapi/models/createFeatureStrategySchema.ts
index 62673c4779..4eec944145 100644
--- a/frontend/src/openapi/models/createFeatureStrategySchema.ts
+++ b/frontend/src/openapi/models/createFeatureStrategySchema.ts
@@ -11,6 +11,8 @@ export interface CreateFeatureStrategySchema {
name: string;
/** A descriptive title for the strategy */
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 */
sortOrder?: number;
/** A list of the constraints attached to the strategy */
diff --git a/frontend/src/openapi/models/featureStrategySchema.ts b/frontend/src/openapi/models/featureStrategySchema.ts
index e806d49854..ff1b62bddb 100644
--- a/frontend/src/openapi/models/featureStrategySchema.ts
+++ b/frontend/src/openapi/models/featureStrategySchema.ts
@@ -16,6 +16,8 @@ export interface FeatureStrategySchema {
name: string;
/** A descriptive title for the strategy */
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 */
featureName?: string;
/** The order of the strategy in the list */
diff --git a/frontend/src/openapi/models/groupSchema.ts b/frontend/src/openapi/models/groupSchema.ts
index df9f7f4755..af576ae26a 100644
--- a/frontend/src/openapi/models/groupSchema.ts
+++ b/frontend/src/openapi/models/groupSchema.ts
@@ -10,6 +10,8 @@ export interface GroupSchema {
name: string;
description?: string | null;
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;
createdAt?: string | null;
users?: GroupUserModelSchema[];
diff --git a/frontend/src/openapi/models/playgroundStrategySchema.ts b/frontend/src/openapi/models/playgroundStrategySchema.ts
index 1a46fe14db..ea0bcb607f 100644
--- a/frontend/src/openapi/models/playgroundStrategySchema.ts
+++ b/frontend/src/openapi/models/playgroundStrategySchema.ts
@@ -17,6 +17,8 @@ export interface PlaygroundStrategySchema {
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` */
result: PlaygroundStrategySchemaResult;
+ /** The strategy's status. Disabled strategies are not evaluated */
+ disabled: boolean | null;
/** The strategy's segments and their evaluation results. */
segments: PlaygroundSegmentSchema[];
/** The strategy's constraints and their evaluation results. */
diff --git a/frontend/src/openapi/models/stateSchema.ts b/frontend/src/openapi/models/stateSchema.ts
index fa6cc453e9..cb2735b100 100644
--- a/frontend/src/openapi/models/stateSchema.ts
+++ b/frontend/src/openapi/models/stateSchema.ts
@@ -15,6 +15,10 @@ import type { EnvironmentSchema } from './environmentSchema';
import type { SegmentSchema } from './segmentSchema';
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 {
version: number;
features?: FeatureSchema[];
diff --git a/frontend/src/openapi/models/updateFeatureStrategySchema.ts b/frontend/src/openapi/models/updateFeatureStrategySchema.ts
index 670eb4881e..fb8a52c76c 100644
--- a/frontend/src/openapi/models/updateFeatureStrategySchema.ts
+++ b/frontend/src/openapi/models/updateFeatureStrategySchema.ts
@@ -10,5 +10,9 @@ export interface UpdateFeatureStrategySchema {
name?: string;
sortOrder?: number;
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;
}
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index 9801dd9fce..050279a6bf 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -87,6 +87,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false,
"projectScopedStickiness": false,
"responseTimeWithAppNameKillSwitch": false,
+ "strategyDisable": false,
"strategyTitle": false,
"strictSchemaValidation": false,
},
@@ -114,6 +115,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false,
"projectScopedStickiness": false,
"responseTimeWithAppNameKillSwitch": false,
+ "strategyDisable": false,
"strategyTitle": false,
"strictSchemaValidation": false,
},
diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts
index 14431a2357..509712cf14 100644
--- a/src/lib/types/experimental.ts
+++ b/src/lib/types/experimental.ts
@@ -80,6 +80,10 @@ const flags = {
process.env.UNLEASH_STRATEGY_TITLE,
false,
),
+ strategyDisable: parseEnvVarBoolean(
+ process.env.UNLEASH_STRATEGY_DISABLE,
+ false,
+ ),
googleAuthEnabled: parseEnvVarBoolean(
process.env.GOOGLE_AUTH_ENABLED,
false,