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 { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { EnvironmentVariantsCopyFrom } from './EnvironmentVariantsCopyFrom/EnvironmentVariantsCopyFrom';
|
import { EnvironmentVariantsCopyFrom } from './EnvironmentVariantsCopyFrom/EnvironmentVariantsCopyFrom';
|
||||||
|
import { PushVariantsButton } from './PushVariantsButton/PushVariantsButton';
|
||||||
|
|
||||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(4),
|
marginBottom: theme.spacing(4),
|
||||||
@ -43,7 +44,8 @@ export const FeatureEnvironmentVariants = () => {
|
|||||||
projectId,
|
projectId,
|
||||||
featureId
|
featureId
|
||||||
);
|
);
|
||||||
const { patchFeatureEnvironmentVariants } = useFeatureApi();
|
const { patchFeatureEnvironmentVariants, overrideVariantsInEnvironments } =
|
||||||
|
useFeatureApi();
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [selectedEnvironment, setSelectedEnvironment] =
|
const [selectedEnvironment, setSelectedEnvironment] =
|
||||||
@ -88,6 +90,27 @@ export const FeatureEnvironmentVariants = () => {
|
|||||||
refetchFeature();
|
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) => {
|
const addVariant = (environment: IFeatureEnvironment) => {
|
||||||
setSelectedEnvironment(environment);
|
setSelectedEnvironment(environment);
|
||||||
setSelectedVariant(undefined);
|
setSelectedVariant(undefined);
|
||||||
@ -241,6 +264,18 @@ export const FeatureEnvironmentVariants = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
|
<PushVariantsButton
|
||||||
|
current={environment.name}
|
||||||
|
environments={feature.environments}
|
||||||
|
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
||||||
|
projectId={projectId}
|
||||||
|
onSubmit={selected =>
|
||||||
|
pushToEnvironments(
|
||||||
|
environment.variants ?? [],
|
||||||
|
selected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<EnvironmentVariantsCopyFrom
|
<EnvironmentVariantsCopyFrom
|
||||||
environment={environment}
|
environment={environment}
|
||||||
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
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 { IConstraint } from 'interfaces/strategy';
|
||||||
import { CreateFeatureSchema } from 'openapi';
|
import { CreateFeatureSchema } from 'openapi';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
import { IFeatureVariant } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
const useFeatureApi = () => {
|
const useFeatureApi = () => {
|
||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
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 (
|
const cloneFeatureToggle = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureId: string,
|
featureId: string,
|
||||||
@ -257,6 +278,7 @@ const useFeatureApi = () => {
|
|||||||
patchFeatureToggle,
|
patchFeatureToggle,
|
||||||
patchFeatureVariants,
|
patchFeatureVariants,
|
||||||
patchFeatureEnvironmentVariants,
|
patchFeatureEnvironmentVariants,
|
||||||
|
overrideVariantsInEnvironments,
|
||||||
cloneFeatureToggle,
|
cloneFeatureToggle,
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user