From ee730e070846d86a83501ccefda6a8708265fdc9 Mon Sep 17 00:00:00 2001
From: Youssef Khedher
Date: Fri, 4 Mar 2022 23:39:41 +0100
Subject: [PATCH] Feat/custom strategy screen (#722)
* feat: setup new screen structure
* refactor: strategyParameter
* feat: add strategy input errors for required fields
* feat: add create strategy to routes
* feat: add EditStrategy component
* feat: edit strategy view and EditStrategy component
* feat: update EditStrategy component
* test: update snapshots
* fix: styles
* test: update snapshots
* refactor: rename StrategyForm and fix ts errors
* test: update snapshots
* fix: remove test route
* fix: update PR based on feedback
* fix: update PR based on feedback
* refactor: restore feature settings (#712)
* refactor: resotre feature settings
* fix: update PR based on feedback
* feat: add feature information in Metadata container
* fix: update PR based on feedback
* fix: update PR based on feedback
Co-authored-by: Fredrik Strand Oseberg
* chore(deps): update dependency @types/react-dom to v17.0.13
* refactor: expect existing TS errors (#767)
* refactor: expect existing TS errors
* refactor: fail build on new TS errors
* fix: styles
* refactor: rename StrategyForm and fix ts errors
* fix: update PR based on feedback
* fix: cleaning up
* fix: remove errors and warnings
* fix: remove ts-expect-error and fix errors
* fix: ts errors
* Update src/component/strategies/StrategyView/StrategyView.tsx
* Update src/component/strategies/StrategyView/StrategyView.tsx
Co-authored-by: Fredrik Strand Oseberg
Co-authored-by: Renovate Bot
Co-authored-by: olav
---
.../api-token/ApiTokenForm/ApiTokenForm.tsx | 2 +-
.../common/GeneralSelect/GeneralSelect.tsx | 2 +
.../context/ContextForm/ContextForm.tsx | 2 +-
.../EnvironmentForm/EnvironmentForm.tsx | 2 +-
.../feature/FeatureForm/FeatureForm.tsx | 2 +-
.../__snapshots__/routes-test.jsx.snap | 13 +-
frontend/src/component/menu/routes.js | 18 +-
.../Project/ProjectForm/ProjectForm.tsx | 2 +-
.../CreateStrategy/CreateStrategy.tsx | 99 ++++++++
.../strategies/EditStrategy/EditStrategy.tsx | 102 ++++++++
.../StrategiesList/StrategiesList.styles.ts | 8 +-
.../StrategiesList/StrategiesList.tsx | 89 ++++---
.../StrategyForm/StrategyForm.styles.ts | 67 +++++
.../strategies/StrategyForm/StrategyForm.tsx | 237 ++++++------------
.../StrategyParameter/StrategyParameter.jsx | 67 -----
.../StrategyParameter.styles.ts | 43 ++++
.../StrategyParameter/StrategyParameter.tsx | 132 ++++++++++
.../StrategyParameters/StrategyParameters.jsx | 27 --
.../StrategyParameters/StrategyParameters.tsx | 31 +++
.../StrategyDetails/StrategyDetails.tsx | 14 +-
.../strategies/StrategyView/StrategyView.tsx | 103 ++++----
.../list-component-test.jsx.snap | 118 ++++++++-
.../strategies/hooks/useStrategyForm.ts | 90 +++++++
.../tags/TagTypeForm/TagTypeForm.tsx | 2 +-
.../useStrategiesApi/useStrategiesApi.ts | 7 +-
.../getters/useStrategy/defaultStrategy.ts | 10 +
.../api/getters/useStrategy/useStrategy.ts | 30 +++
frontend/src/interfaces/strategy.ts | 19 ++
28 files changed, 950 insertions(+), 388 deletions(-)
create mode 100644 frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx
create mode 100644 frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
create mode 100644 frontend/src/component/strategies/StrategyForm/StrategyForm.styles.ts
delete mode 100644 frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.jsx
create mode 100644 frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.styles.ts
create mode 100644 frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.tsx
delete mode 100644 frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.jsx
create mode 100644 frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.tsx
create mode 100644 frontend/src/component/strategies/hooks/useStrategyForm.ts
create mode 100644 frontend/src/hooks/api/getters/useStrategy/defaultStrategy.ts
create mode 100644 frontend/src/hooks/api/getters/useStrategy/useStrategy.ts
diff --git a/frontend/src/component/admin/api-token/ApiTokenForm/ApiTokenForm.tsx b/frontend/src/component/admin/api-token/ApiTokenForm/ApiTokenForm.tsx
index ef62565172..9f160f0ad2 100644
--- a/frontend/src/component/admin/api-token/ApiTokenForm/ApiTokenForm.tsx
+++ b/frontend/src/component/admin/api-token/ApiTokenForm/ApiTokenForm.tsx
@@ -18,7 +18,7 @@ interface IApiTokenFormProps {
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
- mode: string;
+ mode: 'Create' | 'Edit';
clearErrors: () => void;
}
const ApiTokenForm: React.FC = ({
diff --git a/frontend/src/component/common/GeneralSelect/GeneralSelect.tsx b/frontend/src/component/common/GeneralSelect/GeneralSelect.tsx
index 4c76656312..8127a90350 100644
--- a/frontend/src/component/common/GeneralSelect/GeneralSelect.tsx
+++ b/frontend/src/component/common/GeneralSelect/GeneralSelect.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core';
import { SELECT_ITEM_ID } from '../../../testIds';
+import { KeyboardArrowDownOutlined } from '@material-ui/icons';
export interface ISelectOption {
key: string;
@@ -71,6 +72,7 @@ const GeneralSelect: React.FC = ({
label={label}
id={id}
value={value}
+ IconComponent={KeyboardArrowDownOutlined}
{...rest}
>
{renderSelectItems()}
diff --git a/frontend/src/component/context/ContextForm/ContextForm.tsx b/frontend/src/component/context/ContextForm/ContextForm.tsx
index b9ad29089e..1170e02a48 100644
--- a/frontend/src/component/context/ContextForm/ContextForm.tsx
+++ b/frontend/src/component/context/ContextForm/ContextForm.tsx
@@ -17,7 +17,7 @@ interface IContextForm {
handleSubmit: (e: any) => void;
onCancel: () => void;
errors: { [key: string]: string };
- mode: string;
+ mode: 'Create' | 'Edit';
clearErrors: () => void;
validateContext?: () => void;
setErrors: React.Dispatch>;
diff --git a/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx
index ed29fc049d..abb7fab8e9 100644
--- a/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx
+++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx
@@ -14,7 +14,7 @@ interface IEnvironmentForm {
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
- mode: string;
+ mode: 'Create' | 'Edit';
clearErrors: () => void;
}
diff --git a/frontend/src/component/feature/FeatureForm/FeatureForm.tsx b/frontend/src/component/feature/FeatureForm/FeatureForm.tsx
index 1e75297b7b..fb573f2f8f 100644
--- a/frontend/src/component/feature/FeatureForm/FeatureForm.tsx
+++ b/frontend/src/component/feature/FeatureForm/FeatureForm.tsx
@@ -35,7 +35,7 @@ interface IFeatureToggleForm {
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
- mode: string;
+ mode: 'Create' | 'Edit';
clearErrors: () => void;
}
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
index b065d0886f..db7196d66c 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
@@ -208,8 +208,17 @@ Array [
"layout": "main",
"menu": Object {},
"parent": "/strategies",
- "path": "/strategies/:activeTab/:strategyName",
- "title": ":strategyName",
+ "path": "/strategies/:name/edit",
+ "title": ":name",
+ "type": "protected",
+ },
+ Object {
+ "component": [Function],
+ "layout": "main",
+ "menu": Object {},
+ "parent": "/strategies",
+ "path": "/strategies/:name",
+ "title": ":name",
"type": "protected",
},
Object {
diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js
index 50e4594094..02273e404e 100644
--- a/frontend/src/component/menu/routes.js
+++ b/frontend/src/component/menu/routes.js
@@ -1,5 +1,4 @@
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
-import { StrategyForm } from '../strategies/StrategyForm/StrategyForm';
import { StrategyView } from '../strategies/StrategyView/StrategyView';
import { StrategiesList } from '../strategies/StrategiesList/StrategiesList';
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
@@ -45,6 +44,8 @@ import { EditAddon } from '../addons/EditAddon/EditAddon';
import { CopyFeatureToggle } from '../feature/CopyFeature/CopyFeature';
import { EventHistoryPage } from '../history/EventHistoryPage/EventHistoryPage';
import { FeatureEventHistoryPage } from '../history/FeatureEventHistoryPage/FeatureEventHistoryPage';
+import { CreateStrategy } from '../strategies/CreateStrategy/CreateStrategy';
+import { EditStrategy } from '../strategies/EditStrategy/EditStrategy';
export const routes = [
// Project
@@ -243,14 +244,23 @@ export const routes = [
path: '/strategies/create',
title: 'Create',
parent: '/strategies',
- component: StrategyForm,
+ component: CreateStrategy,
type: 'protected',
layout: 'main',
menu: {},
},
{
- path: '/strategies/:activeTab/:strategyName',
- title: ':strategyName',
+ path: '/strategies/:name/edit',
+ title: ':name',
+ parent: '/strategies',
+ component: EditStrategy,
+ type: 'protected',
+ layout: 'main',
+ menu: {},
+ },
+ {
+ path: '/strategies/:name',
+ title: ':name',
parent: '/strategies',
component: StrategyView,
type: 'protected',
diff --git a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx
index 67b040f969..cc40ee91db 100644
--- a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx
+++ b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx
@@ -14,7 +14,7 @@ interface IProjectForm {
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
- mode: string;
+ mode: 'Create' | 'Edit';
clearErrors: () => void;
validateIdUniqueness: () => void;
}
diff --git a/frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx b/frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx
new file mode 100644
index 0000000000..e429f371b4
--- /dev/null
+++ b/frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx
@@ -0,0 +1,99 @@
+import { useHistory } from 'react-router-dom';
+import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
+import useToast from '../../../hooks/useToast';
+import FormTemplate from '../../common/FormTemplate/FormTemplate';
+import { useStrategyForm } from '../hooks/useStrategyForm';
+import { StrategyForm } from '../StrategyForm/StrategyForm';
+import PermissionButton from '../../common/PermissionButton/PermissionButton';
+import { CREATE_STRATEGY } from '../../providers/AccessProvider/permissions';
+import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStrategiesApi';
+import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
+import { formatUnknownError } from 'utils/format-unknown-error';
+
+export const CreateStrategy = () => {
+ const { setToastData, setToastApiError } = useToast();
+ const { uiConfig } = useUiConfig();
+ const history = useHistory();
+ const {
+ strategyName,
+ strategyDesc,
+ params,
+ setParams,
+ setStrategyName,
+ setStrategyDesc,
+ getStrategyPayload,
+ validateStrategyName,
+ validateParams,
+ clearErrors,
+ setErrors,
+ errors,
+ } = useStrategyForm();
+ const { createStrategy, loading } = useStrategiesApi();
+ const { refetchStrategies } = useStrategies();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ clearErrors();
+ e.preventDefault();
+ const validName = validateStrategyName();
+
+ if (validName && validateParams()) {
+ const payload = getStrategyPayload();
+ try {
+ await createStrategy(payload);
+ refetchStrategies();
+ history.push(`/strategies/${strategyName}`);
+ setToastData({
+ title: 'Strategy created',
+ text: 'Successfully created strategy',
+ confetti: true,
+ type: 'success',
+ });
+ } catch (e: unknown) {
+ setToastApiError(formatUnknownError(e));
+ }
+ }
+ };
+
+ const formatApiCode = () => {
+ return `curl --location --request POST '${
+ uiConfig.unleashUrl
+ }/api/admin/strategies' \\
+--header 'Authorization: INSERT_API_KEY' \\
+--header 'Content-Type: application/json' \\
+--data-raw '${JSON.stringify(getStrategyPayload(), undefined, 2)}'`;
+ };
+
+ const handleCancel = () => {
+ history.goBack();
+ };
+
+ return (
+
+
+
+ Create strategy
+
+
+
+ );
+};
diff --git a/frontend/src/component/strategies/EditStrategy/EditStrategy.tsx b/frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
new file mode 100644
index 0000000000..efdb3ac90b
--- /dev/null
+++ b/frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
@@ -0,0 +1,102 @@
+import { useHistory, useParams } from 'react-router-dom';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import useToast from 'hooks/useToast';
+import FormTemplate from 'component/common/FormTemplate/FormTemplate';
+import { useStrategyForm } from '../hooks/useStrategyForm';
+import { StrategyForm } from '../StrategyForm/StrategyForm';
+import PermissionButton from 'component/common/PermissionButton/PermissionButton';
+import { CREATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
+import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesApi';
+import useStrategies from 'hooks/api/getters/useStrategies/useStrategies';
+import { formatUnknownError } from 'utils/format-unknown-error';
+import useStrategy from 'hooks/api/getters/useStrategy/useStrategy';
+
+export const EditStrategy = () => {
+ const { setToastData, setToastApiError } = useToast();
+ const { uiConfig } = useUiConfig();
+ const history = useHistory();
+ const { name } = useParams<{ name: string }>();
+ const { strategy } = useStrategy(name);
+ const {
+ strategyName,
+ strategyDesc,
+ params,
+ setParams,
+ setStrategyName,
+ setStrategyDesc,
+ getStrategyPayload,
+ validateParams,
+ clearErrors,
+ setErrors,
+ errors,
+ } = useStrategyForm(
+ strategy?.name,
+ strategy?.description,
+ strategy?.parameters
+ );
+ const { updateStrategy, loading } = useStrategiesApi();
+ const { refetchStrategies } = useStrategies();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ clearErrors();
+ e.preventDefault();
+ if (validateParams()) {
+ const payload = getStrategyPayload();
+ try {
+ await updateStrategy(payload);
+ history.push(`/strategies/${strategyName}`);
+ setToastData({
+ type: 'success',
+ title: 'Success',
+ text: 'Successfully updated strategy',
+ });
+ refetchStrategies();
+ } catch (error: unknown) {
+ setToastApiError(formatUnknownError(error));
+ }
+ }
+ };
+
+ const formatApiCode = () => {
+ return `curl --location --request PUT '${
+ uiConfig.unleashUrl
+ }/api/admin/strategies/${name}' \\
+--header 'Authorization: INSERT_API_KEY' \\
+--header 'Content-Type: application/json' \\
+--data-raw '${JSON.stringify(getStrategyPayload(), undefined, 2)}'`;
+ };
+
+ const handleCancel = () => {
+ history.goBack();
+ };
+
+ return (
+
+
+
+ Save
+
+
+
+ );
+};
diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts b/frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts
index 4c2800d79d..063d1d8165 100644
--- a/frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts
+++ b/frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts
@@ -5,16 +5,10 @@ export const useStyles = makeStyles(theme => ({
padding: '0',
['& a']: {
textDecoration: 'none',
- color: 'inherit',
+ color: theme.palette.primary.light,
},
'&:hover': {
backgroundColor: theme.palette.grey[200],
},
},
- deprecated: {
- '& a': {
- // @ts-expect-error
- color: theme.palette.links.deprecated,
- },
- },
}));
diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx
index e75db53b6d..c75cc09f58 100644
--- a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx
+++ b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx
@@ -1,5 +1,4 @@
import { useContext, useState } from 'react';
-import classnames from 'classnames';
import { Link, useHistory } from 'react-router-dom';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import {
@@ -13,6 +12,7 @@ import {
import {
Add,
Delete,
+ Edit,
Extension,
Visibility,
VisibilityOff,
@@ -22,21 +22,21 @@ import {
DELETE_STRATEGY,
UPDATE_STRATEGY,
} from '../../providers/AccessProvider/permissions';
-import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
-import PageContent from '../../common/PageContent/PageContent';
-import HeaderTitle from '../../common/HeaderTitle';
+import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
+import PageContent from 'component/common/PageContent/PageContent';
+import HeaderTitle from 'component/common/HeaderTitle';
import { useStyles } from './StrategiesList.styles';
-import AccessContext from '../../../contexts/AccessContext';
-import Dialogue from '../../common/Dialogue';
-import { ADD_NEW_STRATEGY_ID } from '../../../testIds';
-import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
-import PermissionButton from '../../common/PermissionButton/PermissionButton';
-import { getHumanReadableStrategyName } from '../../../utils/strategy-names';
-import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
-import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStrategiesApi';
-import useToast from '../../../hooks/useToast';
-import { IStrategy } from '../../../interfaces/strategy';
-import { formatUnknownError } from '../../../utils/format-unknown-error';
+import AccessContext from 'contexts/AccessContext';
+import Dialogue from 'component/common/Dialogue';
+import { ADD_NEW_STRATEGY_ID } from 'testIds';
+import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
+import PermissionButton from 'component/common/PermissionButton/PermissionButton';
+import { getHumanReadableStrategyName } from 'utils/strategy-names';
+import useStrategies from 'hooks/api/getters/useStrategies/useStrategies';
+import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesApi';
+import useToast from 'hooks/useToast';
+import { formatUnknownError } from 'utils/format-unknown-error';
+import { ICustomStrategy } from 'interfaces/strategy';
interface IDialogueMetaData {
show: boolean;
@@ -91,8 +91,8 @@ export const StrategiesList = () => {
/>
);
- const strategyLink = ({ name, deprecated }: IStrategy) => (
-
+ const strategyLink = (name: string, deprecated: boolean) => (
+
{getHumanReadableStrategyName(name)}
{
);
- const onReactivateStrategy = (strategy: IStrategy) => {
+ const onReactivateStrategy = (strategy: ICustomStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really reactivate strategy?',
@@ -121,7 +121,7 @@ export const StrategiesList = () => {
});
};
- const onDeprecateStrategy = (strategy: IStrategy) => {
+ const onDeprecateStrategy = (strategy: ICustomStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really deprecate strategy?',
@@ -141,7 +141,7 @@ export const StrategiesList = () => {
});
};
- const onDeleteStrategy = (strategy: IStrategy) => {
+ const onDeleteStrategy = (strategy: ICustomStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really delete strategy?',
@@ -161,7 +161,7 @@ export const StrategiesList = () => {
});
};
- const reactivateButton = (strategy: IStrategy) => (
+ const reactivateButton = (strategy: ICustomStrategy) => (
onReactivateStrategy(strategy)}
@@ -172,7 +172,7 @@ export const StrategiesList = () => {
);
- const deprecateButton = (strategy: IStrategy) => (
+ const deprecateButton = (strategy: ICustomStrategy) => (
{
/>
);
- const deleteButton = (strategy: IStrategy) => (
+ const editButton = (strategy: ICustomStrategy) => (
+ history.push(`/strategies/${strategy?.name}/edit`)
+ }
+ permission={UPDATE_STRATEGY}
+ tooltip={'Edit strategy'}
+ >
+
+
+ }
+ elseShow={
+
+
+
+
+
+
+
+ }
+ />
+ );
+
+ const deleteButton = (strategy: ICustomStrategy) => (
+ onDeleteStrategy(strategy)}
@@ -223,19 +249,12 @@ export const StrategiesList = () => {
const strategyList = () =>
strategies.map(strategy => (
-
+
{
show={reactivateButton(strategy)}
elseShow={deprecateButton(strategy)}
/>
+
({
+ container: {
+ maxWidth: 400,
+ },
+ form: {
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+ },
+ input: { width: '100%', marginBottom: '1rem' },
+ selectInput: {
+ marginBottom: '1rem',
+ minWidth: '400px',
+ [theme.breakpoints.down(600)]: {
+ minWidth: '379px',
+ },
+ },
+ link: {
+ color: theme.palette.primary.light,
+ },
+ label: {
+ minWidth: '300px',
+ [theme.breakpoints.down(600)]: {
+ minWidth: 'auto',
+ },
+ },
+ buttonContainer: {
+ marginTop: 'auto',
+ display: 'flex',
+ justifyContent: 'flex-end',
+ },
+ cancelButton: {
+ marginLeft: '1.5rem',
+ },
+ inputDescription: {
+ marginBottom: '0.5rem',
+ },
+ typeDescription: {
+ fontSize: theme.fontSizes.smallBody,
+ color: theme.palette.grey[600],
+ top: '-13px',
+ position: 'relative',
+ },
+ formHeader: {
+ fontWeight: 'normal',
+ marginTop: '0',
+ },
+ header: {
+ fontWeight: 'normal',
+ },
+ errorMessage: {
+ fontSize: theme.fontSizes.smallBody,
+ color: theme.palette.error.main,
+ position: 'absolute',
+ top: '-8px',
+ },
+ flexRow: {
+ display: 'flex',
+ alignItems: 'center',
+ marginTop: '0.5rem',
+ },
+ paramButton: {
+ color: theme.palette.primary.dark,
+ },
+}));
diff --git a/frontend/src/component/strategies/StrategyForm/StrategyForm.tsx b/frontend/src/component/strategies/StrategyForm/StrategyForm.tsx
index e1fcbeec9e..1008d75746 100644
--- a/frontend/src/component/strategies/StrategyForm/StrategyForm.tsx
+++ b/frontend/src/component/strategies/StrategyForm/StrategyForm.tsx
@@ -1,195 +1,114 @@
-import React, { useState } from 'react';
-import { Typography, TextField, Button } from '@material-ui/core';
+import Input from '../../common/Input/Input';
+import { Button } from '@material-ui/core';
+import { useStyles } from './StrategyForm.styles';
import { Add } from '@material-ui/icons';
-import PageContent from '../../common/PageContent/PageContent';
-import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
-import { styles as commonStyles, FormButtons } from '../../common';
import { trim } from '../../common/util';
-import StrategyParameters from './StrategyParameters/StrategyParameters';
-import { useHistory } from 'react-router-dom';
-import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStrategiesApi';
-import { IStrategy } from '../../../interfaces/strategy';
-import useToast from '../../../hooks/useToast';
-import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
-import { formatUnknownError } from '../../../utils/format-unknown-error';
-
-interface ICustomStrategyParams {
- name?: string;
- type?: string;
- description?: string;
- required?: boolean;
-}
-
-interface ICustomStrategyErrors {
- name?: string;
-}
+import { StrategyParameters } from './StrategyParameters/StrategyParameters';
+import { ICustomStrategyParameter } from 'interfaces/strategy';
interface IStrategyFormProps {
- editMode: boolean;
- strategy: IStrategy;
+ strategyName: string;
+ strategyDesc: string;
+ params: ICustomStrategyParameter[];
+ setStrategyName: React.Dispatch>;
+ setStrategyDesc: React.Dispatch>;
+ setParams: React.Dispatch>;
+ handleSubmit: (e: React.FormEvent) => void;
+ handleCancel: () => void;
+ errors: { [key: string]: string };
+ mode: 'Create' | 'Edit';
+ clearErrors: () => void;
+ setErrors: React.Dispatch>>;
}
-export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
- const history = useHistory();
- const [name, setName] = useState(strategy?.name || '');
- const [description, setDescription] = useState(strategy?.description || '');
- const [params, setParams] = useState(
- // @ts-expect-error
- strategy?.parameters || []
- );
- const [errors, setErrors] = useState({});
- const { createStrategy, updateStrategy } = useStrategiesApi();
- const { refetchStrategies } = useStrategies();
- const { setToastData, setToastApiError } = useToast();
-
- const clearErrors = () => {
- setErrors({});
- };
-
- const getHeaderTitle = () => {
- if (editMode) return 'Edit strategy';
- return 'Create a new strategy';
- };
-
- const appParameter = () => {
- setParams(prev => [...prev, {}]);
- };
+export const StrategyForm: React.FC = ({
+ children,
+ handleSubmit,
+ handleCancel,
+ strategyName,
+ strategyDesc,
+ params,
+ setParams,
+ setStrategyName,
+ setStrategyDesc,
+ errors,
+ mode,
+ clearErrors,
+}) => {
+ const styles = useStyles();
const updateParameter = (index: number, updated: object) => {
let item = { ...params[index] };
params[index] = Object.assign({}, item, updated);
setParams(prev => [...prev]);
};
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- const parameters = (params || [])
- .filter(({ name }) => !!name)
- .map(
- ({
- name,
- type = 'string',
- description = '',
- required = false,
- }) => ({
- name,
- type,
- description,
- required,
- })
- );
- setParams(prev => [...parameters]);
- if (editMode) {
- try {
- await updateStrategy({ name, description, parameters });
- history.push(`/strategies/view/${name}`);
- setToastData({
- type: 'success',
- title: 'Success',
- text: 'Successfully updated strategy',
- });
- refetchStrategies();
- } catch (error: unknown) {
- setToastApiError(formatUnknownError(error));
- }
- } else {
- try {
- await createStrategy({ name, description, parameters });
- history.push(`/strategies`);
- setToastData({
- type: 'success',
- title: 'Success',
- text: 'Successfully created new strategy',
- });
- refetchStrategies();
- } catch (error: unknown) {
- setToastApiError(formatUnknownError(error));
- }
- }
+ const appParameter = () => {
+ setParams(prev => [
+ ...prev,
+ { name: '', type: 'string', description: '', required: false },
+ ]);
};
- const handleCancel = () => history.goBack();
-
return (
-
-
- Be careful! Changing a strategy definition might also
- require changes to the implementation in the clients.
-
- }
- />
+
+ setStrategyDesc(e.target.value)}
rows={2}
- label="Description"
- name="description"
- placeholder=""
- onChange={e => setDescription(e.target.value)}
- value={description}
- variant="outlined"
- size="small"
+ multiline
/>
-
-
- Save
-
- }
- elseShow={
-
- }
- />
-
-
+
+
+ {children}
+
+
+
);
};
diff --git a/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.jsx b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.jsx
deleted file mode 100644
index e27d0b3135..0000000000
--- a/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { TextField, Checkbox, FormControlLabel } from '@material-ui/core';
-import PropTypes from 'prop-types';
-
-import { styles as commonStyles } from '../../../../common';
-import GeneralSelect from '../../../../common/GeneralSelect/GeneralSelect';
-
-const paramTypesOptions = [
- { key: 'string', label: 'string' },
- { key: 'percentage', label: 'percentage' },
- { key: 'list', label: 'list' },
- { key: 'number', label: 'number' },
- { key: 'boolean', label: 'boolean' },
-];
-
-const StrategyParameter = ({ set, input = {}, index }) => {
- const handleTypeChange = event => {
- set({ type: event.target.value });
- };
-
- return (
-
- set({ name: target.value }, true)}
- value={input.name || ''}
- variant="outlined"
- size="small"
- />
-
-
- set({ description: target.value })}
- value={input.description || ''}
- variant="outlined"
- size="small"
- />
- set({ required: !input.required })}
- />
- }
- label="Required"
- />
-
- );
-};
-
-StrategyParameter.propTypes = {
- input: PropTypes.object,
- set: PropTypes.func,
- index: PropTypes.number,
-};
-
-export default StrategyParameter;
diff --git a/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.styles.ts b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.styles.ts
new file mode 100644
index 0000000000..92f6a1a7b7
--- /dev/null
+++ b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.styles.ts
@@ -0,0 +1,43 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ paramsContainer: {
+ maxWidth: '400px',
+ },
+ divider: { borderStyle: 'dashed', marginBottom: '1rem !important' },
+ nameContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ marginBottom: '1rem',
+ },
+ name: {
+ minWidth: '365px',
+ width: '100%',
+ },
+ input: { minWidth: '365px', width: '100%', marginBottom: '1rem' },
+ description: {
+ minWidth: '365px',
+ marginBottom: '1rem',
+ },
+ checkboxLabel: {
+ marginBottom: '1rem',
+ },
+ inputDescription: {
+ marginBottom: '0.5rem',
+ },
+ typeDescription: {
+ fontSize: theme.fontSizes.smallBody,
+ color: theme.palette.grey[600],
+ top: '-13px',
+ position: 'relative',
+ },
+ errorMessage: {
+ fontSize: theme.fontSizes.smallBody,
+ color: theme.palette.error.main,
+ position: 'absolute',
+ top: '-8px',
+ },
+ paramButton: {
+ color: theme.palette.primary.dark,
+ },
+}));
diff --git a/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.tsx b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.tsx
new file mode 100644
index 0000000000..a69f918198
--- /dev/null
+++ b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameter/StrategyParameter.tsx
@@ -0,0 +1,132 @@
+import { Checkbox, FormControlLabel, IconButton } from '@material-ui/core';
+import { Delete } from '@material-ui/icons';
+import { useStyles } from './StrategyParameter.styles';
+import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
+import Input from 'component/common/Input/Input';
+import ConditionallyRender from 'component/common/ConditionallyRender';
+import React from 'react';
+import { ICustomStrategyParameter } from 'interfaces/strategy';
+
+const paramTypesOptions = [
+ {
+ key: 'string',
+ label: 'string',
+ description: 'A string is a collection of characters',
+ },
+ {
+ key: 'percentage',
+ label: 'percentage',
+ description:
+ 'Percentage is used when you want to make your feature visible to a process part of your customers',
+ },
+ {
+ key: 'list',
+ label: 'list',
+ description:
+ 'A list is used when you want to define several parameters that must be met before your feature becomes visible to your customers',
+ },
+ {
+ key: 'number',
+ label: 'number',
+ description:
+ 'Number is used when you have one or more digits that must be met for your feature to be visible to your customers',
+ },
+ {
+ key: 'boolean',
+ label: 'boolean',
+ description:
+ 'A boolean value represents a truth value, which is either true or false',
+ },
+];
+
+interface IStrategyParameterProps {
+ set: React.Dispatch>;
+ input: ICustomStrategyParameter;
+ index: number;
+ params: ICustomStrategyParameter[];
+ setParams: React.Dispatch>;
+ errors: { [key: string]: string };
+}
+
+export const StrategyParameter = ({
+ set,
+ input,
+ index,
+ params,
+ setParams,
+ errors,
+}: IStrategyParameterProps) => {
+ const styles = useStyles();
+ const handleTypeChange = (
+ event: React.ChangeEvent<{ name?: string; value: unknown }>
+ ) => {
+ set({ type: event.target.value });
+ };
+
+ const renderParamTypeDescription = () => {
+ return paramTypesOptions.find(param => param.key === input.type)
+ ?.description;
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.jsx b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.jsx
deleted file mode 100644
index 7ce319ab65..0000000000
--- a/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import StrategyParameter from './StrategyParameter/StrategyParameter';
-import PropTypes from 'prop-types';
-
-function gerArrayWithEntries(num) {
- return Array.from(Array(num));
-}
-
-const StrategyParameters = ({ input = [], count = 0, updateParameter }) => (
-
- {gerArrayWithEntries(count).map((v, i) => (
- updateParameter(i, v)}
- index={i}
- input={input[i]}
- />
- ))}
-
-);
-
-StrategyParameters.propTypes = {
- input: PropTypes.array,
- updateParameter: PropTypes.func.isRequired,
- count: PropTypes.number,
-};
-
-export default StrategyParameters;
diff --git a/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.tsx b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.tsx
new file mode 100644
index 0000000000..12c9383902
--- /dev/null
+++ b/frontend/src/component/strategies/StrategyForm/StrategyParameters/StrategyParameters.tsx
@@ -0,0 +1,31 @@
+import { StrategyParameter } from './StrategyParameter/StrategyParameter';
+import React from 'react';
+import { ICustomStrategyParameter } from 'interfaces/strategy';
+
+interface IStrategyParametersProps {
+ input: ICustomStrategyParameter[];
+ updateParameter: (index: number, updated: object) => void;
+ setParams: React.Dispatch>;
+ errors: { [key: string]: string };
+}
+
+export const StrategyParameters = ({
+ input = [],
+ updateParameter,
+ setParams,
+ errors,
+}: IStrategyParametersProps) => (
+
+ {input.map((item, index) => (
+ updateParameter(index, value)}
+ index={index}
+ input={input[index]}
+ setParams={setParams}
+ errors={errors}
+ />
+ ))}
+
+);
diff --git a/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx b/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx
index 8016dd31a1..ad85bc7281 100644
--- a/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx
+++ b/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx
@@ -7,13 +7,13 @@ import {
Tooltip,
} from '@material-ui/core';
import { Add, RadioButtonChecked } from '@material-ui/icons';
-import { AppsLinkList } from '../../../common';
-import ConditionallyRender from '../../../common/ConditionallyRender';
+import { AppsLinkList } from 'component/common';
+import ConditionallyRender from 'component/common/ConditionallyRender';
import styles from '../../strategies.module.scss';
import { TogglesLinkList } from '../../TogglesLinkList/TogglesLinkList';
-import { IParameter, IStrategy } from '../../../../interfaces/strategy';
-import { IApplication } from '../../../../interfaces/application';
-import { IFeatureToggle } from '../../../../interfaces/featureToggle';
+import { IParameter, IStrategy } from 'interfaces/strategy';
+import { IApplication } from 'interfaces/application';
+import { IFeatureToggle } from 'interfaces/featureToggle';
interface IStrategyDetailsProps {
strategy: IStrategy;
@@ -28,7 +28,7 @@ export const StrategyDetails = ({
}: IStrategyDetailsProps) => {
const { parameters = [] } = strategy;
const renderParameters = (params: IParameter[]) => {
- if (params) {
+ if (params.length > 0) {
return params.map(({ name, type, description, required }, i) => (
));
} else {
- return (no params);
+ return No params;
}
};
diff --git a/frontend/src/component/strategies/StrategyView/StrategyView.tsx b/frontend/src/component/strategies/StrategyView/StrategyView.tsx
index 5d968dd109..c0f9caf52b 100644
--- a/frontend/src/component/strategies/StrategyView/StrategyView.tsx
+++ b/frontend/src/component/strategies/StrategyView/StrategyView.tsx
@@ -1,77 +1,64 @@
-import { useContext } from 'react';
-import { Grid, Typography } from '@material-ui/core';
-import { StrategyForm } from '../StrategyForm/StrategyForm';
-import { UPDATE_STRATEGY } from '../../providers/AccessProvider/permissions';
-import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
-import TabNav from '../../common/TabNav/TabNav';
-import PageContent from '../../common/PageContent/PageContent';
-import AccessContext from '../../../contexts/AccessContext';
-import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
-import { useParams } from 'react-router-dom';
-import { useFeatures } from '../../../hooks/api/getters/useFeatures/useFeatures';
-import useApplications from '../../../hooks/api/getters/useApplications/useApplications';
+import { Grid } from '@material-ui/core';
+import { UPDATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
+import PageContent from 'component/common/PageContent/PageContent';
+import useStrategies from 'component/../hooks/api/getters/useStrategies/useStrategies';
+import { useHistory, useParams } from 'react-router-dom';
+import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
+import useApplications from 'hooks/api/getters/useApplications/useApplications';
import { StrategyDetails } from './StrategyDetails/StrategyDetails';
+import HeaderTitle from 'component/common/HeaderTitle';
+import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
+import { Edit } from '@material-ui/icons';
+import ConditionallyRender from 'component/common/ConditionallyRender';
export const StrategyView = () => {
- const { hasAccess } = useContext(AccessContext);
- const { strategyName } = useParams<{ strategyName: string }>();
+ const { name } = useParams<{ name: string }>();
const { strategies } = useStrategies();
const { features } = useFeatures();
const { applications } = useApplications();
+ const history = useHistory();
const toggles = features.filter(toggle => {
- return toggle?.strategies?.find(s => s.name === strategyName);
+ return toggle?.strategies?.find(strategy => strategy.name === name);
});
- const strategy = strategies.find(n => n.name === strategyName);
+ const strategy = strategies.find(strategy => strategy.name === name);
- const tabData = [
- {
- label: 'Details',
- component: (
-
- ),
- },
- {
- label: 'Edit',
- // @ts-expect-error
- component: ,
- },
- ];
+ const handleEdit = () => {
+ history.push(`/strategies/${name}/edit`);
+ };
if (!strategy) return null;
return (
-
+
+
+
+ }
+ />
+ }
+ />
+ }
+ >
-
- {strategy.description}
-
-
-
-
- }
- elseShow={
-
- }
+
diff --git a/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap
index 073d9db764..d1f70d522a 100644
--- a/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap
+++ b/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap
@@ -11,23 +11,23 @@ exports[`renders correctly with one strategy 1`] = `
}
>
Strategies
+
+
+
Strategies
+
+
+
{
+ const [strategyName, setStrategyName] = useState(initialStrategyName);
+ const [strategyDesc, setStrategyDesc] = useState(initialStrategyDesc);
+ const [params, setParams] = useState(initialParams);
+ const [errors, setErrors] = useState({});
+ const { strategies } = useStrategies();
+
+ useEffect(() => {
+ setStrategyName(initialStrategyName);
+ /* eslint-disable-next-line */
+ }, [initialStrategyName]);
+
+ useEffect(() => {
+ setStrategyDesc(initialStrategyDesc);
+ /* eslint-disable-next-line */
+ }, [initialStrategyDesc]);
+
+ useEffect(() => {
+ setParams(initialParams);
+ /* eslint-disable-next-line */
+ }, [JSON.stringify(initialParams)]);
+
+ const getStrategyPayload = () => {
+ return {
+ name: strategyName,
+ description: strategyDesc,
+ parameters: params,
+ };
+ };
+
+ const validateStrategyName = () => {
+ if (strategyName.length === 0) {
+ setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
+ return false;
+ }
+ if (strategies.some(strategy => strategy.name === strategyName)) {
+ setErrors(prev => ({
+ ...prev,
+ name: 'A strategy name with that name already exist',
+ }));
+ return false;
+ }
+ return true;
+ };
+
+ const validateParams = () => {
+ let res = true;
+ // eslint-disable-next-line
+ for (const [index, p] of Object.entries(params)) {
+ // eslint-disable-next-line
+ params.forEach((p, index) => {
+ if (p.name.length === 0) {
+ setErrors(prev => ({
+ ...prev,
+ [`paramName${index}`]: 'Name can not be empty',
+ }));
+ res = false;
+ }
+ });
+ }
+ return res;
+ };
+
+ const clearErrors = () => {
+ setErrors({});
+ };
+
+ return {
+ strategyName,
+ strategyDesc,
+ params,
+ setStrategyName,
+ setStrategyDesc,
+ setParams,
+ getStrategyPayload,
+ validateStrategyName,
+ validateParams,
+ setErrors,
+ clearErrors,
+ errors,
+ };
+};
diff --git a/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx b/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx
index fea1001cdb..e962fc0714 100644
--- a/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx
+++ b/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx
@@ -14,7 +14,7 @@ interface ITagTypeForm {
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
- mode: string;
+ mode: 'Create' | 'Edit';
clearErrors: () => void;
validateNameUniqueness?: () => void;
}
diff --git a/frontend/src/hooks/api/actions/useStrategiesApi/useStrategiesApi.ts b/frontend/src/hooks/api/actions/useStrategiesApi/useStrategiesApi.ts
index 867e97fe42..d8cf8fd0d4 100644
--- a/frontend/src/hooks/api/actions/useStrategiesApi/useStrategiesApi.ts
+++ b/frontend/src/hooks/api/actions/useStrategiesApi/useStrategiesApi.ts
@@ -1,11 +1,6 @@
+import { ICustomStrategyPayload } from 'interfaces/strategy';
import useAPI from '../useApi/useApi';
-export interface ICustomStrategyPayload {
- name: string;
- description: string;
- parameters: object[];
-}
-
const useStrategiesApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
diff --git a/frontend/src/hooks/api/getters/useStrategy/defaultStrategy.ts b/frontend/src/hooks/api/getters/useStrategy/defaultStrategy.ts
new file mode 100644
index 0000000000..a89ba584af
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useStrategy/defaultStrategy.ts
@@ -0,0 +1,10 @@
+import { IStrategy } from 'interfaces/strategy';
+
+export const defaultStrategy: IStrategy = {
+ name: '',
+ description: '',
+ displayName: '',
+ editable: false,
+ deprecated: false,
+ parameters: [],
+};
diff --git a/frontend/src/hooks/api/getters/useStrategy/useStrategy.ts b/frontend/src/hooks/api/getters/useStrategy/useStrategy.ts
new file mode 100644
index 0000000000..6ecbafc43c
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useStrategy/useStrategy.ts
@@ -0,0 +1,30 @@
+import useSWR, { mutate, SWRConfiguration } from 'swr';
+import { formatApiPath } from 'utils/format-path';
+import handleErrorResponses from '../httpErrorResponseHandler';
+import { defaultStrategy } from './defaultStrategy';
+
+const useStrategy = (strategyName: string, options: SWRConfiguration = {}) => {
+ const STRATEGY_CACHE_KEY = `api/admin/strategies/${strategyName}`;
+ const path = formatApiPath(STRATEGY_CACHE_KEY);
+
+ const fetcher = () => {
+ return fetch(path)
+ .then(handleErrorResponses(`${strategyName} strategy`))
+ .then(res => res.json());
+ };
+
+ const { data, error } = useSWR(STRATEGY_CACHE_KEY, fetcher, options);
+
+ const refetchStrategy = () => {
+ mutate(STRATEGY_CACHE_KEY);
+ };
+
+ return {
+ strategy: data || defaultStrategy,
+ error,
+ loading: !error && !data,
+ refetchStrategy,
+ };
+};
+
+export default useStrategy;
diff --git a/frontend/src/interfaces/strategy.ts b/frontend/src/interfaces/strategy.ts
index dc5bc099c8..3841258135 100644
--- a/frontend/src/interfaces/strategy.ts
+++ b/frontend/src/interfaces/strategy.ts
@@ -39,3 +39,22 @@ export interface IStrategyPayload {
constraints: IConstraint[];
parameters: IParameter;
}
+export interface ICustomStrategyParameter {
+ name: string;
+ description: string;
+ required: boolean;
+ type: string;
+}
+
+export interface ICustomStrategyPayload {
+ name: string;
+ description: string;
+ parameters: IParameter[];
+}
+
+export interface ICustomStrategy {
+ name: string;
+ description: string;
+ parameters: IParameter[];
+ editable: boolean;
+}