mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-07 01:16:28 +02:00
fix: new create toggle page
This commit is contained in:
parent
58ff86e3bd
commit
edd6706ffe
@ -0,0 +1,17 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
bodyContainer: {
|
||||||
|
borderRadius: '12.5px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: '2rem',
|
||||||
|
},
|
||||||
|
formContainer: {
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
maxWidth: '350px',
|
||||||
|
},
|
||||||
|
nameInput: {
|
||||||
|
marginRight: `1.5rem`,
|
||||||
|
minWidth: `250px`
|
||||||
|
}
|
||||||
|
}));
|
165
frontend/src/component/feature/FeatureCreate/FeatureCreate.tsx
Normal file
165
frontend/src/component/feature/FeatureCreate/FeatureCreate.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useHistory, useParams } from 'react-router';
|
||||||
|
import { useStyles } from './FeatureCreate.styles';
|
||||||
|
import { IFeatureViewParams } from '../../../interfaces/params';
|
||||||
|
import PageContent from '../../common/PageContent';
|
||||||
|
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
|
import { CardActions, TextField } from '@material-ui/core';
|
||||||
|
import FeatureTypeSelect from '../FeatureView2/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
||||||
|
import {
|
||||||
|
CF_CREATE_BTN_ID,
|
||||||
|
CF_DESC_ID,
|
||||||
|
CF_NAME_ID,
|
||||||
|
CF_TYPE_ID,
|
||||||
|
} from '../../../testIds';
|
||||||
|
import { loadNameFromUrl, trim } from '../../common/util';
|
||||||
|
import { getTogglePath } from '../../../utils/route-path-helpers';
|
||||||
|
import { IFeatureToggleDTO } from '../../../interfaces/featureToggle';
|
||||||
|
import { FormEventHandler } from 'react-router/node_modules/@types/react';
|
||||||
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import { FormButtons } from '../../common';
|
||||||
|
|
||||||
|
interface Errors {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureCreate = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
const { projectId } = useParams<IFeatureViewParams>();
|
||||||
|
const { createFeatureToggle, validateFeatureToggleName } = useFeatureApi();
|
||||||
|
const history = useHistory();
|
||||||
|
const [ toggle, setToggle ] = useState<IFeatureToggleDTO>({
|
||||||
|
name: loadNameFromUrl(),
|
||||||
|
description: '',
|
||||||
|
type: 'release',
|
||||||
|
stale: false,
|
||||||
|
variants: [],
|
||||||
|
project: projectId,
|
||||||
|
archived: false,
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.onbeforeunload = () =>
|
||||||
|
'Data will be lost if you leave the page, are you sure?';
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
//@ts-ignore
|
||||||
|
window.onbeforeunload = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onCancel = () => history.push(
|
||||||
|
`/projects/${projectId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const validateName = async (featureToggleName: string) => {
|
||||||
|
const e = { ...errors };
|
||||||
|
try {
|
||||||
|
await validateFeatureToggleName(featureToggleName);
|
||||||
|
e.name = undefined;
|
||||||
|
} catch (err: any) {
|
||||||
|
e.name = err && err.message ? err.message : 'Could not check name';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (evt: FormEventHandler) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
const errorList = Object.values(errors).filter(i => i);
|
||||||
|
|
||||||
|
if (errorList.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createFeatureToggle(projectId, toggle).then(() =>
|
||||||
|
history.push(
|
||||||
|
getTogglePath(toggle.project, toggle.name, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// Trigger
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.toString().includes('not allowed to be empty')) {
|
||||||
|
setErrors({ name: 'Name is not allowed to be empty' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (field:string, value:string) => {
|
||||||
|
setToggle({...toggle, [field]: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent headerContent="Create feature toggle" bodyClass={styles.bodyContainer}>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<input type="hidden" name="project" value={projectId} />
|
||||||
|
<div className={styles.formContainer}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
label="Name"
|
||||||
|
required
|
||||||
|
placeholder="Unique-name"
|
||||||
|
className={styles.nameInput}
|
||||||
|
name="name"
|
||||||
|
inputProps={{
|
||||||
|
'data-test': CF_NAME_ID,
|
||||||
|
}}
|
||||||
|
value={toggle.name}
|
||||||
|
error={errors.name !== undefined}
|
||||||
|
helperText={errors.name}
|
||||||
|
onBlur={v => validateName(v.target.value)}
|
||||||
|
onChange={v => setValue('name', trim(v.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formContainer}>
|
||||||
|
<FeatureTypeSelect
|
||||||
|
value={toggle.type}
|
||||||
|
onChange={v => setValue('type', v.target.value)}
|
||||||
|
label={'Toggle type'}
|
||||||
|
id="feature-type-select"
|
||||||
|
editable
|
||||||
|
inputProps={{
|
||||||
|
'data-test': CF_TYPE_ID,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<section className={styles.formContainer}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
className={commonStyles.fullWidth}
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
label="Description"
|
||||||
|
placeholder="A short description of the feature toggle"
|
||||||
|
error={errors.description !== undefined}
|
||||||
|
helperText={errors.description}
|
||||||
|
value={toggle.description}
|
||||||
|
inputProps={{
|
||||||
|
'data-test': CF_DESC_ID,
|
||||||
|
}}
|
||||||
|
onChange={v => setValue('description', v.target.value)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<CardActions>
|
||||||
|
<FormButtons
|
||||||
|
submitText={'Create'}
|
||||||
|
primaryButtonTestId={CF_CREATE_BTN_ID}
|
||||||
|
onCancel={onCancel}
|
||||||
|
/>
|
||||||
|
</CardActions>
|
||||||
|
</form>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureCreate;
|
@ -57,6 +57,15 @@ Array [
|
|||||||
"title": ":name",
|
"title": ":name",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"layout": "main",
|
||||||
|
"menu": Object {},
|
||||||
|
"parent": "/projects/:id/features",
|
||||||
|
"path": "/projects/:projectId/create-toggle2",
|
||||||
|
"title": "Create feature toggle",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
|
@ -40,6 +40,7 @@ import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
|
|||||||
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
|
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
|
||||||
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
||||||
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
|
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
|
||||||
|
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate'
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Project
|
// Project
|
||||||
@ -98,6 +99,15 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/projects/:projectId/create-toggle2',
|
||||||
|
parent: '/projects/:id/features',
|
||||||
|
title: 'Create feature toggle',
|
||||||
|
component: FeatureCreate,
|
||||||
|
type: 'protected',
|
||||||
|
layout: 'main',
|
||||||
|
menu: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/projects/:id/create-toggle',
|
path: '/projects/:id/create-toggle',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
|
@ -15,6 +15,7 @@ import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton'
|
|||||||
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||||
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||||
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
interface IProjectFeatureToggles {
|
interface IProjectFeatureToggles {
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
@ -29,6 +30,7 @@ const ProjectFeatureToggles = ({
|
|||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
@ -59,7 +61,7 @@ const ProjectFeatureToggles = ({
|
|||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
history.push(
|
history.push(
|
||||||
getCreateTogglePath(id)
|
getCreateTogglePath(id, uiConfig.flags.E)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
maxWidth="700px"
|
maxWidth="700px"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { IFeatureToggleDTO } from '../../../../interfaces/featureToggle';
|
||||||
import { ITag } from '../../../../interfaces/tags';
|
import { ITag } from '../../../../interfaces/tags';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
@ -6,6 +7,44 @@ const useFeatureApi = () => {
|
|||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const validateFeatureToggleName = async (
|
||||||
|
name: string,
|
||||||
|
) => {
|
||||||
|
const path = `api/admin/features/validate`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const createFeatureToggle = async (
|
||||||
|
projectId: string,
|
||||||
|
featureToggle: IFeatureToggleDTO,
|
||||||
|
) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/features`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(featureToggle),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleFeatureEnvironmentOn = async (
|
const toggleFeatureEnvironmentOn = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureId: string,
|
featureId: string,
|
||||||
@ -164,6 +203,8 @@ const useFeatureApi = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
validateFeatureToggleName,
|
||||||
|
createFeatureToggle,
|
||||||
changeFeatureProject,
|
changeFeatureProject,
|
||||||
errors,
|
errors,
|
||||||
toggleFeatureEnvironmentOn,
|
toggleFeatureEnvironmentOn,
|
||||||
|
@ -11,6 +11,16 @@ export interface IEnvironments {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFeatureToggleDTO {
|
||||||
|
stale: boolean;
|
||||||
|
archived: boolean;
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
project: string;
|
||||||
|
type: string;
|
||||||
|
variants: IFeatureVariant[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IFeatureToggle {
|
export interface IFeatureToggle {
|
||||||
stale: boolean;
|
stale: boolean;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
|
@ -9,8 +9,8 @@ export const getToggleCopyPath = (
|
|||||||
return `/projects/${projectId}/features/${featureToggleName}/strategies/copy`;
|
return `/projects/${projectId}/features/${featureToggleName}/strategies/copy`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCreateTogglePath = (projectId: string) => {
|
export const getCreateTogglePath = (projectId: string, newpath: boolean = false) => {
|
||||||
return `/projects/${projectId}/create-toggle?project=${projectId}`;
|
return newpath ? `/projects/${projectId}/create-toggle2` : `/projects/${projectId}/create-toggle?project=${projectId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectEditPath = (projectId: string) => {
|
export const getProjectEditPath = (projectId: string) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user