1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

feat: create edit and creat feature screen

This commit is contained in:
Youssef 2022-01-18 12:16:08 +01:00 committed by Fredrik Oseberg
parent 53cff04349
commit 8c82a6bceb
7 changed files with 532 additions and 3 deletions

View File

@ -24,7 +24,9 @@ const BreadcrumbNav = () => {
item !== 'strategies' &&
item !== 'features' &&
item !== 'features2' &&
item !== 'create-toggle'
item !== 'create-toggle'&&
item !== 'settings'
);
return (

View File

@ -0,0 +1,103 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
import { useHistory } from 'react-router-dom';
import FeatureForm from '../FeatureForm/FeatureForm';
import useFeatureForm from '../hooks/useFeatureForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast';
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
const CreateFeature = () => {
/* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();
const {
type,
setType,
name,
setName,
project,
setProject,
description,
setDescription,
getTogglePayload,
validateName,
clearErrors,
errors,
} = useFeatureForm();
const { createFeatureToggle, loading } = useFeatureApi();
const handleSubmit = async (e: Event) => {
e.preventDefault();
clearErrors();
await validateName(name);
const payload = getTogglePayload();
try {
await createFeatureToggle(project, payload);
history.push(`/projects/${project}/features2/${name}`);
setToastData({
title: 'Toggle created successfully',
text: 'Now you can start using your toggle.',
confetti: true,
type: 'success',
});
} catch (e: any) {
setToastApiError(e.toString());
}
};
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/api/admin/projects/${project}/features' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getTogglePayload(), undefined, 2)}'`;
};
const handleCancel = () => {
history.goBack();
};
return (
<FormTemplate
loading={loading}
title="Create Feature toggle"
description="Feature toggles support different use cases, each with their own specific needs such as simple static routing or more complex routing.
The feature toggle is disabled when created and you decide when to enable"
documentationLink="https://docs.getunleash.io/"
formatApiCode={formatApiCode}
>
<FeatureForm
type={type}
name={name}
project={project}
description={description}
setType={setType}
setName={setName}
setProject={setProject}
setDescription={setDescription}
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
mode="Create"
clearErrors={clearErrors}
>
<PermissionButton
onClick={handleSubmit}
permission={CREATE_FEATURE}
projectId={project}
type="submit"
>
Create toggle
</PermissionButton>
</FeatureForm>
</FormTemplate>
);
};
export default CreateFeature;

View File

@ -0,0 +1,115 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
import { useHistory, useParams } from 'react-router-dom';
import FeatureForm from '../FeatureForm/FeatureForm';
import useFeatureForm from '../hooks/useFeatureForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast';
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
import { IFeatureViewParams } from '../../../../interfaces/params';
import * as jsonpatch from 'fast-json-patch';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions';
const EditFeature = () => {
/* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();
const { projectId, featureId } = useParams<IFeatureViewParams>();
const { patchFeatureToggle, loading } = useFeatureApi();
const { feature } = useFeature(projectId, featureId);
const {
type,
setType,
name,
setName,
project,
setProject,
description,
setDescription,
getTogglePayload,
clearErrors,
errors,
} = useFeatureForm(
feature?.name,
feature?.type,
feature?.project,
feature?.description
);
const createPatch = () => {
const comparison = { ...feature, type, description };
const patch = jsonpatch.compare(feature, comparison);
return patch;
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
clearErrors();
const patch = createPatch();
try {
await patchFeatureToggle(project, featureId, patch);
history.push(`/projects/${project}/features2/${name}`);
setToastData({
title: 'Toggle updated successfully',
text: 'Now you can start using your toggle.',
type: 'success',
});
} catch (e: any) {
setToastApiError(e.toString());
}
};
const formatApiCode = () => {
return `curl --location --request PUT '${
uiConfig.unleashUrl
}/api/admin/projects/${projectId}/features/${featureId}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getTogglePayload(), undefined, 2)}'`;
};
const handleCancel = () => {
history.goBack();
};
return (
<FormTemplate
loading={loading}
title="Create Feature toggle"
description="Feature toggles support different use cases, each with their own specific needs such as simple static routing or more complex routing.
The feature toggle is disabled when created and you decide when to enable"
documentationLink="https://docs.getunleash.io/"
formatApiCode={formatApiCode}
>
<FeatureForm
type={type}
name={name}
project={project}
description={description}
setType={setType}
setName={setName}
setProject={setProject}
setDescription={setDescription}
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
mode="Edit"
clearErrors={clearErrors}
>
<PermissionButton
onClick={handleSubmit}
permission={UPDATE_FEATURE}
projectId={project}
type="submit"
>
Edit toggle
</PermissionButton>
</FeatureForm>
</FormTemplate>
);
};
export default EditFeature;

View File

@ -0,0 +1,61 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
maxWidth: '400px',
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
input: { width: '100%', marginBottom: '1rem' },
selectInput: {
marginBottom: '1rem',
minWidth: '400px',
[theme.breakpoints.down(600)]: {
minWidth: '379px',
},
},
label: {
minWidth: '300px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
buttonContainer: {
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
},
cancelButton: {
marginRight: '1.5rem',
},
inputDescription: {
marginBottom: '0.5rem',
},
typeDescription: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.grey[600],
top: '-13px',
position: 'relative',
},
formHeader: {
fontWeight: 'normal',
marginTop: '0',
},
header: {
fontWeight: 'normal',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
}));

View File

@ -0,0 +1,145 @@
import {
CREATE_FEATURE,
UPDATE_FEATURE,
} from '../../../providers/AccessProvider/permissions';
import Input from '../../../common/Input/Input';
import { Button } from '@material-ui/core';
import { useStyles } from './FeatureForm.styles';
import FeatureTypeSelect from '../../FeatureView2/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
import { CF_DESC_ID, CF_TYPE_ID } from '../../../../testIds';
import useFeatureTypes from '../../../../hooks/api/getters/useFeatureTypes/useFeatureTypes';
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
import { useContext } from 'react';
import useUser from '../../../../hooks/api/getters/useUser/useUser';
import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
import FeatureProjectSelect from '../../FeatureView2/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
import AccessContext from '../../../../contexts/AccessContext';
import ConditionallyRender from '../../../common/ConditionallyRender';
import { trim } from '../../../common/util';
interface IFeatureToggleForm {
type: string;
name: string;
description: string;
project: string;
setType: React.Dispatch<React.SetStateAction<string>>;
setName: React.Dispatch<React.SetStateAction<string>>;
setDescription: React.Dispatch<React.SetStateAction<string>>;
setProject: React.Dispatch<React.SetStateAction<string>>;
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
mode: string;
clearErrors: () => void;
}
const FeatureForm: React.FC<IFeatureToggleForm> = ({
children,
type,
name,
description,
project,
setType,
setName,
setDescription,
setProject,
handleSubmit,
handleCancel,
errors,
mode,
clearErrors,
}) => {
const styles = useStyles();
const { hasAccess } = useContext(AccessContext);
const { featureTypes } = useFeatureTypes();
const { permissions } = useUser();
const editable = hasAccess(UPDATE_FEATURE, project);
const renderToggleDescription = () => {
return featureTypes.find(toggle => toggle.id === type)?.description;
};
return (
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.container}>
<p className={styles.inputDescription}>
What kind of feature toggle do you want to create?
</p>
<FeatureTypeSelect
value={type}
onChange={e => setType(e.target.value)}
label={'Toggle type'}
id="feature-type-select"
editable
inputProps={{
'data-test': CF_TYPE_ID,
}}
IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
/>
<p className={styles.typeDescription}>
{renderToggleDescription()}
</p>
<p className={styles.inputDescription}>
What would you like to call your toggle?
</p>
<Input
disabled={mode === 'Edit'}
className={styles.input}
label="Name"
error={Boolean(errors.name)}
errorText={errors.name}
onFocus={() => clearErrors()}
value={name}
onChange={e => setName(trim(e.target.value))}
/>
<ConditionallyRender
condition={editable}
show={
<p className={styles.inputDescription}>
In which project do you want to save the toggle?
</p>
}
/>
<FeatureProjectSelect
value={project}
onChange={e => setProject(e.target.value)}
enabled={editable}
label="Project"
filter={projectFilterGenerator(
{ permissions },
CREATE_FEATURE
)}
IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
/>
<p className={styles.inputDescription}>
How would you describe your feature toggle?
</p>
<Input
className={styles.input}
multiline
rows={4}
label="Description"
placeholder="A short description of the feature toggle"
value={description}
inputProps={{
'data-test': CF_DESC_ID,
}}
onChange={e => setDescription(e.target.value)}
/>
</div>
<div className={styles.buttonContainer}>
<Button onClick={handleCancel} className={styles.cancelButton}>
Cancel
</Button>
{children}
</div>
</form>
);
};
export default FeatureForm;

View File

@ -0,0 +1,90 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import useFeatureApi from '../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import useQueryParams from '../../../../hooks/useQueryParams';
import { IFeatureViewParams } from '../../../../interfaces/params';
const useFeatureForm = (
initialName = '',
initialType = 'release',
initialProject = 'default',
initialDescription = ''
) => {
const { projectId } = useParams<IFeatureViewParams>();
const params = useQueryParams();
const { validateFeatureToggleName } = useFeatureApi();
const toggleQueryName = params.get('name');
const [type, setType] = useState(initialType);
const [name, setName] = useState(toggleQueryName || initialName);
const [project, setProject] = useState(projectId || initialProject);
const [description, setDescription] = useState(initialDescription);
const [errors, setErrors] = useState({});
useEffect(() => {
setType(initialType);
}, [initialType]);
useEffect(() => {
if (!toggleQueryName) setName(initialName);
else setName(toggleQueryName);
}, [initialName, toggleQueryName]);
useEffect(() => {
if (!projectId) setProject(initialProject);
else setProject(projectId);
}, [initialProject, projectId]);
useEffect(() => {
setDescription(initialDescription);
}, [initialDescription]);
const getTogglePayload = () => {
return {
type: type,
name: name,
projectId: project,
description: description,
};
};
const validateName = async (name: string) => {
if (name.length === 0) {
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
return false;
}
if (name.length > 0) {
try {
await validateFeatureToggleName(name);
} catch (err: any) {
setErrors(prev => ({
...prev,
name:
err && err.message
? err.message
: 'Could not check name',
}));
}
}
};
const clearErrors = () => {
setErrors({});
};
return {
type,
setType,
name,
setName,
project,
setProject,
description,
setDescription,
getTogglePayload,
validateName,
clearErrors,
errors,
};
};
export default useFeatureForm;

View File

@ -31,13 +31,13 @@ import RedirectFeatureViewPage from '../../page/features/redirect';
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate';
import ProjectRoles from '../admin/project-roles/ProjectRoles/ProjectRoles';
import CreateProjectRole from '../admin/project-roles/CreateProjectRole/CreateProjectRole';
import EditProjectRole from '../admin/project-roles/EditProjectRole/EditProjectRole';
import CreateUser from '../admin/users/CreateUser/CreateUser';
import EditUser from '../admin/users/EditUser/EditUser';
import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken';
<<<<<<< HEAD
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
import CreateContext from '../context/CreateContext/CreateContext';
@ -46,6 +46,10 @@ import EditTagType from '../tagTypes/EditTagType/EditTagType';
import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
import EditProject from '../project/Project/EditProject/EditProject';
import CreateProject from '../project/Project/CreateProject/CreateProject';
=======
import CreateFeature from '../feature/CreateFeature/CreateFeature/CreateFeature';
import EditFeature from '../feature/CreateFeature/EditFeature/EditFeature';
>>>>>>> 937e090b (feat: create edit and creat feature screen)
export const routes = [
// Project
@ -95,6 +99,15 @@ export const routes = [
layout: 'main',
menu: {},
},
{
path: '/projects/:projectId/features2/:featureId/settings',
parent: '/projects',
title: 'Edit Feature',
component: EditFeature,
type: 'protected',
layout: 'main',
menu: {},
},
{
path: '/projects/:projectId/features2/:featureId',
parent: '/projects',
@ -118,7 +131,7 @@ export const routes = [
path: '/projects/:projectId/create-toggle',
parent: '/projects/:id/features',
title: 'Create feature toggle',
component: FeatureCreate,
component: CreateFeature,
type: 'protected',
layout: 'main',
menu: {},