From ee3f5be522c8cfa66b879d49d1e6cf116bac92bd Mon Sep 17 00:00:00 2001 From: olav Date: Tue, 2 Aug 2022 14:08:07 +0200 Subject: [PATCH 1/2] refactor: improve spacing between strategy form parameters (#1188) --- .../GeneralStrategy/GeneralStrategy.styles.ts | 12 +-- .../GeneralStrategy/GeneralStrategy.tsx | 11 ++- .../StrategyInputList/StrategyInputList.tsx | 87 +++++++++++-------- 3 files changed, 63 insertions(+), 47 deletions(-) diff --git a/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.styles.ts b/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.styles.ts index 8693a7b480..75b187d585 100644 --- a/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.styles.ts +++ b/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.styles.ts @@ -1,13 +1,15 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ + container: { + display: 'grid', + gap: theme.spacing(4), + }, helpText: { - color: 'rgba(0, 0, 0, 0.54)', + color: theme.palette.text.secondary, fontSize: theme.fontSizes.smallerBody, lineHeight: '14px', - margin: '0.5rem 0', - }, - generalSection: { - margin: '1rem 0', + margin: 0, + marginTop: theme.spacing(1), }, })); diff --git a/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.tsx b/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.tsx index fa9639cff0..75b782b170 100644 --- a/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.tsx +++ b/frontend/src/component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy.tsx @@ -53,14 +53,13 @@ const GeneralStrategy = ({ } return ( - <> +
{strategyDefinition.parameters.map( ({ name, type, description, required }) => { if (type === 'percentage') { const value = parseParameterNumber(parameters[name]); return (
-
0 ? !regex.test(value) : false; return ( -
+
+
+
+
); }; diff --git a/frontend/src/component/feature/StrategyTypes/StrategyInputList/StrategyInputList.tsx b/frontend/src/component/feature/StrategyTypes/StrategyInputList/StrategyInputList.tsx index 51b0529684..ffd22ad756 100644 --- a/frontend/src/component/feature/StrategyTypes/StrategyInputList/StrategyInputList.tsx +++ b/frontend/src/component/feature/StrategyTypes/StrategyInputList/StrategyInputList.tsx @@ -1,5 +1,12 @@ import React, { ChangeEvent, useState } from 'react'; -import { Button, Chip, TextField, Typography } from '@mui/material'; +import { + Button, + Chip, + TextField, + Typography, + styled, + TextFieldProps, +} from '@mui/material'; import { Add } from '@mui/icons-material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ADD_TO_STRATEGY_INPUT_LIST, STRATEGY_INPUT_LIST } from 'utils/testIds'; @@ -12,6 +19,21 @@ interface IStrategyInputList { disabled: boolean; } +const Container = styled('div')(({ theme }) => ({ + display: 'grid', + gap: theme.spacing(1), +})); + +const ChipsList = styled('div')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), +})); + +const InputContainer = styled('div')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), +})); + const StrategyInputList = ({ name, list, @@ -61,49 +83,42 @@ const StrategyInputList = ({ ); }; - // @ts-expect-error - const onChange = e => { - setInput(e.currentTarget.value); + const onChange: TextFieldProps['onChange'] = event => { + setInput(event.currentTarget.value); }; return ( -
+ List of {name} -
- {list.map((entryValue, index) => ( - 0} + show={ + + {list.map((entryValue, index) => ( + + } + onDelete={ + disabled ? undefined : () => onClose(index) + } + title="Remove value" /> - } - style={{ marginRight: '3px' }} - onDelete={disabled ? undefined : () => onClose(index)} - title="Remove value" - /> - ))} -
+ ))} + + } + /> + Add -
+ } /> -
+ ); }; From 826c8ff116be6b210b46df20919c117beb7dd2fb Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Tue, 2 Aug 2022 20:19:29 +0200 Subject: [PATCH 2/2] copy all strategies into an environment (#1182) --- .../CopyButton/CopyButton.tsx | 106 ++++++++++++++++++ .../FeatureStrategyEmpty.tsx | 83 ++++++++++++-- .../CopyStrategyIconMenu.tsx | 21 ++-- 3 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/CopyButton/CopyButton.tsx diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/CopyButton/CopyButton.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/CopyButton/CopyButton.tsx new file mode 100644 index 0000000000..e8c0037482 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/CopyButton/CopyButton.tsx @@ -0,0 +1,106 @@ +import { MouseEvent, useContext, useState, VFC } from 'react'; +import { + Button, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Tooltip, +} from '@mui/material'; +import { Lock } from '@mui/icons-material'; +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'; + +interface ICopyButtonProps { + environmentId: IFeatureEnvironment['name']; + environments: IFeatureEnvironment['name'][]; + onClick: (environmentId: string) => void; +} + +export const CopyButton: VFC = ({ + environmentId, + environments, + onClick, +}) => { + const projectId = useRequiredPathParam('projectId'); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const { hasAccess } = useContext(AccessContext); + const enabled = environments.some(environment => + hasAccess(CREATE_FEATURE_STRATEGY, projectId, environment) + ); + + return ( +
+ +
+ +
+
+ { + setAnchorEl(null); + }} + MenuListProps={{ + 'aria-labelledby': `copy-all-strategies-${environmentId}`, + }} + > + {environments.map(environment => { + const access = hasAccess( + CREATE_FEATURE_STRATEGY, + projectId, + environment + ); + + return ( + +
+ onClick(environment)} + disabled={!access} + > + + + + } + /> + + Copy from {environment} + + +
+
+ ); + })} +
+
+ ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx index 2bd7481087..c1490eea47 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx @@ -10,6 +10,8 @@ import { useStyles } from './FeatureStrategyEmpty.styles'; import { formatUnknownError } from 'utils/formatUnknownError'; import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable'; import { getFeatureStrategyIcon } from 'utils/strategyNames'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { CopyButton } from './CopyButton/CopyButton'; interface IFeatureStrategyEmptyProps { projectId: string; @@ -30,18 +32,55 @@ export const FeatureStrategyEmpty = ({ projectId, featureId ); + const { feature } = useFeature(projectId, featureId); + const otherAvailableEnvironments = feature?.environments.filter( + environment => + environment.name !== environmentId && + environment.strategies && + environment.strategies.length > 0 + ); - const onAfterAddStrategy = () => { + const onAfterAddStrategy = (multiple = false) => { refetchFeature(); refetchFeatureImmutable(); setToastData({ - title: 'Strategy created', - text: 'Successfully created strategy', + title: multiple ? 'Strategies created' : 'Strategy created', + text: multiple + ? 'Successfully copied from another environment' + : 'Successfully created strategy', type: 'success', }); }; + const onCopyStrategies = async (fromEnvironmentName: string) => { + const strategies = + otherAvailableEnvironments?.find( + environment => environment.name === fromEnvironmentName + )?.strategies || []; + + try { + await Promise.all( + strategies.map(strategy => { + const { id, ...strategyCopy } = { + ...strategy, + environment: environmentId, + }; + + return addStrategyToFeature( + projectId, + featureId, + environmentId, + strategyCopy + ); + }) + ); + onAfterAddStrategy(true); + } catch (error) { + setToastApiError(formatUnknownError(error)); + } + }; + const onAddSimpleStrategy = async () => { try { await addStrategyToFeature(projectId, featureId, environmentId, { @@ -82,12 +121,38 @@ export const FeatureStrategyEmpty = ({ API key configured for this environment.

- + + + 0 + } + show={ + environment.name + )} + onClick={onCopyStrategies} + /> + } + /> + Or use a strategy template 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 index e2950df9cd..e16482a8c1 100644 --- 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 @@ -40,10 +40,7 @@ export const CopyStrategyIconMenu: VFC = ({ projectId, featureId ); - const handleClick = (event: MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { + const onClose = () => { setAnchorEl(null); }; const { hasAccess } = useContext(AccessContext); @@ -70,7 +67,7 @@ export const CopyStrategyIconMenu: VFC = ({ } catch (error) { setToastApiError(formatUnknownError(error)); } - handleClose(); + onClose(); }; const enabled = environments.some(environment => @@ -87,11 +84,13 @@ export const CopyStrategyIconMenu: VFC = ({
) => { + setAnchorEl(event.currentTarget); + }} disabled={!enabled} > @@ -102,9 +101,9 @@ export const CopyStrategyIconMenu: VFC = ({ id="basic-menu" anchorEl={anchorEl} open={open} - onClose={handleClose} + onClose={onClose} MenuListProps={{ - 'aria-labelledby': 'basic-button', + 'aria-labelledby': `copy-strategy-icon-menu-${strategy.id}`, }} > {environments.map(environment => { @@ -136,7 +135,9 @@ export const CopyStrategyIconMenu: VFC = ({ } /> - {environment} + + Copy to {environment} +