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),