From 99d63cff334b884a329565da189940f845350c9b Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Fri, 14 Jul 2023 14:28:02 +0200 Subject: [PATCH] feat: strategy variant UI spike (#4246) --- .../FeatureStrategyEdit.tsx | 1 + .../FeatureStrategyForm.tsx | 11 ++ .../EnvironmentVariantsModal.tsx | 1 - .../VariantForm/VariantForm.tsx | 1 - .../FlexibleStrategy/FlexibleStrategy.tsx | 31 ----- .../StrategyTypes/StrategyVariants.tsx | 125 ++++++++++++++++++ frontend/src/interfaces/featureToggle.ts | 8 ++ frontend/src/interfaces/strategy.ts | 3 + 8 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 frontend/src/component/feature/StrategyTypes/StrategyVariants.tsx diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx index 97346c5b80..43036006f2 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx @@ -242,6 +242,7 @@ export const createStrategyPayload = ( title: strategy.title, constraints: strategy.constraints ?? [], parameters: strategy.parameters ?? {}, + variants: strategy.variants ?? [], segments: segments.map(segment => segment.id), disabled: strategy.disabled ?? false, }); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx index c8f518b2f0..cba83a9ece 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx @@ -31,6 +31,7 @@ import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequ import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess'; import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle'; import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled'; +import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants'; interface IFeatureStrategyFormProps { feature: IFeatureToggle; @@ -246,6 +247,16 @@ export const FeatureStrategyForm = ({ hasAccess={access} /> + + } + /> + diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx index 8d80af481b..63802b697a 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal.tsx @@ -390,7 +390,6 @@ export const EnvironmentVariantsModal = ({ ) ) } - projectId={projectId} apiPayload={apiPayload} /> ))} diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx index 969c541ecb..1e5ac5880e 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx @@ -150,7 +150,6 @@ interface IVariantFormProps { variants: IFeatureVariantEdit[]; updateVariant: (updatedVariant: IFeatureVariantEdit) => void; removeVariant: (variantId: string) => void; - projectId: string; apiPayload: { patch: Operation[]; error?: string; diff --git a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx index d4fead7c3f..43659c5ce5 100644 --- a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx +++ b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx @@ -17,8 +17,6 @@ import Loader from '../../../common/Loader/Loader'; import { useEffect, useMemo } from 'react'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useLocation } from 'react-router'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; interface IFlexibleStrategyProps { parameters: IFeatureStrategyParameters; @@ -35,7 +33,6 @@ const FlexibleStrategy = ({ const projectId = useRequiredPathParam('projectId'); const { defaultStickiness, loading } = useDefaultProjectSettings(projectId); const { pathname } = useLocation(); - const { uiConfig } = useUiConfig(); const isDefaultStrategyEdit = pathname.includes('default-strategy'); const onUpdate = (field: string) => (newValue: string) => { @@ -126,34 +123,6 @@ const FlexibleStrategy = ({ onChange={e => onUpdate('groupId')(e.target.value)} data-testid={FLEXIBLE_STRATEGY_GROUP_ID} /> - -
- - Variant - - - onUpdate('variant')(e.target.value) - } - /> - - } - /> ); diff --git a/frontend/src/component/feature/StrategyTypes/StrategyVariants.tsx b/frontend/src/component/feature/StrategyTypes/StrategyVariants.tsx new file mode 100644 index 0000000000..29900efdef --- /dev/null +++ b/frontend/src/component/feature/StrategyTypes/StrategyVariants.tsx @@ -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> + >; + strategy: Partial; +}> = ({ strategy, setStrategy }) => { + const projectId = useRequiredPathParam('projectId'); + const environment = useRequiredQueryParam('environmentId'); + const [variantsEdit, setVariantsEdit] = useState([]); + const [newVariant, setNewVariant] = useState(); + 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 ( + <> + + {variantsEdit.map(variant => ( + + updateVariant(updatedVariant, variant.id) + } + removeVariant={() => + setVariantsEdit(variantsEdit => + updateWeightEdit( + variantsEdit.filter( + v => v.id !== variant.id + ), + 1000 + ) + ) + } + apiPayload={{ patch: [] }} + /> + ))} + + + Add variant + + + ); +}; diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index affb40cfbd..e38f54ff0e 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -57,6 +57,14 @@ export interface IFeatureVariant { payload?: IPayload; } +export interface IFeatureStrategyVariant { + name: string; + stickiness: string; + weight: number; + weightType: string; + payload?: IPayload; +} + export interface IOverride { contextName: string; values: string[]; diff --git a/frontend/src/interfaces/strategy.ts b/frontend/src/interfaces/strategy.ts index 3109783efe..67915af1f7 100644 --- a/frontend/src/interfaces/strategy.ts +++ b/frontend/src/interfaces/strategy.ts @@ -1,4 +1,5 @@ import { Operator } from 'constants/operators'; +import { IFeatureStrategyVariant } from './featureToggle'; export interface IFeatureStrategy { id: string; @@ -7,6 +8,7 @@ export interface IFeatureStrategy { title?: string; constraints: IConstraint[]; parameters: IFeatureStrategyParameters; + variants?: IFeatureStrategyVariant[]; featureName?: string; projectId?: string; environment?: string; @@ -24,6 +26,7 @@ export interface IFeatureStrategyPayload { title?: string; constraints: IConstraint[]; parameters: IFeatureStrategyParameters; + variants?: IFeatureStrategyVariant[]; segments?: number[]; disabled?: boolean; }