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 (
+
+
+
+
+
+
+
+
+ );
+};
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}
+
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
-
+
}
/>
-
+
);
};