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:
parent
edb093f1ab
commit
d57ee97263
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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={
|
||||
|
Loading…
Reference in New Issue
Block a user