1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

Copy a strategy between environments (#1166)

* initial ui for copying strategy between environments

* copy startegy api call

* feat: disable copy strategy button if no available target environments
This commit is contained in:
Tymoteusz Czech 2022-07-29 11:52:26 +02:00 committed by GitHub
parent edb093f1ab
commit d57ee97263
5 changed files with 174 additions and 1 deletions

View File

@ -14,11 +14,13 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
interface IEnvironmentAccordionBodyProps {
isDisabled: boolean;
featureEnvironment?: IFeatureEnvironment;
otherEnvironments?: IFeatureEnvironment['name'][];
}
const EnvironmentAccordionBody = ({
featureEnvironment,
isDisabled,
otherEnvironments,
}: IEnvironmentAccordionBodyProps) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@ -93,6 +95,7 @@ const EnvironmentAccordionBody = ({
strategy={strategy}
index={index}
environmentName={featureEnvironment.name}
otherEnvironments={otherEnvironments}
/>
))}
</>

View File

@ -1,6 +1,7 @@
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { MoveListItem, useDragItem } from 'hooks/useDragItem';
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { IFeatureStrategy } from 'interfaces/strategy';
import { StrategyItem } from './StrategyItem/StrategyItem';
@ -8,6 +9,7 @@ interface IStrategyDraggableItemProps {
strategy: IFeatureStrategy;
environmentName: string;
index: number;
otherEnvironments?: IFeatureEnvironment['name'][];
onDragAndDrop: MoveListItem;
}
@ -15,6 +17,7 @@ export const StrategyDraggableItem = ({
strategy,
index,
environmentName,
otherEnvironments,
onDragAndDrop,
}: IStrategyDraggableItemProps) => {
const ref = useDragItem(index, onDragAndDrop);
@ -28,6 +31,7 @@ export const StrategyDraggableItem = ({
<StrategyItem
strategy={strategy}
environmentId={environmentName}
otherEnvironments={otherEnvironments}
isDraggable
/>
</div>

View File

@ -0,0 +1,148 @@
import { MouseEvent, useContext, useState, VFC } from 'react';
import {
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip,
} from '@mui/material';
import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-material';
import { IFeatureStrategy } from 'interfaces/strategy';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import AccessContext from 'contexts/AccessContext';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import useToast from 'hooks/useToast';
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
import { formatUnknownError } from 'utils/formatUnknownError';
interface ICopyStrategyIconMenuProps {
environments: IFeatureEnvironment['name'][];
strategy: IFeatureStrategy;
}
export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
environments,
strategy,
}) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const { addStrategyToFeature } = useFeatureStrategyApi();
const { setToastData, setToastApiError } = useToast();
const { refetchFeature } = useFeature(projectId, featureId);
const { refetchFeature: refetchFeatureImmutable } = useFeatureImmutable(
projectId,
featureId
);
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const { hasAccess } = useContext(AccessContext);
const onClick = async (environmentId: string) => {
const { id, ...strategyCopy } = {
...strategy,
environment: environmentId,
};
try {
await addStrategyToFeature(
projectId,
featureId,
environmentId,
strategyCopy
);
refetchFeature();
refetchFeatureImmutable();
setToastData({
title: `Strategy created`,
text: `Successfully copied a strategy to ${environmentId}`,
type: 'success',
});
} catch (error) {
setToastApiError(formatUnknownError(error));
}
handleClose();
};
const enabled = environments.some(environment =>
hasAccess(CREATE_FEATURE_STRATEGY, projectId, environment)
);
return (
<div>
<Tooltip
title={`Copy to another environment${
enabled ? '' : ' (Access denied)'
}`}
>
<div>
<IconButton
size="large"
id="basic-button"
aria-controls={open ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
disabled={!enabled}
>
<CopyIcon />
</IconButton>
</div>
</Tooltip>
<Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
{environments.map(environment => {
const access = hasAccess(
CREATE_FEATURE_STRATEGY,
projectId,
environment
);
return (
<Tooltip
title={
access
? ''
: "You don't have access to add a strategy to this environment"
}
key={environment}
>
<div>
<MenuItem
onClick={() => onClick(environment)}
disabled={!access}
>
<ConditionallyRender
condition={!access}
show={
<ListItemIcon>
<Lock fontSize="small" />
</ListItemIcon>
}
/>
<ListItemText>{environment}</ListItemText>
</MenuItem>
</div>
</Tooltip>
);
})}
</Menu>
</div>
);
};

View File

@ -1,6 +1,7 @@
import { DragIndicator, Edit } from '@mui/icons-material';
import { styled, useTheme, IconButton } from '@mui/material';
import { Link } from 'react-router-dom';
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { IFeatureStrategy } from 'interfaces/strategy';
import {
getFeatureStrategyIcon,
@ -13,13 +14,15 @@ import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/Feature
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { useStyles } from './StrategyItem.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
import { useStyles } from './StrategyItem.styles';
interface IStrategyItemProps {
environmentId: string;
strategy: IFeatureStrategy;
isDraggable?: boolean;
otherEnvironments?: IFeatureEnvironment['name'][];
}
const DragIcon = styled(IconButton)(({ theme }) => ({
@ -32,6 +35,7 @@ export const StrategyItem = ({
environmentId,
strategy,
isDraggable,
otherEnvironments,
}: IStrategyItemProps) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@ -67,6 +71,17 @@ export const StrategyItem = ({
text={formatStrategyName(strategy.name)}
/>
<div className={styles.actions}>
<ConditionallyRender
condition={Boolean(
otherEnvironments && otherEnvironments?.length > 0
)}
show={() => (
<CopyStrategyIconMenu
environments={otherEnvironments as string[]}
strategy={strategy}
/>
)}
/>
<PermissionIconButton
permission={UPDATE_FEATURE_STRATEGY}
environmentId={environmentId}

View File

@ -126,6 +126,9 @@ const FeatureOverviewEnvironment = ({
<EnvironmentAccordionBody
featureEnvironment={featureEnvironment}
isDisabled={!env.enabled}
otherEnvironments={feature?.environments
.map(({ name }) => name)
.filter(name => name !== env.name)}
/>
<ConditionallyRender
condition={