mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02:00
feat: strategy variant UI spike (#4246)
This commit is contained in:
parent
e8ea79c967
commit
99d63cff33
@ -242,6 +242,7 @@ export const createStrategyPayload = (
|
|||||||
title: strategy.title,
|
title: strategy.title,
|
||||||
constraints: strategy.constraints ?? [],
|
constraints: strategy.constraints ?? [],
|
||||||
parameters: strategy.parameters ?? {},
|
parameters: strategy.parameters ?? {},
|
||||||
|
variants: strategy.variants ?? [],
|
||||||
segments: segments.map(segment => segment.id),
|
segments: segments.map(segment => segment.id),
|
||||||
disabled: strategy.disabled ?? false,
|
disabled: strategy.disabled ?? false,
|
||||||
});
|
});
|
||||||
|
@ -31,6 +31,7 @@ import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequ
|
|||||||
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';
|
import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled';
|
||||||
|
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
|
||||||
|
|
||||||
interface IFeatureStrategyFormProps {
|
interface IFeatureStrategyFormProps {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
@ -246,6 +247,16 @@ export const FeatureStrategyForm = ({
|
|||||||
hasAccess={access}
|
hasAccess={access}
|
||||||
/>
|
/>
|
||||||
<StyledHr />
|
<StyledHr />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(uiConfig?.flags?.strategyVariant)}
|
||||||
|
show={
|
||||||
|
<StrategyVariants
|
||||||
|
strategy={strategy}
|
||||||
|
setStrategy={setStrategy}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledHr />
|
||||||
<FeatureStrategyEnabledDisabled
|
<FeatureStrategyEnabledDisabled
|
||||||
enabled={!strategy?.disabled}
|
enabled={!strategy?.disabled}
|
||||||
onToggleEnabled={() =>
|
onToggleEnabled={() =>
|
||||||
|
@ -390,7 +390,6 @@ export const EnvironmentVariantsModal = ({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
projectId={projectId}
|
|
||||||
apiPayload={apiPayload}
|
apiPayload={apiPayload}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -150,7 +150,6 @@ interface IVariantFormProps {
|
|||||||
variants: IFeatureVariantEdit[];
|
variants: IFeatureVariantEdit[];
|
||||||
updateVariant: (updatedVariant: IFeatureVariantEdit) => void;
|
updateVariant: (updatedVariant: IFeatureVariantEdit) => void;
|
||||||
removeVariant: (variantId: string) => void;
|
removeVariant: (variantId: string) => void;
|
||||||
projectId: string;
|
|
||||||
apiPayload: {
|
apiPayload: {
|
||||||
patch: Operation[];
|
patch: Operation[];
|
||||||
error?: string;
|
error?: string;
|
||||||
|
@ -17,8 +17,6 @@ import Loader from '../../../common/Loader/Loader';
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
|
|
||||||
interface IFlexibleStrategyProps {
|
interface IFlexibleStrategyProps {
|
||||||
parameters: IFeatureStrategyParameters;
|
parameters: IFeatureStrategyParameters;
|
||||||
@ -35,7 +33,6 @@ const FlexibleStrategy = ({
|
|||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId);
|
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
const isDefaultStrategyEdit = pathname.includes('default-strategy');
|
const isDefaultStrategyEdit = pathname.includes('default-strategy');
|
||||||
const onUpdate = (field: string) => (newValue: string) => {
|
const onUpdate = (field: string) => (newValue: string) => {
|
||||||
@ -126,34 +123,6 @@ const FlexibleStrategy = ({
|
|||||||
onChange={e => onUpdate('groupId')(e.target.value)}
|
onChange={e => onUpdate('groupId')(e.target.value)}
|
||||||
data-testid={FLEXIBLE_STRATEGY_GROUP_ID}
|
data-testid={FLEXIBLE_STRATEGY_GROUP_ID}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(uiConfig?.flags?.strategyVariant)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
style={{
|
|
||||||
marginBottom: '1rem',
|
|
||||||
display: 'flex',
|
|
||||||
gap: '1ch',
|
|
||||||
}}
|
|
||||||
component="h2"
|
|
||||||
>
|
|
||||||
Variant
|
|
||||||
</Typography>
|
|
||||||
<Input
|
|
||||||
label="variant"
|
|
||||||
id="variant-input"
|
|
||||||
value={parseParameterString(parameters.variant)}
|
|
||||||
disabled={!editable}
|
|
||||||
onChange={e =>
|
|
||||||
onUpdate('variant')(e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,125 @@
|
|||||||
|
import { VariantForm } from '../FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm';
|
||||||
|
import { updateWeightEdit } from '../../common/util';
|
||||||
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
import { IFeatureVariantEdit } from '../FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal';
|
||||||
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
|
import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from '../../providers/AccessProvider/permissions';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { WeightType } from '../../../constants/variantTypes';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
|
||||||
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
|
||||||
|
const StyledVariantForms = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StrategyVariants: FC<{
|
||||||
|
setStrategy: React.Dispatch<
|
||||||
|
React.SetStateAction<Partial<IFeatureStrategy>>
|
||||||
|
>;
|
||||||
|
strategy: Partial<IFeatureStrategy>;
|
||||||
|
}> = ({ strategy, setStrategy }) => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const environment = useRequiredQueryParam('environmentId');
|
||||||
|
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
|
||||||
|
const [newVariant, setNewVariant] = useState<string>();
|
||||||
|
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVariantsEdit(
|
||||||
|
(strategy.variants || []).map(variant => ({
|
||||||
|
...variant,
|
||||||
|
new: false,
|
||||||
|
isValid: true,
|
||||||
|
id: uuidv4(),
|
||||||
|
overrides: [],
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStrategy(prev => ({
|
||||||
|
...prev,
|
||||||
|
variants: variantsEdit.map(variant => ({
|
||||||
|
name: variant.name,
|
||||||
|
weight: variant.weight,
|
||||||
|
stickiness: variant.stickiness,
|
||||||
|
payload: variant.payload,
|
||||||
|
weightType: variant.weightType,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}, [JSON.stringify(variantsEdit)]);
|
||||||
|
|
||||||
|
const updateVariant = (updatedVariant: IFeatureVariantEdit, id: string) => {
|
||||||
|
setVariantsEdit(prevVariants =>
|
||||||
|
updateWeightEdit(
|
||||||
|
prevVariants.map(prevVariant =>
|
||||||
|
prevVariant.id === id ? updatedVariant : prevVariant
|
||||||
|
),
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addVariant = () => {
|
||||||
|
const id = uuidv4();
|
||||||
|
setVariantsEdit(variantsEdit => [
|
||||||
|
...variantsEdit,
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
weightType: WeightType.VARIABLE,
|
||||||
|
weight: 0,
|
||||||
|
overrides: [],
|
||||||
|
stickiness:
|
||||||
|
variantsEdit?.length > 0
|
||||||
|
? variantsEdit[0].stickiness
|
||||||
|
: defaultStickiness,
|
||||||
|
new: true,
|
||||||
|
isValid: false,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setNewVariant(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledVariantForms>
|
||||||
|
{variantsEdit.map(variant => (
|
||||||
|
<VariantForm
|
||||||
|
key={variant.id}
|
||||||
|
variant={variant}
|
||||||
|
variants={variantsEdit}
|
||||||
|
updateVariant={updatedVariant =>
|
||||||
|
updateVariant(updatedVariant, variant.id)
|
||||||
|
}
|
||||||
|
removeVariant={() =>
|
||||||
|
setVariantsEdit(variantsEdit =>
|
||||||
|
updateWeightEdit(
|
||||||
|
variantsEdit.filter(
|
||||||
|
v => v.id !== variant.id
|
||||||
|
),
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
apiPayload={{ patch: [] }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledVariantForms>
|
||||||
|
<PermissionButton
|
||||||
|
onClick={addVariant}
|
||||||
|
variant="outlined"
|
||||||
|
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentId={environment}
|
||||||
|
>
|
||||||
|
Add variant
|
||||||
|
</PermissionButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -57,6 +57,14 @@ export interface IFeatureVariant {
|
|||||||
payload?: IPayload;
|
payload?: IPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFeatureStrategyVariant {
|
||||||
|
name: string;
|
||||||
|
stickiness: string;
|
||||||
|
weight: number;
|
||||||
|
weightType: string;
|
||||||
|
payload?: IPayload;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IOverride {
|
export interface IOverride {
|
||||||
contextName: string;
|
contextName: string;
|
||||||
values: string[];
|
values: string[];
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Operator } from 'constants/operators';
|
import { Operator } from 'constants/operators';
|
||||||
|
import { IFeatureStrategyVariant } from './featureToggle';
|
||||||
|
|
||||||
export interface IFeatureStrategy {
|
export interface IFeatureStrategy {
|
||||||
id: string;
|
id: string;
|
||||||
@ -7,6 +8,7 @@ export interface IFeatureStrategy {
|
|||||||
title?: string;
|
title?: string;
|
||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
parameters: IFeatureStrategyParameters;
|
parameters: IFeatureStrategyParameters;
|
||||||
|
variants?: IFeatureStrategyVariant[];
|
||||||
featureName?: string;
|
featureName?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
@ -24,6 +26,7 @@ export interface IFeatureStrategyPayload {
|
|||||||
title?: string;
|
title?: string;
|
||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
parameters: IFeatureStrategyParameters;
|
parameters: IFeatureStrategyParameters;
|
||||||
|
variants?: IFeatureStrategyVariant[];
|
||||||
segments?: number[];
|
segments?: number[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user