mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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