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:
parent
c71c0bb3ac
commit
3713764a4b
@ -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}
|
||||
|
@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user