mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-13 11:17:26 +02:00
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 <fredrik.no@gmail.com> * 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 <fredrik.no@gmail.com> Co-authored-by: Renovate Bot <bot@renovateapp.com> Co-authored-by: olav <mail@olav.io>
This commit is contained in:
parent
fa33bd3ddd
commit
ee730e0708
@ -18,7 +18,7 @@ interface IApiTokenFormProps {
|
|||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: string;
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
}
|
}
|
||||||
const ApiTokenForm: React.FC<IApiTokenFormProps> = ({
|
const ApiTokenForm: React.FC<IApiTokenFormProps> = ({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core';
|
import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core';
|
||||||
import { SELECT_ITEM_ID } from '../../../testIds';
|
import { SELECT_ITEM_ID } from '../../../testIds';
|
||||||
|
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
|
||||||
|
|
||||||
export interface ISelectOption {
|
export interface ISelectOption {
|
||||||
key: string;
|
key: string;
|
||||||
@ -71,6 +72,7 @@ const GeneralSelect: React.FC<ISelectMenuProps> = ({
|
|||||||
label={label}
|
label={label}
|
||||||
id={id}
|
id={id}
|
||||||
value={value}
|
value={value}
|
||||||
|
IconComponent={KeyboardArrowDownOutlined}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{renderSelectItems()}
|
{renderSelectItems()}
|
||||||
|
@ -17,7 +17,7 @@ interface IContextForm {
|
|||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: string;
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
validateContext?: () => void;
|
validateContext?: () => void;
|
||||||
setErrors: React.Dispatch<React.SetStateAction<Object>>;
|
setErrors: React.Dispatch<React.SetStateAction<Object>>;
|
||||||
|
@ -14,7 +14,7 @@ interface IEnvironmentForm {
|
|||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: string;
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ interface IFeatureToggleForm {
|
|||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: string;
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,8 +208,17 @@ Array [
|
|||||||
"layout": "main",
|
"layout": "main",
|
||||||
"menu": Object {},
|
"menu": Object {},
|
||||||
"parent": "/strategies",
|
"parent": "/strategies",
|
||||||
"path": "/strategies/:activeTab/:strategyName",
|
"path": "/strategies/:name/edit",
|
||||||
"title": ":strategyName",
|
"title": ":name",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"layout": "main",
|
||||||
|
"menu": Object {},
|
||||||
|
"parent": "/strategies",
|
||||||
|
"path": "/strategies/:name",
|
||||||
|
"title": ":name",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
|
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
|
||||||
import { StrategyForm } from '../strategies/StrategyForm/StrategyForm';
|
|
||||||
import { StrategyView } from '../strategies/StrategyView/StrategyView';
|
import { StrategyView } from '../strategies/StrategyView/StrategyView';
|
||||||
import { StrategiesList } from '../strategies/StrategiesList/StrategiesList';
|
import { StrategiesList } from '../strategies/StrategiesList/StrategiesList';
|
||||||
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
|
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
|
||||||
@ -45,6 +44,8 @@ import { EditAddon } from '../addons/EditAddon/EditAddon';
|
|||||||
import { CopyFeatureToggle } from '../feature/CopyFeature/CopyFeature';
|
import { CopyFeatureToggle } from '../feature/CopyFeature/CopyFeature';
|
||||||
import { EventHistoryPage } from '../history/EventHistoryPage/EventHistoryPage';
|
import { EventHistoryPage } from '../history/EventHistoryPage/EventHistoryPage';
|
||||||
import { FeatureEventHistoryPage } from '../history/FeatureEventHistoryPage/FeatureEventHistoryPage';
|
import { FeatureEventHistoryPage } from '../history/FeatureEventHistoryPage/FeatureEventHistoryPage';
|
||||||
|
import { CreateStrategy } from '../strategies/CreateStrategy/CreateStrategy';
|
||||||
|
import { EditStrategy } from '../strategies/EditStrategy/EditStrategy';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Project
|
// Project
|
||||||
@ -243,14 +244,23 @@ export const routes = [
|
|||||||
path: '/strategies/create',
|
path: '/strategies/create',
|
||||||
title: 'Create',
|
title: 'Create',
|
||||||
parent: '/strategies',
|
parent: '/strategies',
|
||||||
component: StrategyForm,
|
component: CreateStrategy,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/strategies/:activeTab/:strategyName',
|
path: '/strategies/:name/edit',
|
||||||
title: ':strategyName',
|
title: ':name',
|
||||||
|
parent: '/strategies',
|
||||||
|
component: EditStrategy,
|
||||||
|
type: 'protected',
|
||||||
|
layout: 'main',
|
||||||
|
menu: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/strategies/:name',
|
||||||
|
title: ':name',
|
||||||
parent: '/strategies',
|
parent: '/strategies',
|
||||||
component: StrategyView,
|
component: StrategyView,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
|
@ -14,7 +14,7 @@ interface IProjectForm {
|
|||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: string;
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
validateIdUniqueness: () => void;
|
validateIdUniqueness: () => void;
|
||||||
}
|
}
|
||||||
|
@ -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<HTMLFormElement>) => {
|
||||||
|
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 (
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
title="Create strategy type"
|
||||||
|
description="The strategy type and the parameters will be selectable when adding an activation strategy to a toggle in the environments.
|
||||||
|
The parameter defines the type of activation strategy. E.g. you can create a type 'Teams' and add a parameter 'List'. Then it's easy to add team names to the activation strategy"
|
||||||
|
documentationLink="https://docs.getunleash.io/advanced/custom_activation_strategy"
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<StrategyForm
|
||||||
|
errors={errors}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
strategyName={strategyName}
|
||||||
|
setStrategyName={setStrategyName}
|
||||||
|
strategyDesc={strategyDesc}
|
||||||
|
setStrategyDesc={setStrategyDesc}
|
||||||
|
params={params}
|
||||||
|
setParams={setParams}
|
||||||
|
mode="Create"
|
||||||
|
setErrors={setErrors}
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
>
|
||||||
|
<PermissionButton permission={CREATE_STRATEGY} type="submit">
|
||||||
|
Create strategy
|
||||||
|
</PermissionButton>
|
||||||
|
</StrategyForm>
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
102
frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
Normal file
102
frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
Normal file
@ -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<HTMLFormElement>) => {
|
||||||
|
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 (
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
title="Edit strategy type"
|
||||||
|
description="The strategy type and the parameters will be selectable when adding an activation strategy to a toggle in the environments.
|
||||||
|
The parameter defines the type of activation strategy. E.g. you can create a type 'Teams' and add a parameter 'List'. Then it's easy to add team names to the activation strategy"
|
||||||
|
documentationLink="https://docs.getunleash.io/advanced/custom_activation_strategy"
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<StrategyForm
|
||||||
|
errors={errors}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
strategyName={strategyName}
|
||||||
|
setStrategyName={setStrategyName}
|
||||||
|
strategyDesc={strategyDesc}
|
||||||
|
setStrategyDesc={setStrategyDesc}
|
||||||
|
params={params}
|
||||||
|
setParams={setParams}
|
||||||
|
mode="Edit"
|
||||||
|
setErrors={setErrors}
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
>
|
||||||
|
<PermissionButton permission={CREATE_STRATEGY} type="submit">
|
||||||
|
Save
|
||||||
|
</PermissionButton>
|
||||||
|
</StrategyForm>
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
@ -5,16 +5,10 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
padding: '0',
|
padding: '0',
|
||||||
['& a']: {
|
['& a']: {
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: 'inherit',
|
color: theme.palette.primary.light,
|
||||||
},
|
},
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.grey[200],
|
backgroundColor: theme.palette.grey[200],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
deprecated: {
|
|
||||||
'& a': {
|
|
||||||
// @ts-expect-error
|
|
||||||
color: theme.palette.links.deprecated,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import classnames from 'classnames';
|
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
import {
|
import {
|
||||||
@ -13,6 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Add,
|
Add,
|
||||||
Delete,
|
Delete,
|
||||||
|
Edit,
|
||||||
Extension,
|
Extension,
|
||||||
Visibility,
|
Visibility,
|
||||||
VisibilityOff,
|
VisibilityOff,
|
||||||
@ -22,21 +22,21 @@ import {
|
|||||||
DELETE_STRATEGY,
|
DELETE_STRATEGY,
|
||||||
UPDATE_STRATEGY,
|
UPDATE_STRATEGY,
|
||||||
} from '../../providers/AccessProvider/permissions';
|
} from '../../providers/AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from 'component/common/PageContent/PageContent';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from 'component/common/HeaderTitle';
|
||||||
import { useStyles } from './StrategiesList.styles';
|
import { useStyles } from './StrategiesList.styles';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import Dialogue from '../../common/Dialogue';
|
import Dialogue from 'component/common/Dialogue';
|
||||||
import { ADD_NEW_STRATEGY_ID } from '../../../testIds';
|
import { ADD_NEW_STRATEGY_ID } from 'testIds';
|
||||||
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { getHumanReadableStrategyName } from '../../../utils/strategy-names';
|
import { getHumanReadableStrategyName } from 'utils/strategy-names';
|
||||||
import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
|
import useStrategies from 'hooks/api/getters/useStrategies/useStrategies';
|
||||||
import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStrategiesApi';
|
import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesApi';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { IStrategy } from '../../../interfaces/strategy';
|
import { formatUnknownError } from 'utils/format-unknown-error';
|
||||||
import { formatUnknownError } from '../../../utils/format-unknown-error';
|
import { ICustomStrategy } from 'interfaces/strategy';
|
||||||
|
|
||||||
interface IDialogueMetaData {
|
interface IDialogueMetaData {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@ -91,8 +91,8 @@ export const StrategiesList = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const strategyLink = ({ name, deprecated }: IStrategy) => (
|
const strategyLink = (name: string, deprecated: boolean) => (
|
||||||
<Link to={`/strategies/view/${name}`}>
|
<Link to={`/strategies/${name}`}>
|
||||||
<strong>{getHumanReadableStrategyName(name)}</strong>
|
<strong>{getHumanReadableStrategyName(name)}</strong>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={deprecated}
|
condition={deprecated}
|
||||||
@ -101,7 +101,7 @@ export const StrategiesList = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReactivateStrategy = (strategy: IStrategy) => {
|
const onReactivateStrategy = (strategy: ICustomStrategy) => {
|
||||||
setDialogueMetaData({
|
setDialogueMetaData({
|
||||||
show: true,
|
show: true,
|
||||||
title: 'Really reactivate strategy?',
|
title: 'Really reactivate strategy?',
|
||||||
@ -121,7 +121,7 @@ export const StrategiesList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDeprecateStrategy = (strategy: IStrategy) => {
|
const onDeprecateStrategy = (strategy: ICustomStrategy) => {
|
||||||
setDialogueMetaData({
|
setDialogueMetaData({
|
||||||
show: true,
|
show: true,
|
||||||
title: 'Really deprecate strategy?',
|
title: 'Really deprecate strategy?',
|
||||||
@ -141,7 +141,7 @@ export const StrategiesList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteStrategy = (strategy: IStrategy) => {
|
const onDeleteStrategy = (strategy: ICustomStrategy) => {
|
||||||
setDialogueMetaData({
|
setDialogueMetaData({
|
||||||
show: true,
|
show: true,
|
||||||
title: 'Really delete strategy?',
|
title: 'Really delete strategy?',
|
||||||
@ -161,7 +161,7 @@ export const StrategiesList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const reactivateButton = (strategy: IStrategy) => (
|
const reactivateButton = (strategy: ICustomStrategy) => (
|
||||||
<Tooltip title="Reactivate activation strategy">
|
<Tooltip title="Reactivate activation strategy">
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
onClick={() => onReactivateStrategy(strategy)}
|
onClick={() => onReactivateStrategy(strategy)}
|
||||||
@ -172,7 +172,7 @@ export const StrategiesList = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
const deprecateButton = (strategy: IStrategy) => (
|
const deprecateButton = (strategy: ICustomStrategy) => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategy.name === 'default'}
|
condition={strategy.name === 'default'}
|
||||||
show={
|
show={
|
||||||
@ -198,9 +198,35 @@ export const StrategiesList = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteButton = (strategy: IStrategy) => (
|
const editButton = (strategy: ICustomStrategy) => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategy.editable}
|
condition={strategy?.editable}
|
||||||
|
show={
|
||||||
|
<PermissionIconButton
|
||||||
|
onClick={() =>
|
||||||
|
history.push(`/strategies/${strategy?.name}/edit`)
|
||||||
|
}
|
||||||
|
permission={UPDATE_STRATEGY}
|
||||||
|
tooltip={'Edit strategy'}
|
||||||
|
>
|
||||||
|
<Edit titleAccess="Edit strategy" />
|
||||||
|
</PermissionIconButton>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Tooltip title="You cannot delete a built-in strategy">
|
||||||
|
<div>
|
||||||
|
<IconButton disabled>
|
||||||
|
<Edit titleAccess="Edit strategy" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteButton = (strategy: ICustomStrategy) => (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={strategy?.editable}
|
||||||
show={
|
show={
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
onClick={() => onDeleteStrategy(strategy)}
|
onClick={() => onDeleteStrategy(strategy)}
|
||||||
@ -223,19 +249,12 @@ export const StrategiesList = () => {
|
|||||||
|
|
||||||
const strategyList = () =>
|
const strategyList = () =>
|
||||||
strategies.map(strategy => (
|
strategies.map(strategy => (
|
||||||
<ListItem
|
<ListItem key={strategy.name} className={styles.listItem}>
|
||||||
key={strategy.name}
|
|
||||||
classes={{
|
|
||||||
root: classnames(styles.listItem, {
|
|
||||||
[styles.deprecated]: strategy.deprecated,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Extension style={{ color: '#0000008a' }} />
|
<Extension style={{ color: '#0000008a' }} />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={strategyLink(strategy)}
|
primary={strategyLink(strategy?.name, strategy?.deprecated)}
|
||||||
secondary={strategy.description}
|
secondary={strategy.description}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -243,6 +262,10 @@ export const StrategiesList = () => {
|
|||||||
show={reactivateButton(strategy)}
|
show={reactivateButton(strategy)}
|
||||||
elseShow={deprecateButton(strategy)}
|
elseShow={deprecateButton(strategy)}
|
||||||
/>
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(UPDATE_STRATEGY)}
|
||||||
|
show={editButton(strategy)}
|
||||||
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(DELETE_STRATEGY)}
|
condition={hasAccess(DELETE_STRATEGY)}
|
||||||
show={deleteButton(strategy)}
|
show={deleteButton(strategy)}
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}));
|
@ -1,195 +1,114 @@
|
|||||||
import React, { useState } from 'react';
|
import Input from '../../common/Input/Input';
|
||||||
import { Typography, TextField, Button } from '@material-ui/core';
|
import { Button } from '@material-ui/core';
|
||||||
|
import { useStyles } from './StrategyForm.styles';
|
||||||
import { Add } from '@material-ui/icons';
|
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 { trim } from '../../common/util';
|
||||||
import StrategyParameters from './StrategyParameters/StrategyParameters';
|
import { StrategyParameters } from './StrategyParameters/StrategyParameters';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { ICustomStrategyParameter } from 'interfaces/strategy';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IStrategyFormProps {
|
interface IStrategyFormProps {
|
||||||
editMode: boolean;
|
strategyName: string;
|
||||||
strategy: IStrategy;
|
strategyDesc: string;
|
||||||
|
params: ICustomStrategyParameter[];
|
||||||
|
setStrategyName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setStrategyDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setParams: React.Dispatch<React.SetStateAction<ICustomStrategyParameter[]>>;
|
||||||
|
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
handleCancel: () => void;
|
||||||
|
errors: { [key: string]: string };
|
||||||
|
mode: 'Create' | 'Edit';
|
||||||
|
clearErrors: () => void;
|
||||||
|
setErrors: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
||||||
}
|
}
|
||||||
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<ICustomStrategyParams[]>(
|
|
||||||
// @ts-expect-error
|
|
||||||
strategy?.parameters || []
|
|
||||||
);
|
|
||||||
const [errors, setErrors] = useState<ICustomStrategyErrors>({});
|
|
||||||
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<IStrategyFormProps> = ({
|
||||||
|
children,
|
||||||
|
handleSubmit,
|
||||||
|
handleCancel,
|
||||||
|
strategyName,
|
||||||
|
strategyDesc,
|
||||||
|
params,
|
||||||
|
setParams,
|
||||||
|
setStrategyName,
|
||||||
|
setStrategyDesc,
|
||||||
|
errors,
|
||||||
|
mode,
|
||||||
|
clearErrors,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles();
|
||||||
const updateParameter = (index: number, updated: object) => {
|
const updateParameter = (index: number, updated: object) => {
|
||||||
let item = { ...params[index] };
|
let item = { ...params[index] };
|
||||||
params[index] = Object.assign({}, item, updated);
|
params[index] = Object.assign({}, item, updated);
|
||||||
setParams(prev => [...prev]);
|
setParams(prev => [...prev]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const appParameter = () => {
|
||||||
e.preventDefault();
|
setParams(prev => [
|
||||||
const parameters = (params || [])
|
...prev,
|
||||||
.filter(({ name }) => !!name)
|
{ name: '', type: 'string', description: '', required: false },
|
||||||
.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 handleCancel = () => history.goBack();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent={getHeaderTitle()}>
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
<ConditionallyRender
|
<h3 className={styles.formHeader}>Strategy type information</h3>
|
||||||
condition={editMode}
|
|
||||||
show={
|
|
||||||
<Typography variant="body1">
|
|
||||||
Be careful! Changing a strategy definition might also
|
|
||||||
require changes to the implementation in the clients.
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form
|
<div className={styles.container}>
|
||||||
onSubmit={handleSubmit}
|
<p className={styles.inputDescription}>
|
||||||
className={commonStyles.contentSpacing}
|
What would you like to call your strategy?
|
||||||
style={{ maxWidth: '400px' }}
|
</p>
|
||||||
>
|
<Input
|
||||||
<TextField
|
disabled={mode === 'Edit'}
|
||||||
label="Strategy name"
|
autoFocus
|
||||||
name="name"
|
className={styles.input}
|
||||||
placeholder=""
|
label="Strategy name*"
|
||||||
disabled={editMode}
|
value={strategyName}
|
||||||
|
onChange={e => setStrategyName(trim(e.target.value))}
|
||||||
error={Boolean(errors.name)}
|
error={Boolean(errors.name)}
|
||||||
helperText={errors.name}
|
errorText={errors.name}
|
||||||
onChange={e => {
|
onFocus={() => clearErrors()}
|
||||||
clearErrors();
|
|
||||||
setName(trim(e.target.value));
|
|
||||||
}}
|
|
||||||
value={name}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
/>
|
/>
|
||||||
|
<p className={styles.inputDescription}>
|
||||||
<TextField
|
What is your strategy description?
|
||||||
className={commonStyles.fullwidth}
|
</p>
|
||||||
multiline
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
label="Strategy description"
|
||||||
|
value={strategyDesc}
|
||||||
|
onChange={e => setStrategyDesc(e.target.value)}
|
||||||
rows={2}
|
rows={2}
|
||||||
label="Description"
|
multiline
|
||||||
name="description"
|
|
||||||
placeholder=""
|
|
||||||
onChange={e => setDescription(e.target.value)}
|
|
||||||
value={description}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StrategyParameters
|
<StrategyParameters
|
||||||
input={params}
|
input={params}
|
||||||
count={params.length}
|
|
||||||
updateParameter={updateParameter}
|
updateParameter={updateParameter}
|
||||||
|
setParams={setParams}
|
||||||
|
errors={errors}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
appParameter();
|
appParameter();
|
||||||
}}
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
className={styles.paramButton}
|
||||||
startIcon={<Add />}
|
startIcon={<Add />}
|
||||||
>
|
>
|
||||||
Add parameter
|
Add parameter
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
<ConditionallyRender
|
<div className={styles.buttonContainer}>
|
||||||
condition={editMode}
|
{children}
|
||||||
show={
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="submit"
|
onClick={handleCancel}
|
||||||
variant="contained"
|
className={styles.cancelButton}
|
||||||
color="primary"
|
>
|
||||||
style={{ display: 'block' }}
|
Cancel
|
||||||
>
|
</Button>
|
||||||
Save
|
</div>
|
||||||
</Button>
|
</form>
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<FormButtons
|
|
||||||
submitText={'Create'}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</PageContent>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
|
||||||
<div className={commonStyles.contentSpacing}>
|
|
||||||
<TextField
|
|
||||||
style={{ width: '50%', marginRight: '5px' }}
|
|
||||||
label={`Parameter name ${index + 1}`}
|
|
||||||
onChange={({ target }) => set({ name: target.value }, true)}
|
|
||||||
value={input.name || ''}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<GeneralSelect
|
|
||||||
label="Type"
|
|
||||||
options={paramTypesOptions}
|
|
||||||
value={input.type || 'string'}
|
|
||||||
onChange={handleTypeChange}
|
|
||||||
id={`prop-type-${index}-select`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
rows={2}
|
|
||||||
multiline
|
|
||||||
label={`Parameter name ${index + 1} description`}
|
|
||||||
onChange={({ target }) => set({ description: target.value })}
|
|
||||||
value={input.description || ''}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={!!input.required}
|
|
||||||
onChange={() => set({ required: !input.required })}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Required"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
StrategyParameter.propTypes = {
|
|
||||||
input: PropTypes.object,
|
|
||||||
set: PropTypes.func,
|
|
||||||
index: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StrategyParameter;
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}));
|
@ -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<React.SetStateAction<object>>;
|
||||||
|
input: ICustomStrategyParameter;
|
||||||
|
index: number;
|
||||||
|
params: ICustomStrategyParameter[];
|
||||||
|
setParams: React.Dispatch<React.SetStateAction<ICustomStrategyParameter[]>>;
|
||||||
|
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 (
|
||||||
|
<div className={styles.paramsContainer}>
|
||||||
|
<hr className={styles.divider} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={index === 0}
|
||||||
|
show={
|
||||||
|
<p className={styles.input}>
|
||||||
|
The parameters define how the strategy will look like.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
label={`Parameter name ${index + 1}*`}
|
||||||
|
onChange={e => set({ name: e.target.value })}
|
||||||
|
value={input.name}
|
||||||
|
className={styles.name}
|
||||||
|
error={Boolean(errors?.[`paramName${index}`])}
|
||||||
|
errorText={errors?.[`paramName${index}`]}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setParams(params.filter((e, i) => i !== index));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete titleAccess="Delete" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<GeneralSelect
|
||||||
|
label="Type*"
|
||||||
|
name="type"
|
||||||
|
options={paramTypesOptions}
|
||||||
|
value={input.type}
|
||||||
|
onChange={handleTypeChange}
|
||||||
|
id={`prop-type-${index}-select`}
|
||||||
|
className={styles.input}
|
||||||
|
/>
|
||||||
|
<p className={styles.typeDescription}>
|
||||||
|
{renderParamTypeDescription()}
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
rows={2}
|
||||||
|
multiline
|
||||||
|
label={`Parameter name ${index + 1} description`}
|
||||||
|
onChange={({ target }) => set({ description: target.value })}
|
||||||
|
value={input.description}
|
||||||
|
className={styles.description}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={Boolean(input.required)}
|
||||||
|
onChange={() => set({ required: !input.required })}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Required"
|
||||||
|
className={styles.checkboxLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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 }) => (
|
|
||||||
<div>
|
|
||||||
{gerArrayWithEntries(count).map((v, i) => (
|
|
||||||
<StrategyParameter
|
|
||||||
key={i}
|
|
||||||
set={v => updateParameter(i, v)}
|
|
||||||
index={i}
|
|
||||||
input={input[i]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
StrategyParameters.propTypes = {
|
|
||||||
input: PropTypes.array,
|
|
||||||
updateParameter: PropTypes.func.isRequired,
|
|
||||||
count: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StrategyParameters;
|
|
@ -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<React.SetStateAction<ICustomStrategyParameter[]>>;
|
||||||
|
errors: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StrategyParameters = ({
|
||||||
|
input = [],
|
||||||
|
updateParameter,
|
||||||
|
setParams,
|
||||||
|
errors,
|
||||||
|
}: IStrategyParametersProps) => (
|
||||||
|
<div>
|
||||||
|
{input.map((item, index) => (
|
||||||
|
<StrategyParameter
|
||||||
|
params={input}
|
||||||
|
key={index}
|
||||||
|
set={value => updateParameter(index, value)}
|
||||||
|
index={index}
|
||||||
|
input={input[index]}
|
||||||
|
setParams={setParams}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
@ -7,13 +7,13 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Add, RadioButtonChecked } from '@material-ui/icons';
|
import { Add, RadioButtonChecked } from '@material-ui/icons';
|
||||||
import { AppsLinkList } from '../../../common';
|
import { AppsLinkList } from 'component/common';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||||
import styles from '../../strategies.module.scss';
|
import styles from '../../strategies.module.scss';
|
||||||
import { TogglesLinkList } from '../../TogglesLinkList/TogglesLinkList';
|
import { TogglesLinkList } from '../../TogglesLinkList/TogglesLinkList';
|
||||||
import { IParameter, IStrategy } from '../../../../interfaces/strategy';
|
import { IParameter, IStrategy } from 'interfaces/strategy';
|
||||||
import { IApplication } from '../../../../interfaces/application';
|
import { IApplication } from 'interfaces/application';
|
||||||
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
interface IStrategyDetailsProps {
|
interface IStrategyDetailsProps {
|
||||||
strategy: IStrategy;
|
strategy: IStrategy;
|
||||||
@ -28,7 +28,7 @@ export const StrategyDetails = ({
|
|||||||
}: IStrategyDetailsProps) => {
|
}: IStrategyDetailsProps) => {
|
||||||
const { parameters = [] } = strategy;
|
const { parameters = [] } = strategy;
|
||||||
const renderParameters = (params: IParameter[]) => {
|
const renderParameters = (params: IParameter[]) => {
|
||||||
if (params) {
|
if (params.length > 0) {
|
||||||
return params.map(({ name, type, description, required }, i) => (
|
return params.map(({ name, type, description, required }, i) => (
|
||||||
<ListItem key={`${name}-${i}`}>
|
<ListItem key={`${name}-${i}`}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -59,7 +59,7 @@ export const StrategyDetails = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
return <ListItem>(no params)</ListItem>;
|
return <ListItem>No params</ListItem>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,77 +1,64 @@
|
|||||||
import { useContext } from 'react';
|
import { Grid } from '@material-ui/core';
|
||||||
import { Grid, Typography } from '@material-ui/core';
|
import { UPDATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
import { StrategyForm } from '../StrategyForm/StrategyForm';
|
import PageContent from 'component/common/PageContent/PageContent';
|
||||||
import { UPDATE_STRATEGY } from '../../providers/AccessProvider/permissions';
|
import useStrategies from 'component/../hooks/api/getters/useStrategies/useStrategies';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import TabNav from '../../common/TabNav/TabNav';
|
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
||||||
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 { StrategyDetails } from './StrategyDetails/StrategyDetails';
|
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 = () => {
|
export const StrategyView = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { name } = useParams<{ name: string }>();
|
||||||
const { strategyName } = useParams<{ strategyName: string }>();
|
|
||||||
const { strategies } = useStrategies();
|
const { strategies } = useStrategies();
|
||||||
const { features } = useFeatures();
|
const { features } = useFeatures();
|
||||||
const { applications } = useApplications();
|
const { applications } = useApplications();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const toggles = features.filter(toggle => {
|
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 = [
|
const handleEdit = () => {
|
||||||
{
|
history.push(`/strategies/${name}/edit`);
|
||||||
label: 'Details',
|
};
|
||||||
component: (
|
|
||||||
<StrategyDetails
|
|
||||||
// @ts-expect-error
|
|
||||||
strategy={strategy}
|
|
||||||
toggles={toggles}
|
|
||||||
applications={applications}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
// @ts-expect-error
|
|
||||||
component: <StrategyForm strategy={strategy} editMode />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!strategy) return null;
|
if (!strategy) return null;
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent={strategy.name}>
|
<PageContent
|
||||||
|
headerContent={
|
||||||
|
<HeaderTitle
|
||||||
|
title={strategy?.name}
|
||||||
|
subtitle={strategy?.description}
|
||||||
|
actions={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={strategy.editable}
|
||||||
|
show={
|
||||||
|
<PermissionIconButton
|
||||||
|
permission={UPDATE_STRATEGY}
|
||||||
|
tooltip={'Edit strategy'}
|
||||||
|
data-loading
|
||||||
|
onClick={handleEdit}
|
||||||
|
>
|
||||||
|
<Edit titleAccess="Edit strategy" />
|
||||||
|
</PermissionIconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12} sm={12}>
|
<Grid item xs={12} sm={12}>
|
||||||
<Typography variant="subtitle1">
|
<StrategyDetails
|
||||||
{strategy.description}
|
strategy={strategy}
|
||||||
</Typography>
|
toggles={toggles}
|
||||||
<ConditionallyRender
|
applications={applications}
|
||||||
condition={
|
|
||||||
strategy.editable && hasAccess(UPDATE_STRATEGY)
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<div>
|
|
||||||
<TabNav tabData={tabData} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<section>
|
|
||||||
<div className="content">
|
|
||||||
<StrategyDetails
|
|
||||||
strategy={strategy}
|
|
||||||
toggles={toggles}
|
|
||||||
applications={applications}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -11,23 +11,23 @@ exports[`renders correctly with one strategy 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-3"
|
className="makeStyles-headerContainer-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerTitleContainer-7"
|
className="makeStyles-headerTitleContainer-6"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className=""
|
className=""
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className="MuiTypography-root makeStyles-headerTitle-8 MuiTypography-h2"
|
className="MuiTypography-root makeStyles-headerTitle-7 MuiTypography-h2"
|
||||||
>
|
>
|
||||||
Strategies
|
Strategies
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerActions-9"
|
className="makeStyles-headerActions-8"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-describedby={null}
|
aria-describedby={null}
|
||||||
@ -76,7 +76,7 @@ exports[`renders correctly with one strategy 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-bodyContainer-4"
|
className="makeStyles-bodyContainer-3"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="MuiList-root MuiList-padding"
|
className="MuiList-root MuiList-padding"
|
||||||
@ -111,7 +111,7 @@ exports[`renders correctly with one strategy 1`] = `
|
|||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/strategies/view/flexibleRollout"
|
href="/strategies/flexibleRollout"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<strong>
|
<strong>
|
||||||
@ -175,6 +175,53 @@ exports[`renders correctly with one strategy 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-describedby={null}
|
||||||
|
className=""
|
||||||
|
onBlur={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
title="You cannot delete a built-in strategy"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
|
||||||
|
disabled={true}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={-1}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||||
|
/>
|
||||||
|
<title>
|
||||||
|
Edit strategy
|
||||||
|
</title>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-describedby={null}
|
aria-describedby={null}
|
||||||
className=""
|
className=""
|
||||||
@ -236,23 +283,23 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-3"
|
className="makeStyles-headerContainer-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerTitleContainer-7"
|
className="makeStyles-headerTitleContainer-6"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className=""
|
className=""
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className="MuiTypography-root makeStyles-headerTitle-8 MuiTypography-h2"
|
className="MuiTypography-root makeStyles-headerTitle-7 MuiTypography-h2"
|
||||||
>
|
>
|
||||||
Strategies
|
Strategies
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerActions-9"
|
className="makeStyles-headerActions-8"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-describedby={null}
|
aria-describedby={null}
|
||||||
@ -301,7 +348,7 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-bodyContainer-4"
|
className="makeStyles-bodyContainer-3"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="MuiList-root MuiList-padding"
|
className="MuiList-root MuiList-padding"
|
||||||
@ -336,7 +383,7 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
|||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/strategies/view/flexibleRollout"
|
href="/strategies/flexibleRollout"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<strong>
|
<strong>
|
||||||
@ -400,6 +447,53 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-describedby={null}
|
||||||
|
className=""
|
||||||
|
onBlur={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
title="You cannot delete a built-in strategy"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
|
||||||
|
disabled={true}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={-1}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||||
|
/>
|
||||||
|
<title>
|
||||||
|
Edit strategy
|
||||||
|
</title>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-describedby={null}
|
aria-describedby={null}
|
||||||
className=""
|
className=""
|
||||||
|
90
frontend/src/component/strategies/hooks/useStrategyForm.ts
Normal file
90
frontend/src/component/strategies/hooks/useStrategyForm.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { ICustomStrategyParameter } from 'interfaces/strategy';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import useStrategies from 'hooks/api/getters/useStrategies/useStrategies';
|
||||||
|
|
||||||
|
export const useStrategyForm = (
|
||||||
|
initialStrategyName: string = '',
|
||||||
|
initialStrategyDesc: string = '',
|
||||||
|
initialParams: ICustomStrategyParameter[] = []
|
||||||
|
) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
@ -14,7 +14,7 @@ interface ITagTypeForm {
|
|||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: string;
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
validateNameUniqueness?: () => void;
|
validateNameUniqueness?: () => void;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
|
import { ICustomStrategyPayload } from 'interfaces/strategy';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
export interface ICustomStrategyPayload {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
parameters: object[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStrategiesApi = () => {
|
const useStrategiesApi = () => {
|
||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { IStrategy } from 'interfaces/strategy';
|
||||||
|
|
||||||
|
export const defaultStrategy: IStrategy = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
displayName: '',
|
||||||
|
editable: false,
|
||||||
|
deprecated: false,
|
||||||
|
parameters: [],
|
||||||
|
};
|
30
frontend/src/hooks/api/getters/useStrategy/useStrategy.ts
Normal file
30
frontend/src/hooks/api/getters/useStrategy/useStrategy.ts
Normal file
@ -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;
|
@ -39,3 +39,22 @@ export interface IStrategyPayload {
|
|||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
parameters: IParameter;
|
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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user