From 070fedf83f0036bf6c79a74575a5202cf5a64ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Mon, 2 Jan 2023 17:06:13 +0000 Subject: [PATCH] feat: toggleable variants per env --- .../EnvironmentVariantModal.tsx | 42 ++- .../EnvironmentVariantsCard.tsx | 20 +- .../FeatureEnvironmentVariants.tsx | 266 ++++++++++++------ .../actions/useFeatureApi/useFeatureApi.ts | 8 +- 4 files changed, 238 insertions(+), 98 deletions(-) diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantModal/EnvironmentVariantModal.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantModal/EnvironmentVariantModal.tsx index 6d68409ccb..99f643f6e6 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantModal/EnvironmentVariantModal.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantModal/EnvironmentVariantModal.tsx @@ -152,6 +152,7 @@ interface IEnvironmentVariantModalProps { newVariants: IFeatureVariant[] ) => { patch: Operation[]; error?: string }; onConfirm: (updatedVariants: IFeatureVariant[]) => void; + global?: boolean; } export const EnvironmentVariantModal = ({ @@ -161,6 +162,7 @@ export const EnvironmentVariantModal = ({ setOpen, getApiPayload, onConfirm, + global, }: IEnvironmentVariantModalProps) => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); @@ -259,11 +261,19 @@ export const EnvironmentVariantModal = ({ onConfirm(getUpdatedVariants()); }; - const formatApiCode = () => `curl --location --request PATCH '${ - uiConfig.unleashUrl - }/api/admin/projects/${projectId}/features/${featureId}/environments/${ - environment?.name - }/variants' \\ + const formatApiCode = () => + global + ? `curl --location --request PATCH '${ + uiConfig.unleashUrl + }/api/admin/projects/${projectId}/features/${featureId}/variants' \\ + --header 'Authorization: INSERT_API_KEY' \\ + --header 'Content-Type: application/json' \\ + --data-raw '${JSON.stringify(apiPayload.patch, undefined, 2)}'` + : `curl --location --request PATCH '${ + uiConfig.unleashUrl + }/api/admin/projects/${projectId}/features/${featureId}/environments/${ + environment?.name + }/variants' \\ --header 'Authorization: INSERT_API_KEY' \\ --header 'Content-Type: application/json' \\ --data-raw '${JSON.stringify(apiPayload.patch, undefined, 2)}'`; @@ -300,7 +310,9 @@ export const EnvironmentVariantModal = ({ if (!isNameUnique(name)) { setError( ErrorField.NAME, - 'A variant with that name already exists for this environment.' + global + ? 'A variant with that name already exists.' + : 'A variant with that name already exists for this environment.' ); } setName(name); @@ -345,10 +357,20 @@ export const EnvironmentVariantModal = ({ loading={!open} > - - - {environment?.name} - + All environments} + elseShow={ + <> + + + {environment?.name} + + + } + />
diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx index fa8ccf7a9c..a80fd5167d 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx @@ -59,6 +59,7 @@ interface IEnvironmentVariantsCardProps { onEditVariant: (variant: IFeatureVariant) => void; onDeleteVariant: (variant: IFeatureVariant) => void; onUpdateStickiness: (variant: IFeatureVariant[]) => void; + global?: boolean; children?: React.ReactNode; } @@ -68,6 +69,7 @@ export const EnvironmentVariantsCard = ({ onEditVariant, onDeleteVariant, onUpdateStickiness, + global, children, }: IEnvironmentVariantsCardProps) => { const { context } = useUnleashContext(); @@ -104,10 +106,20 @@ export const EnvironmentVariantsCard = ({
- - - {environment.name} - + All environments} + elseShow={ + <> + + + {environment.name} + + + } + />
{children}
diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx index 25d8c05bf5..f0239c30c0 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx @@ -1,6 +1,13 @@ import * as jsonpatch from 'fast-json-patch'; -import { Alert, styled, useMediaQuery, useTheme } from '@mui/material'; +import { + Alert, + FormControlLabel, + styled, + Switch, + useMediaQuery, + useTheme, +} from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; @@ -11,7 +18,7 @@ import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from 'component/providers/AccessP import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { IFeatureEnvironment, IFeatureVariant } from 'interfaces/featureToggle'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { EnvironmentVariantModal } from './EnvironmentVariantModal/EnvironmentVariantModal'; import { EnvironmentVariantsCard } from './EnvironmentVariantsCard/EnvironmentVariantsCard'; import { VariantDeleteDialog } from './VariantDeleteDialog/VariantDeleteDialog'; @@ -19,6 +26,7 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; import { formatUnknownError } from 'utils/formatUnknownError'; import useToast from 'hooks/useToast'; import { EnvironmentVariantsCopyFrom } from './EnvironmentVariantsCopyFrom/EnvironmentVariantsCopyFrom'; +import { dequal } from 'dequal'; const StyledAlert = styled(Alert)(({ theme }) => ({ marginBottom: theme.spacing(4), @@ -52,6 +60,20 @@ export const FeatureEnvironmentVariants = () => { const [modalOpen, setModalOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); + const [perEnvironment, setPerEnvironment] = useState(false); + + const envSpecificVariants = !feature.environments.reduce( + (acc, { variants }) => + acc && dequal(feature.environments[0].variants, variants), + true + ); + + useEffect(() => { + if (envSpecificVariants) { + setPerEnvironment(envSpecificVariants); + } + }, [envSpecificVariants]); + const createPatch = ( variants: IFeatureVariant[], newVariants: IFeatureVariant[] @@ -75,10 +97,13 @@ export const FeatureEnvironmentVariants = () => { }; const updateVariants = async ( - environment: IFeatureEnvironment, - variants: IFeatureVariant[] + variants: IFeatureVariant[], + environment?: IFeatureEnvironment ) => { - const environmentVariants = environment.variants ?? []; + const environmentVariants = + (environment + ? environment.variants + : feature.environments[0].variants) ?? []; const { patch } = getApiPayload(environmentVariants, variants); if (patch.length === 0) return; @@ -86,21 +111,21 @@ export const FeatureEnvironmentVariants = () => { await patchFeatureEnvironmentVariants( projectId, featureId, - environment.name, - patch + patch, + environment?.name ); refetchFeature(); }; - const addVariant = (environment: IFeatureEnvironment) => { + const addVariant = (environment?: IFeatureEnvironment) => { setSelectedEnvironment(environment); setSelectedVariant(undefined); setModalOpen(true); }; const editVariant = ( - environment: IFeatureEnvironment, - variant: IFeatureVariant + variant: IFeatureVariant, + environment?: IFeatureEnvironment ) => { setSelectedEnvironment(environment); setSelectedVariant(variant); @@ -108,8 +133,8 @@ export const FeatureEnvironmentVariants = () => { }; const deleteVariant = ( - environment: IFeatureEnvironment, - variant: IFeatureVariant + variant: IFeatureVariant, + environment?: IFeatureEnvironment ) => { setSelectedEnvironment(environment); setSelectedVariant(variant); @@ -117,15 +142,18 @@ export const FeatureEnvironmentVariants = () => { }; const onDeleteConfirm = async () => { - if (selectedEnvironment && selectedVariant) { - const variants = selectedEnvironment.variants ?? []; + if (selectedVariant) { + const variants = + (selectedEnvironment + ? selectedEnvironment.variants + : feature.environments[0].variants) ?? []; const updatedVariants = variants.filter( ({ name }) => name !== selectedVariant.name ); try { - await updateVariants(selectedEnvironment, updatedVariants); + await updateVariants(updatedVariants, selectedEnvironment); setDeleteOpen(false); setToastData({ title: `Variant deleted successfully`, @@ -138,19 +166,17 @@ export const FeatureEnvironmentVariants = () => { }; const onVariantConfirm = async (updatedVariants: IFeatureVariant[]) => { - if (selectedEnvironment) { - try { - await updateVariants(selectedEnvironment, updatedVariants); - setModalOpen(false); - setToastData({ - title: `Variant ${ - selectedVariant ? 'updated' : 'added' - } successfully`, - type: 'success', - }); - } catch (error: unknown) { - setToastApiError(formatUnknownError(error)); - } + try { + await updateVariants(updatedVariants, selectedEnvironment); + setModalOpen(false); + setToastData({ + title: `Variant ${ + selectedVariant ? 'updated' : 'added' + } successfully`, + type: 'success', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); } }; @@ -160,7 +186,7 @@ export const FeatureEnvironmentVariants = () => { ) => { try { const variants = fromEnvironment.variants ?? []; - await updateVariants(toEnvironment, variants); + await updateVariants(variants, toEnvironment); setToastData({ title: 'Variants copied successfully', type: 'success', @@ -171,11 +197,11 @@ export const FeatureEnvironmentVariants = () => { }; const onUpdateStickiness = async ( - environment: IFeatureEnvironment, - updatedVariants: IFeatureVariant[] + updatedVariants: IFeatureVariant[], + environment?: IFeatureEnvironment ) => { try { - await updateVariants(environment, updatedVariants); + await updateVariants(updatedVariants, environment); setToastData({ title: 'Variant stickiness updated successfully', type: 'success', @@ -192,17 +218,32 @@ export const FeatureEnvironmentVariants = () => { + <> + - - } - /> + } + /> + + setPerEnvironment(!perEnvironment) + } + color="primary" + disabled={envSpecificVariants} + /> + } + /> + } > { variants you should use the getVariant() method in the Client SDK. - {feature.environments.map(environment => { - const otherEnvsWithVariants = feature.environments.filter( - ({ name, variants }) => - name !== environment.name && variants?.length - ); + { + const otherEnvsWithVariants = + feature.environments.filter( + ({ name, variants }) => + name !== environment.name && + variants?.length + ); - return ( - - editVariant(environment, variant) - } - onDeleteVariant={(variant: IFeatureVariant) => - deleteVariant(environment, variant) - } - onUpdateStickiness={(variants: IFeatureVariant[]) => - onUpdateStickiness(environment, variants) - } - > - - - addVariant(environment)} - variant="outlined" - permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS} - projectId={projectId} - environmentId={environment.name} + return ( + + editVariant(variant, environment) + } + onDeleteVariant={( + variant: IFeatureVariant + ) => deleteVariant(variant, environment)} + onUpdateStickiness={( + variants: IFeatureVariant[] + ) => + onUpdateStickiness( + variants, + environment + ) + } + > + + + + addVariant(environment) + } + variant="outlined" + permission={ + UPDATE_FEATURE_ENVIRONMENT_VARIANTS + } + projectId={projectId} + environmentId={environment.name} + > + Add variant + + + + ); + })} + elseShow={ + + editVariant(variant) + } + onDeleteVariant={(variant: IFeatureVariant) => + deleteVariant(variant) + } + onUpdateStickiness={( + variants: IFeatureVariant[] + ) => onUpdateStickiness(variants)} > - Add variant - - - - ); - })} + + addVariant()} + variant="outlined" + permission={ + UPDATE_FEATURE_ENVIRONMENT_VARIANTS + } + projectId={projectId} + environmentId={ + feature.environments[0]?.name + } + > + Add variant + + + + } + /> + } + elseShow={ + + Variants needs at least one environment. + + } + /> { const patchFeatureEnvironmentVariants = async ( projectId: string, featureId: string, - environmentName: string, - patchPayload: Operation[] + patchPayload: Operation[], + environmentName?: string ) => { - const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentName}/variants`; + const path = environmentName + ? `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentName}/variants` + : `api/admin/projects/${projectId}/features/${featureId}/variants`; const req = createRequest(path, { method: 'PATCH', body: JSON.stringify(patchPayload),