mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02: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 {
|
interface IEnvironmentAccordionBodyProps {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
featureEnvironment?: IFeatureEnvironment;
|
featureEnvironment?: IFeatureEnvironment;
|
||||||
|
otherEnvironments?: IFeatureEnvironment['name'][];
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnvironmentAccordionBody = ({
|
const EnvironmentAccordionBody = ({
|
||||||
featureEnvironment,
|
featureEnvironment,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
otherEnvironments,
|
||||||
}: IEnvironmentAccordionBodyProps) => {
|
}: IEnvironmentAccordionBodyProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
@ -93,6 +95,7 @@ const EnvironmentAccordionBody = ({
|
|||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
index={index}
|
index={index}
|
||||||
environmentName={featureEnvironment.name}
|
environmentName={featureEnvironment.name}
|
||||||
|
otherEnvironments={otherEnvironments}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
import { MoveListItem, useDragItem } from 'hooks/useDragItem';
|
import { MoveListItem, useDragItem } from 'hooks/useDragItem';
|
||||||
|
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
import { StrategyItem } from './StrategyItem/StrategyItem';
|
import { StrategyItem } from './StrategyItem/StrategyItem';
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ interface IStrategyDraggableItemProps {
|
|||||||
strategy: IFeatureStrategy;
|
strategy: IFeatureStrategy;
|
||||||
environmentName: string;
|
environmentName: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
otherEnvironments?: IFeatureEnvironment['name'][];
|
||||||
onDragAndDrop: MoveListItem;
|
onDragAndDrop: MoveListItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,6 +17,7 @@ export const StrategyDraggableItem = ({
|
|||||||
strategy,
|
strategy,
|
||||||
index,
|
index,
|
||||||
environmentName,
|
environmentName,
|
||||||
|
otherEnvironments,
|
||||||
onDragAndDrop,
|
onDragAndDrop,
|
||||||
}: IStrategyDraggableItemProps) => {
|
}: IStrategyDraggableItemProps) => {
|
||||||
const ref = useDragItem(index, onDragAndDrop);
|
const ref = useDragItem(index, onDragAndDrop);
|
||||||
@ -28,6 +31,7 @@ export const StrategyDraggableItem = ({
|
|||||||
<StrategyItem
|
<StrategyItem
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
environmentId={environmentName}
|
environmentId={environmentName}
|
||||||
|
otherEnvironments={otherEnvironments}
|
||||||
isDraggable
|
isDraggable
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { DragIndicator, Edit } from '@mui/icons-material';
|
||||||
import { styled, useTheme, IconButton } from '@mui/material';
|
import { styled, useTheme, IconButton } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
import {
|
import {
|
||||||
getFeatureStrategyIcon,
|
getFeatureStrategyIcon,
|
||||||
@ -13,13 +14,15 @@ import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/Feature
|
|||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
||||||
import { useStyles } from './StrategyItem.styles';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
|
||||||
|
import { useStyles } from './StrategyItem.styles';
|
||||||
|
|
||||||
interface IStrategyItemProps {
|
interface IStrategyItemProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
strategy: IFeatureStrategy;
|
strategy: IFeatureStrategy;
|
||||||
isDraggable?: boolean;
|
isDraggable?: boolean;
|
||||||
|
otherEnvironments?: IFeatureEnvironment['name'][];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DragIcon = styled(IconButton)(({ theme }) => ({
|
const DragIcon = styled(IconButton)(({ theme }) => ({
|
||||||
@ -32,6 +35,7 @@ export const StrategyItem = ({
|
|||||||
environmentId,
|
environmentId,
|
||||||
strategy,
|
strategy,
|
||||||
isDraggable,
|
isDraggable,
|
||||||
|
otherEnvironments,
|
||||||
}: IStrategyItemProps) => {
|
}: IStrategyItemProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
@ -67,6 +71,17 @@ export const StrategyItem = ({
|
|||||||
text={formatStrategyName(strategy.name)}
|
text={formatStrategyName(strategy.name)}
|
||||||
/>
|
/>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(
|
||||||
|
otherEnvironments && otherEnvironments?.length > 0
|
||||||
|
)}
|
||||||
|
show={() => (
|
||||||
|
<CopyStrategyIconMenu
|
||||||
|
environments={otherEnvironments as string[]}
|
||||||
|
strategy={strategy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
permission={UPDATE_FEATURE_STRATEGY}
|
permission={UPDATE_FEATURE_STRATEGY}
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
|
@ -126,6 +126,9 @@ const FeatureOverviewEnvironment = ({
|
|||||||
<EnvironmentAccordionBody
|
<EnvironmentAccordionBody
|
||||||
featureEnvironment={featureEnvironment}
|
featureEnvironment={featureEnvironment}
|
||||||
isDisabled={!env.enabled}
|
isDisabled={!env.enabled}
|
||||||
|
otherEnvironments={feature?.environments
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.filter(name => name !== env.name)}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
|
Loading…
Reference in New Issue
Block a user