1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: add push to all button to UI (#2969)

## 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 <github@nunogois.com>
This commit is contained in:
Gastón Fournier 2023-01-24 10:22:02 +01:00 committed by GitHub
parent c71c0bb3ac
commit 3713764a4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 216 additions and 1 deletions

View File

@ -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 = () => {
}
>
<StyledButtonContainer>
<PushVariantsButton
current={environment.name}
environments={feature.environments}
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
projectId={projectId}
onSubmit={selected =>
pushToEnvironments(
environment.variants ?? [],
selected
)
}
/>
<EnvironmentVariantsCopyFrom
environment={environment}
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}

View File

@ -0,0 +1,158 @@
import {
Button,
Checkbox,
Divider,
FormControlLabel,
Menu,
MenuItem,
styled,
} from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { useState } from 'react';
import { useCheckProjectAccess } from 'hooks/useHasAccess';
const StyledMenu = styled(Menu)(({ theme }) => ({
'&>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 | HTMLElement>(
null
);
const pushToOpen = Boolean(pushToAnchorEl);
const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>(
[]
);
const hasAccess = useCheckProjectAccess(projectId);
const hasAccessTo = environments.reduce((acc, env) => {
acc[env.name] = hasAccess(permission, env.name);
return acc;
}, {} as Record<string, boolean>);
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 (
<ConditionallyRender
condition={variants.length > 0 && environments.length > 1}
show={
<>
<Button
onClick={e => {
setPushToAnchorEl(e.currentTarget);
}}
id={`push-to-menu-${current}`}
aria-controls={pushToOpen ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={pushToOpen ? 'true' : undefined}
variant="outlined"
>
Push to environment
</Button>
<StyledMenu
anchorEl={pushToAnchorEl}
open={pushToOpen}
onClose={() => setPushToAnchorEl(null)}
MenuListProps={{
'aria-labelledby': `push-to-menu-${current}`,
}}
>
{environments
.filter(environment => environment.name !== current)
.map(otherEnvironment => (
<MenuItem key={otherEnvironment.name}>
<FormControlLabel
disabled={
!hasAccessTo[
otherEnvironment.name
] ?? false
}
control={
<Checkbox
onChange={event => {
if (event.target.checked) {
addSelectedEnvironment(
otherEnvironment.name
);
} else {
removeSelectedEnvironment(
otherEnvironment.name
);
}
}}
checked={selectedEnvironments.includes(
otherEnvironment.name
)}
value={otherEnvironment.name}
/>
}
label={otherEnvironment.name}
/>
</MenuItem>
))}
<Divider />
<StyledButton
variant="outlined"
onClick={() => {
onSubmit(selectedEnvironments);
cleanupState();
}}
disabled={selectedEnvironments.length === 0}
>
Push to selected ({selectedEnvironments.length})
</StyledButton>
</StyledMenu>
</>
}
/>
);
};

View File

@ -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,
};