mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: initial setup (#5583)
This PR sets up the feature flag for the new strategy configuration and duplicates the components for the new setup
This commit is contained in:
parent
2322e1149a
commit
ec670450fd
@ -0,0 +1,20 @@
|
|||||||
|
import { formatAddStrategyApiCode } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||||
|
|
||||||
|
test('formatAddStrategyApiCode', () => {
|
||||||
|
expect(
|
||||||
|
formatAddStrategyApiCode(
|
||||||
|
'projectId',
|
||||||
|
'featureId',
|
||||||
|
'environmentId',
|
||||||
|
{ id: 'strategyId' },
|
||||||
|
'unleashUrl',
|
||||||
|
),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"curl --location --request POST 'unleashUrl/api/admin/projects/projectId/features/featureId/environments/environmentId/strategies' \\\\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\\\
|
||||||
|
--header 'Content-Type: application/json' \\\\
|
||||||
|
--data-raw '{
|
||||||
|
\\"id\\": \\"strategyId\\"
|
||||||
|
}'"
|
||||||
|
`);
|
||||||
|
});
|
@ -0,0 +1,249 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
|
||||||
|
import { FeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm';
|
||||||
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { IFeatureStrategy, IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||||
|
import {
|
||||||
|
createStrategyPayload,
|
||||||
|
featureStrategyDocsLink,
|
||||||
|
featureStrategyDocsLinkLabel,
|
||||||
|
featureStrategyHelp,
|
||||||
|
formatFeaturePath,
|
||||||
|
} from '../FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
|
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { ISegment } from 'interfaces/segment';
|
||||||
|
import { formatStrategyName } from 'utils/strategyNames';
|
||||||
|
import { useFormErrors } from 'hooks/useFormErrors';
|
||||||
|
import { createFeatureStrategy } from 'utils/createFeatureStrategy';
|
||||||
|
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
|
||||||
|
import { useCollaborateData } from 'hooks/useCollaborateData';
|
||||||
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
|
import { comparisonModerator } from '../featureStrategy.utils';
|
||||||
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import useQueryParams from 'hooks/useQueryParams';
|
||||||
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
|
import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
|
||||||
|
|
||||||
|
export const NewFeatureStrategyCreate = () => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const featureId = useRequiredPathParam('featureId');
|
||||||
|
const environmentId = useRequiredQueryParam('environmentId');
|
||||||
|
const strategyName = useRequiredQueryParam('strategyName');
|
||||||
|
const { strategy: defaultStrategy, defaultStrategyFallback } =
|
||||||
|
useDefaultStrategy(projectId, environmentId);
|
||||||
|
const shouldUseDefaultStrategy: boolean = JSON.parse(
|
||||||
|
useQueryParams().get('defaultStrategy') || 'false',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { segments: allSegments } = useSegments();
|
||||||
|
const strategySegments = (allSegments || []).filter((segment) => {
|
||||||
|
return defaultStrategy?.segments?.includes(segment.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({});
|
||||||
|
|
||||||
|
const [segments, setSegments] = useState<ISegment[]>(
|
||||||
|
shouldUseDefaultStrategy ? strategySegments : [],
|
||||||
|
);
|
||||||
|
const { strategyDefinition } = useStrategy(strategyName);
|
||||||
|
const errors = useFormErrors();
|
||||||
|
|
||||||
|
const { addStrategyToFeature, loading } = useFeatureStrategyApi();
|
||||||
|
const { addChange } = useChangeRequestApi();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { unleashUrl } = uiConfig;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
const ref = useRef<IFeatureToggle>(feature);
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const { refetch: refetchChangeRequests } =
|
||||||
|
usePendingChangeRequests(projectId);
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
|
const { data, staleDataNotification, forceRefreshCache } =
|
||||||
|
useCollaborateData<IFeatureToggle>(
|
||||||
|
{
|
||||||
|
unleashGetter: useFeature,
|
||||||
|
params: [projectId, featureId],
|
||||||
|
dataKey: 'feature',
|
||||||
|
refetchFunctionKey: 'refetchFeature',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
feature,
|
||||||
|
{
|
||||||
|
afterSubmitAction: refetchFeature,
|
||||||
|
},
|
||||||
|
comparisonModerator,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current.name === '' && feature.name) {
|
||||||
|
forceRefreshCache(feature);
|
||||||
|
ref.current = feature;
|
||||||
|
}
|
||||||
|
}, [feature.name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldUseDefaultStrategy) {
|
||||||
|
const strategyTemplate = defaultStrategy || defaultStrategyFallback;
|
||||||
|
if (strategyTemplate.parameters?.groupId === '' && featureId) {
|
||||||
|
setStrategy({
|
||||||
|
...strategyTemplate,
|
||||||
|
parameters: {
|
||||||
|
...strategyTemplate.parameters,
|
||||||
|
groupId: featureId,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
} else {
|
||||||
|
setStrategy(strategyTemplate as any);
|
||||||
|
}
|
||||||
|
} else if (strategyDefinition) {
|
||||||
|
setStrategy(createFeatureStrategy(featureId, strategyDefinition));
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
featureId,
|
||||||
|
JSON.stringify(strategyDefinition),
|
||||||
|
shouldUseDefaultStrategy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onAddStrategy = async (payload: IFeatureStrategyPayload) => {
|
||||||
|
await addStrategyToFeature(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentId,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
setToastData({
|
||||||
|
title: 'Strategy created',
|
||||||
|
type: 'success',
|
||||||
|
confetti: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStrategyRequestAdd = async (payload: IFeatureStrategyPayload) => {
|
||||||
|
await addChange(projectId, environmentId, {
|
||||||
|
action: 'addStrategy',
|
||||||
|
feature: featureId,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
// FIXME: segments in change requests
|
||||||
|
setToastData({
|
||||||
|
title: 'Strategy added to draft',
|
||||||
|
type: 'success',
|
||||||
|
confetti: true,
|
||||||
|
});
|
||||||
|
refetchChangeRequests();
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = createStrategyPayload(strategy, segments);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
trackEvent('strategyTitle', {
|
||||||
|
props: {
|
||||||
|
hasTitle: Boolean(strategy.title),
|
||||||
|
on: 'create',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isChangeRequestConfigured(environmentId)) {
|
||||||
|
await onStrategyRequestAdd(payload);
|
||||||
|
} else {
|
||||||
|
await onAddStrategy(payload);
|
||||||
|
}
|
||||||
|
refetchFeature();
|
||||||
|
navigate(formatFeaturePath(projectId, featureId));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyFeature = !data || !data.project;
|
||||||
|
|
||||||
|
if (emptyFeature) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTemplate
|
||||||
|
modal
|
||||||
|
title={formatStrategyName(strategyName)}
|
||||||
|
description={featureStrategyHelp}
|
||||||
|
documentationLink={featureStrategyDocsLink}
|
||||||
|
documentationLinkLabel={featureStrategyDocsLinkLabel}
|
||||||
|
formatApiCode={() =>
|
||||||
|
formatAddStrategyApiCode(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentId,
|
||||||
|
payload,
|
||||||
|
unleashUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h1>NEW CREATE FORM</h1>
|
||||||
|
<FeatureStrategyForm
|
||||||
|
projectId={projectId}
|
||||||
|
feature={data}
|
||||||
|
strategy={strategy}
|
||||||
|
setStrategy={setStrategy}
|
||||||
|
segments={segments}
|
||||||
|
setSegments={setSegments}
|
||||||
|
environmentId={environmentId}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
loading={loading}
|
||||||
|
permission={CREATE_FEATURE_STRATEGY}
|
||||||
|
errors={errors}
|
||||||
|
isChangeRequest={isChangeRequestConfigured(environmentId)}
|
||||||
|
/>
|
||||||
|
{staleDataNotification}
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatCreateStrategyPath = (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string,
|
||||||
|
strategyName: string,
|
||||||
|
defaultStrategy: boolean = false,
|
||||||
|
): string => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
environmentId,
|
||||||
|
strategyName,
|
||||||
|
defaultStrategy: String(defaultStrategy),
|
||||||
|
});
|
||||||
|
|
||||||
|
return `/projects/${projectId}/features/${featureId}/strategies/create?${params}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatAddStrategyApiCode = (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string,
|
||||||
|
strategy: Partial<IFeatureStrategy>,
|
||||||
|
unleashUrl?: string,
|
||||||
|
): string => {
|
||||||
|
if (!unleashUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${unleashUrl}/api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies`;
|
||||||
|
const payload = JSON.stringify(strategy, undefined, 2);
|
||||||
|
|
||||||
|
return `curl --location --request POST '${url}' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${payload}'`;
|
||||||
|
};
|
@ -0,0 +1,54 @@
|
|||||||
|
import { formatUpdateStrategyApiCode } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
|
import { IFeatureStrategy, IStrategy } from 'interfaces/strategy';
|
||||||
|
|
||||||
|
test('formatUpdateStrategyApiCode', () => {
|
||||||
|
const strategy: IFeatureStrategy = {
|
||||||
|
id: 'a',
|
||||||
|
name: 'b',
|
||||||
|
parameters: {
|
||||||
|
c: 1,
|
||||||
|
b: 2,
|
||||||
|
a: 3,
|
||||||
|
},
|
||||||
|
constraints: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const strategyDefinition: IStrategy = {
|
||||||
|
name: 'c',
|
||||||
|
displayName: 'd',
|
||||||
|
description: 'e',
|
||||||
|
editable: false,
|
||||||
|
deprecated: false,
|
||||||
|
parameters: [
|
||||||
|
{ name: 'a', description: '', type: '', required: false },
|
||||||
|
{ name: 'b', description: '', type: '', required: false },
|
||||||
|
{ name: 'c', description: '', type: '', required: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
formatUpdateStrategyApiCode(
|
||||||
|
'projectId',
|
||||||
|
'featureId',
|
||||||
|
'environmentId',
|
||||||
|
'strategyId',
|
||||||
|
strategy,
|
||||||
|
strategyDefinition,
|
||||||
|
'unleashUrl',
|
||||||
|
),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"curl --location --request PUT 'unleashUrl/api/admin/projects/projectId/features/featureId/environments/environmentId/strategies/strategyId' \\\\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\\\
|
||||||
|
--header 'Content-Type: application/json' \\\\
|
||||||
|
--data-raw '{
|
||||||
|
\\"id\\": \\"a\\",
|
||||||
|
\\"name\\": \\"b\\",
|
||||||
|
\\"parameters\\": {
|
||||||
|
\\"a\\": 3,
|
||||||
|
\\"b\\": 2,
|
||||||
|
\\"c\\": 1
|
||||||
|
},
|
||||||
|
\\"constraints\\": []
|
||||||
|
}'"
|
||||||
|
`);
|
||||||
|
});
|
@ -0,0 +1,308 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { FeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm';
|
||||||
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import {
|
||||||
|
IFeatureStrategy,
|
||||||
|
IFeatureStrategyPayload,
|
||||||
|
IStrategy,
|
||||||
|
} from 'interfaces/strategy';
|
||||||
|
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { ISegment } from 'interfaces/segment';
|
||||||
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
|
import { formatStrategyName } from 'utils/strategyNames';
|
||||||
|
import { useFormErrors } from 'hooks/useFormErrors';
|
||||||
|
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
|
||||||
|
import { sortStrategyParameters } from 'utils/sortStrategyParameters';
|
||||||
|
import { useCollaborateData } from 'hooks/useCollaborateData';
|
||||||
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
|
import { comparisonModerator } from '../featureStrategy.utils';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
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 NewFeatureStrategyEdit = () => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const featureId = useRequiredPathParam('featureId');
|
||||||
|
const environmentId = useRequiredQueryParam('environmentId');
|
||||||
|
const strategyId = useRequiredQueryParam('strategyId');
|
||||||
|
|
||||||
|
const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({});
|
||||||
|
const [segments, setSegments] = useState<ISegment[]>([]);
|
||||||
|
const { updateStrategyOnFeature, loading } = useFeatureStrategyApi();
|
||||||
|
const { strategyDefinition } = useStrategy(strategy.name);
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const errors = useFormErrors();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { unleashUrl } = uiConfig;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { addChange } = useChangeRequestApi();
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const { refetch: refetchChangeRequests } =
|
||||||
|
usePendingChangeRequests(projectId);
|
||||||
|
const { setPreviousTitle } = useTitleTracking();
|
||||||
|
|
||||||
|
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
const ref = useRef<IFeatureToggle>(feature);
|
||||||
|
|
||||||
|
const { data, staleDataNotification, forceRefreshCache } =
|
||||||
|
useCollaborateData<IFeatureToggle>(
|
||||||
|
{
|
||||||
|
unleashGetter: useFeature,
|
||||||
|
params: [projectId, featureId],
|
||||||
|
dataKey: 'feature',
|
||||||
|
refetchFunctionKey: 'refetchFeature',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
feature,
|
||||||
|
{
|
||||||
|
afterSubmitAction: refetchFeature,
|
||||||
|
},
|
||||||
|
comparisonModerator,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current.name === '' && feature.name) {
|
||||||
|
forceRefreshCache(feature);
|
||||||
|
ref.current = feature;
|
||||||
|
}
|
||||||
|
}, [feature]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
segments: savedStrategySegments,
|
||||||
|
refetchSegments: refetchSavedStrategySegments,
|
||||||
|
} = useSegments(strategyId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedStrategy = data?.environments
|
||||||
|
.flatMap((environment) => environment.strategies)
|
||||||
|
.find((strategy) => strategy.id === strategyId);
|
||||||
|
setStrategy((prev) => ({ ...prev, ...savedStrategy }));
|
||||||
|
setPreviousTitle(savedStrategy?.title || '');
|
||||||
|
}, [strategyId, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fill in the selected segments once they've been fetched.
|
||||||
|
savedStrategySegments && setSegments(savedStrategySegments);
|
||||||
|
}, [JSON.stringify(savedStrategySegments)]);
|
||||||
|
|
||||||
|
const payload = createStrategyPayload(strategy, segments);
|
||||||
|
|
||||||
|
const onStrategyEdit = async (payload: IFeatureStrategyPayload) => {
|
||||||
|
await updateStrategyOnFeature(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentId,
|
||||||
|
strategyId,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
await refetchSavedStrategySegments();
|
||||||
|
setToastData({
|
||||||
|
title: 'Strategy updated',
|
||||||
|
type: 'success',
|
||||||
|
confetti: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStrategyRequestEdit = async (payload: IFeatureStrategyPayload) => {
|
||||||
|
await addChange(projectId, environmentId, {
|
||||||
|
action: 'updateStrategy',
|
||||||
|
feature: featureId,
|
||||||
|
payload: { ...payload, id: strategyId },
|
||||||
|
});
|
||||||
|
// FIXME: segments in change requests
|
||||||
|
setToastData({
|
||||||
|
title: 'Change added to draft',
|
||||||
|
type: 'success',
|
||||||
|
confetti: true,
|
||||||
|
});
|
||||||
|
refetchChangeRequests();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (isChangeRequestConfigured(environmentId)) {
|
||||||
|
await onStrategyRequestEdit(payload);
|
||||||
|
} else {
|
||||||
|
await onStrategyEdit(payload);
|
||||||
|
}
|
||||||
|
refetchFeature();
|
||||||
|
navigate(formatFeaturePath(projectId, featureId));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!strategy.id || !strategyDefinition) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTemplate
|
||||||
|
modal
|
||||||
|
title={formatStrategyName(strategy.name ?? '')}
|
||||||
|
description={featureStrategyHelp}
|
||||||
|
documentationLink={featureStrategyDocsLink}
|
||||||
|
documentationLinkLabel={featureStrategyDocsLinkLabel}
|
||||||
|
formatApiCode={() =>
|
||||||
|
formatUpdateStrategyApiCode(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentId,
|
||||||
|
strategyId,
|
||||||
|
payload,
|
||||||
|
strategyDefinition,
|
||||||
|
unleashUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h1>NEW EDIT FORM</h1>
|
||||||
|
<FeatureStrategyForm
|
||||||
|
projectId={projectId}
|
||||||
|
feature={data}
|
||||||
|
strategy={strategy}
|
||||||
|
setStrategy={setStrategy}
|
||||||
|
segments={segments}
|
||||||
|
setSegments={setSegments}
|
||||||
|
environmentId={environmentId}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
loading={loading}
|
||||||
|
permission={UPDATE_FEATURE_STRATEGY}
|
||||||
|
errors={errors}
|
||||||
|
isChangeRequest={isChangeRequestConfigured(environmentId)}
|
||||||
|
/>
|
||||||
|
{staleDataNotification}
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createStrategyPayload = (
|
||||||
|
strategy: Partial<IFeatureStrategy>,
|
||||||
|
segments: ISegment[],
|
||||||
|
): IFeatureStrategyPayload => ({
|
||||||
|
name: strategy.name,
|
||||||
|
title: strategy.title,
|
||||||
|
constraints: strategy.constraints ?? [],
|
||||||
|
parameters: strategy.parameters ?? {},
|
||||||
|
variants: strategy.variants ?? [],
|
||||||
|
segments: segments.map((segment) => segment.id),
|
||||||
|
disabled: strategy.disabled ?? false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const formatFeaturePath = (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
): string => {
|
||||||
|
return `/projects/${projectId}/features/${featureId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatEditStrategyPath = (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string,
|
||||||
|
strategyId: string,
|
||||||
|
): string => {
|
||||||
|
const params = new URLSearchParams({ environmentId, strategyId });
|
||||||
|
|
||||||
|
return `/projects/${projectId}/features/${featureId}/strategies/edit?${params}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatUpdateStrategyApiCode = (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string,
|
||||||
|
strategyId: string,
|
||||||
|
strategy: Partial<IFeatureStrategy>,
|
||||||
|
strategyDefinition: IStrategy,
|
||||||
|
unleashUrl?: string,
|
||||||
|
): string => {
|
||||||
|
if (!unleashUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the strategy parameters payload so that they match
|
||||||
|
// the order of the input fields in the form, for usability.
|
||||||
|
const sortedStrategy = {
|
||||||
|
...strategy,
|
||||||
|
parameters: sortStrategyParameters(
|
||||||
|
strategy.parameters ?? {},
|
||||||
|
strategyDefinition,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${unleashUrl}/api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`;
|
||||||
|
const payload = JSON.stringify(sortedStrategy, undefined, 2);
|
||||||
|
|
||||||
|
return `curl --location --request PUT '${url}' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${payload}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const featureStrategyHelp = `
|
||||||
|
An activation strategy will only run when a feature toggle is enabled and provides a way to control who will get access to the feature.
|
||||||
|
If any of a feature toggle's activation strategies returns true, the user will get access.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const featureStrategyDocsLink =
|
||||||
|
'https://docs.getunleash.io/reference/activation-strategies';
|
||||||
|
|
||||||
|
export const featureStrategyDocsLinkLabel = 'Strategies documentation';
|
@ -12,6 +12,10 @@ import { usePageTitle } from 'hooks/usePageTitle';
|
|||||||
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
|
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
|
||||||
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { NewFeatureStrategyCreate } from 'component/feature/FeatureStrategy/NewFeatureStrategyCreate/NewFeatureStrategyCreate';
|
||||||
|
import { NewFeatureStrategyEdit } from 'component/feature/FeatureStrategy/NewFeatureStrategyEdit/NewFeatureStrategyEdit';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -40,6 +44,8 @@ const FeatureOverview = () => {
|
|||||||
const onSidebarClose = () => navigate(featurePath);
|
const onSidebarClose = () => navigate(featurePath);
|
||||||
usePageTitle(featureId);
|
usePageTitle(featureId);
|
||||||
|
|
||||||
|
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<div>
|
<div>
|
||||||
@ -61,7 +67,11 @@ const FeatureOverview = () => {
|
|||||||
onClose={onSidebarClose}
|
onClose={onSidebarClose}
|
||||||
open
|
open
|
||||||
>
|
>
|
||||||
<FeatureStrategyCreate />
|
<ConditionallyRender
|
||||||
|
condition={newStrategyConfiguration}
|
||||||
|
show={<NewFeatureStrategyCreate />}
|
||||||
|
elseShow={<FeatureStrategyCreate />}
|
||||||
|
/>
|
||||||
</SidebarModal>
|
</SidebarModal>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -73,7 +83,11 @@ const FeatureOverview = () => {
|
|||||||
onClose={onSidebarClose}
|
onClose={onSidebarClose}
|
||||||
open
|
open
|
||||||
>
|
>
|
||||||
<FeatureStrategyEdit />
|
<ConditionallyRender
|
||||||
|
condition={newStrategyConfiguration}
|
||||||
|
show={<NewFeatureStrategyEdit />}
|
||||||
|
elseShow={<FeatureStrategyEdit />}
|
||||||
|
/>
|
||||||
</SidebarModal>
|
</SidebarModal>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -67,6 +67,7 @@ export type UiFlags = {
|
|||||||
scheduledConfigurationChanges?: boolean;
|
scheduledConfigurationChanges?: boolean;
|
||||||
featureSearchAPI?: boolean;
|
featureSearchAPI?: boolean;
|
||||||
featureSearchFrontend?: boolean;
|
featureSearchFrontend?: boolean;
|
||||||
|
newStrategyConfiguration?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -96,6 +96,7 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"migrationLock": true,
|
"migrationLock": true,
|
||||||
|
"newStrategyConfiguration": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
"privateProjects": false,
|
"privateProjects": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
|
@ -30,7 +30,8 @@ export type IFlagKey =
|
|||||||
| 'featureSearchFrontend'
|
| 'featureSearchFrontend'
|
||||||
| 'scheduledConfigurationChanges'
|
| 'scheduledConfigurationChanges'
|
||||||
| 'detectSegmentUsageInChangeRequests'
|
| 'detectSegmentUsageInChangeRequests'
|
||||||
| 'stripClientHeadersOn304';
|
| 'stripClientHeadersOn304'
|
||||||
|
| 'newStrategyConfiguration';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -136,6 +137,10 @@ const flags: IFlags = {
|
|||||||
.UNLEASH_EXPERIMENTAL_DETECT_SEGMENT_USAGE_IN_CHANGE_REQUESTS,
|
.UNLEASH_EXPERIMENTAL_DETECT_SEGMENT_USAGE_IN_CHANGE_REQUESTS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
newStrategyConfiguration: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_NEW_STRATEGY_CONFIGURATION,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -41,6 +41,7 @@ process.nextTick(async () => {
|
|||||||
featureSearchAPI: true,
|
featureSearchAPI: true,
|
||||||
featureSearchFrontend: true,
|
featureSearchFrontend: true,
|
||||||
stripClientHeadersOn304: true,
|
stripClientHeadersOn304: true,
|
||||||
|
newStrategyConfiguration: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user