From 3713764a4bf7ad168bf9290e4c068edb18fe5d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Tue, 24 Jan 2023 10:22:02 +0100 Subject: [PATCH] feat: add push to all button to UI (#2969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## About the changes This adds the push to all button to the UI Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: #2254 ### UI ![Screenshot from 2023-01-16 12-25-49](https://user-images.githubusercontent.com/455064/214073136-999c32f3-d119-4d55-ae29-47f6a9304e84.png) Co-authored-by: Nuno Góis --- .../FeatureEnvironmentVariants.tsx | 37 +++- .../PushVariantsButton/PushVariantsButton.tsx | 158 ++++++++++++++++++ .../actions/useFeatureApi/useFeatureApi.ts | 22 +++ 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/PushVariantsButton/PushVariantsButton.tsx diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx index d3a63395aa..60180371da 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx @@ -19,6 +19,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 { PushVariantsButton } from './PushVariantsButton/PushVariantsButton'; const StyledAlert = styled(Alert)(({ theme }) => ({ marginBottom: theme.spacing(4), @@ -43,7 +44,8 @@ export const FeatureEnvironmentVariants = () => { projectId, featureId ); - const { patchFeatureEnvironmentVariants } = useFeatureApi(); + const { patchFeatureEnvironmentVariants, overrideVariantsInEnvironments } = + useFeatureApi(); const [searchValue, setSearchValue] = useState(''); const [selectedEnvironment, setSelectedEnvironment] = @@ -88,6 +90,27 @@ export const FeatureEnvironmentVariants = () => { refetchFeature(); }; + const pushToEnvironments = async ( + variants: IFeatureVariant[], + selected: string[] + ) => { + try { + await overrideVariantsInEnvironments( + projectId, + featureId, + variants, + selected + ); + refetchFeature(); + setToastData({ + title: `Variants pushed successfully`, + type: 'success', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + const addVariant = (environment: IFeatureEnvironment) => { setSelectedEnvironment(environment); setSelectedVariant(undefined); @@ -241,6 +264,18 @@ export const FeatureEnvironmentVariants = () => { } > + + pushToEnvironments( + environment.variants ?? [], + selected + ) + } + /> ({ + '&>div>ul': { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + padding: theme.spacing(2), + '&>li': { + padding: theme.spacing(0), + }, + }, +})); + +const StyledButton = styled(Button)(({ theme }) => ({ + marginTop: theme.spacing(1), +})); + +interface IPushVariantsButtonProps { + current: string; + environments: IFeatureEnvironment[]; + permission: string; + projectId: string; + onSubmit: (selected: string[]) => void; +} + +export const PushVariantsButton = ({ + current, + environments, + permission, + projectId, + onSubmit, +}: IPushVariantsButtonProps) => { + const [pushToAnchorEl, setPushToAnchorEl] = useState( + null + ); + const pushToOpen = Boolean(pushToAnchorEl); + + const [selectedEnvironments, setSelectedEnvironments] = useState( + [] + ); + + const hasAccess = useCheckProjectAccess(projectId); + const hasAccessTo = environments.reduce((acc, env) => { + acc[env.name] = hasAccess(permission, env.name); + return acc; + }, {} as Record); + + const addSelectedEnvironment = (name: string) => { + setSelectedEnvironments(prevSelectedEnvironments => [ + ...prevSelectedEnvironments, + name, + ]); + }; + + const removeSelectedEnvironment = (name: string) => { + setSelectedEnvironments(prevSelectedEnvironments => + prevSelectedEnvironments.filter(env => env !== name) + ); + }; + + const cleanupState = () => { + setSelectedEnvironments([]); + setPushToAnchorEl(null); + }; + + const variants = + environments.find(environment => environment.name === current) + ?.variants ?? []; + + return ( + 0 && environments.length > 1} + show={ + <> + + + setPushToAnchorEl(null)} + MenuListProps={{ + 'aria-labelledby': `push-to-menu-${current}`, + }} + > + {environments + .filter(environment => environment.name !== current) + .map(otherEnvironment => ( + + { + if (event.target.checked) { + addSelectedEnvironment( + otherEnvironment.name + ); + } else { + removeSelectedEnvironment( + otherEnvironment.name + ); + } + }} + checked={selectedEnvironments.includes( + otherEnvironment.name + )} + value={otherEnvironment.name} + /> + } + label={otherEnvironment.name} + /> + + ))} + + { + onSubmit(selectedEnvironments); + cleanupState(); + }} + disabled={selectedEnvironments.length === 0} + > + Push to selected ({selectedEnvironments.length}) + + + + } + /> + ); +}; diff --git a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts index 617f5fa6a4..6a39119c8a 100644 --- a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts +++ b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts @@ -4,6 +4,7 @@ import { Operation } from 'fast-json-patch'; import { IConstraint } from 'interfaces/strategy'; import { CreateFeatureSchema } from 'openapi'; import useAPI from '../useApi/useApi'; +import { IFeatureVariant } from 'interfaces/featureToggle'; const useFeatureApi = () => { const { makeRequest, createRequest, errors, loading } = useAPI({ @@ -223,6 +224,26 @@ const useFeatureApi = () => { } }; + const overrideVariantsInEnvironments = async ( + projectId: string, + featureId: string, + variants: IFeatureVariant[], + environments: string[] + ) => { + const put = `api/admin/projects/${projectId}/features/${featureId}/variants-batch`; + const req = createRequest(put, { + method: 'PUT', + body: JSON.stringify({ variants, environments }), + }); + + try { + const res = await makeRequest(req.caller, req.id); + return res; + } catch (e) { + throw e; + } + }; + const cloneFeatureToggle = async ( projectId: string, featureId: string, @@ -257,6 +278,7 @@ const useFeatureApi = () => { patchFeatureToggle, patchFeatureVariants, patchFeatureEnvironmentVariants, + overrideVariantsInEnvironments, cloneFeatureToggle, loading, };