From d57ee9726366d092cb8a1106f1ce43e06208cd2a Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 29 Jul 2022 11:52:26 +0200 Subject: [PATCH] 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 --- .../EnvironmentAccordionBody.tsx | 3 + .../StrategyDraggableItem.tsx | 4 + .../CopyStrategyIconMenu.tsx | 148 ++++++++++++++++++ .../StrategyItem/StrategyItem.tsx | 17 +- .../FeatureOverviewEnvironment.tsx | 3 + 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/CopyStrategyIconMenu/CopyStrategyIconMenu.tsx diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx index 4cca8ddd9f..8c51528caf 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx @@ -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} /> ))} diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx index 7465b6b3f6..080e1731e2 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx @@ -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 = ({ diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/CopyStrategyIconMenu/CopyStrategyIconMenu.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/CopyStrategyIconMenu/CopyStrategyIconMenu.tsx new file mode 100644 index 0000000000..e2950df9cd --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/CopyStrategyIconMenu/CopyStrategyIconMenu.tsx @@ -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 = ({ + environments, + strategy, +}) => { + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const [anchorEl, setAnchorEl] = useState(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) => { + 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 ( +
+ +
+ + + +
+
+ + {environments.map(environment => { + const access = hasAccess( + CREATE_FEATURE_STRATEGY, + projectId, + environment + ); + + return ( + +
+ onClick(environment)} + disabled={!access} + > + + + + } + /> + {environment} + +
+
+ ); + })} +
+
+ ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx index f44f82ce6a..be80809709 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx @@ -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)} />
+ 0 + )} + show={() => ( + + )} + /> name) + .filter(name => name !== env.name)} />