1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: add create api token screen (NEW PR) (#600)

* feat: add create api token screen

* fix: update headers

* fix: remove old api create

Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Youssef Khedher 2022-01-17 11:56:53 +01:00 committed by GitHub
parent 3a41de2246
commit b209368c84
11 changed files with 404 additions and 272 deletions

View File

@ -1,224 +0,0 @@
import { TextField } from '@material-ui/core';
import classNames from 'classnames';
import React, { useState, useEffect } from 'react';
import { styles as commonStyles } from '../../../common';
import { IApiTokenCreate } from '../../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useEnvironments from '../../../../hooks/api/getters/useEnvironments/useEnvironments';
import useProjects from '../../../../hooks/api/getters/useProjects/useProjects';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import ConditionallyRender from '../../../common/ConditionallyRender';
import Dialogue from '../../../common/Dialogue';
import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect';
import { useStyles } from './styles';
const ALL = '*';
const TYPE_ADMIN = 'ADMIN';
const TYPE_CLIENT = 'CLIENT';
interface IApiTokenCreateProps {
showDialog: boolean;
closeDialog: () => void;
createToken: (token: IApiTokenCreate) => Promise<void>;
}
interface IDataError {
username?: string;
general?: string;
}
const INITIAL_DATA: IApiTokenCreate = {
username: '',
type: TYPE_CLIENT,
project: ALL,
};
const ApiTokenCreate = ({
showDialog,
closeDialog,
createToken,
}: IApiTokenCreateProps) => {
const styles = useStyles();
const [data, setData] = useState(INITIAL_DATA);
const [error, setError] = useState<IDataError>({});
const { projects } = useProjects();
const { environments } = useEnvironments();
const { uiConfig } = useUiConfig();
useEffect(() => {
if (
environments &&
environments.length > 0 &&
data.type === TYPE_CLIENT &&
!data.environment
) {
setData({ ...data, environment: environments[0].name });
}
}, [data, environments]);
const clear = () => {
const environment =
environments && environments.length > 0
? environments[0].name
: undefined;
setData({ ...INITIAL_DATA, environment });
setError({});
};
const onCancel = (e: Event) => {
clear();
closeDialog();
};
const isValid = () => {
if (!data.username) {
setError({ username: 'Username is required.' });
return false;
} else {
setError({});
return true;
}
};
const submit = async () => {
if (!isValid()) {
return;
}
try {
await createToken(data);
clear();
closeDialog();
} catch (error) {
setError({ general: 'Unable to create new API token' });
}
};
const setType = (event: React.ChangeEvent<{ value: string }>) => {
const value = event.target.value;
if (value === TYPE_ADMIN) {
setData({ ...data, type: value, environment: ALL, project: ALL });
} else {
setData({
...data,
type: value,
environment: environments[0].name,
});
}
};
const setUsername = (event: React.ChangeEvent<{ value: string }>) => {
const value = event.target.value;
setData({ ...data, username: value });
};
const setProject = (event: React.ChangeEvent<{ value: string }>) => {
const value = event.target.value;
setData({ ...data, project: value });
};
const setEnvironment = (event: React.ChangeEvent<{ value: string }>) => {
const value = event.target.value;
setData({ ...data, environment: value });
};
const selectableProjects = [{ id: '*', name: 'ALL' }, ...projects].map(
i => ({
key: i.id,
label: i.name,
title: i.name,
})
);
const selectableEnvs =
data.type === TYPE_ADMIN
? [{ key: '*', label: 'ALL' }]
: environments.map(i => ({
key: i.name,
label: i.name,
title: i.name,
}));
const selectableTypes = [
{ key: 'CLIENT', label: 'Client', title: 'Client SDK token' },
{ key: 'ADMIN', label: 'Admin', title: 'Admin API token' },
];
const formId = 'create-api-token-form';
return (
<Dialogue
onClick={() => submit()}
open={showDialog}
onClose={onCancel}
primaryButtonText="Create"
secondaryButtonText="Cancel"
title="New API token"
formId={formId}
>
<form
id={formId}
onSubmit={submit}
className={classNames(
styles.addApiKeyForm,
commonStyles.contentSpacing
)}
>
<TextField
autoFocus
value={data.username}
name="username"
onChange={setUsername}
onBlur={isValid}
label="Username"
style={{ width: '200px' }}
error={error.username !== undefined}
helperText={error.username}
variant="outlined"
size="small"
required
/>
<GeneralSelect
disabled={false}
options={selectableTypes}
value={data.type}
onChange={setType}
label="Token Type"
id="api_key_type"
name="type"
className={undefined}
classes={undefined}
/>
<GeneralSelect
disabled={data.type === TYPE_ADMIN}
options={selectableProjects}
value={data.project}
onChange={setProject}
label="Project"
id="api_key_project"
name="project"
className={undefined}
classes={undefined}
/>
<ConditionallyRender condition={uiConfig?.flags.E} show={
<>
<GeneralSelect
disabled={data.type === TYPE_ADMIN}
options={selectableEnvs}
value={data.environment}
required
onChange={setEnvironment}
label="Environment"
id="api_key_environment"
name="environment"
className={undefined}
classes={undefined}
/>
</>
} />
</form>
</Dialogue>
);
};
export default ApiTokenCreate;

View File

@ -1,8 +0,0 @@
import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles({
addApiKeyForm: {
display: 'flex',
flexDirection: 'column',
},
});

View File

@ -0,0 +1,53 @@
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',
},
formHeader: {
fontWeight: 'normal',
marginTop: '0',
},
header: {
fontWeight: 'normal',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
}));

View File

@ -0,0 +1,142 @@
import { Button } from '@material-ui/core';
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
import React from 'react';
import useEnvironments from '../../../../hooks/api/getters/useEnvironments/useEnvironments';
import useProjects from '../../../../hooks/api/getters/useProjects/useProjects';
import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect';
import Input from '../../../common/Input/Input';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import { useStyles } from './ApiTokenForm.styles';
interface IApiTokenFormProps {
username: string;
type: string;
project: string;
environment: string;
setTokenType: (value: string) => void;
setUsername: React.Dispatch<React.SetStateAction<string>>;
setProject: React.Dispatch<React.SetStateAction<string>>;
setEnvironment: React.Dispatch<React.SetStateAction<string>>;
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
submitButtonText: string;
clearErrors: () => void;
}
const ApiTokenForm = ({
username,
type,
project,
environment,
setUsername,
setTokenType,
setProject,
setEnvironment,
handleSubmit,
handleCancel,
errors,
clearErrors,
submitButtonText,
}: IApiTokenFormProps) => {
const TYPE_ADMIN = 'ADMIN';
const styles = useStyles();
const { environments } = useEnvironments();
const { projects } = useProjects();
const selectableTypes = [
{ key: 'CLIENT', label: 'Client', title: 'Client SDK token' },
{ key: 'ADMIN', label: 'Admin', title: 'Admin API token' },
];
const selectableProjects = [{ id: '*', name: 'ALL' }, ...projects].map(
i => ({
key: i.id,
label: i.name,
title: i.name,
})
);
const selectableEnvs =
type === TYPE_ADMIN
? [{ key: '*', label: 'ALL' }]
: environments.map(i => ({
key: i.name,
label: i.name,
title: i.name,
}));
return (
<form onSubmit={handleSubmit} className={styles.form}>
<h3 className={styles.formHeader}>Token information</h3>
<div className={styles.container}>
<p className={styles.inputDescription}>
Who are you generating the token for?
</p>
<Input
className={styles.input}
value={username}
name="username"
onChange={e => setUsername(e.target.value)}
label="Username"
error={errors.username !== undefined}
errorText={errors.username}
onFocus={() => clearErrors()}
/>
<p className={styles.inputDescription}>
What is your token type?
</p>
<GeneralSelect
options={selectableTypes}
value={type}
onChange={e => setTokenType(e.target.value as string)}
label="Token Type"
id="api_key_type"
name="type"
IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
/>
<p className={styles.inputDescription}>
Which project do you want to give access to?
</p>
<GeneralSelect
disabled={type === TYPE_ADMIN}
value={project}
options={selectableProjects}
onChange={e => setProject(e.target.value as string)}
label="Project"
IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
/>
<p className={styles.inputDescription}>
Which environment should the token have access to?
</p>
<GeneralSelect
disabled={type === TYPE_ADMIN}
options={selectableEnvs}
value={environment}
onChange={e => setEnvironment(e.target.value as string)}
label="Environment"
id="api_key_environment"
name="environment"
IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
/>
</div>
<div className={styles.buttonContainer}>
<Button onClick={handleCancel} className={styles.cancelButton}>
Cancel
</Button>
<PermissionButton
onClick={handleSubmit}
permission={ADMIN}
type="submit"
>
{submitButtonText} token
</PermissionButton>
</div>
</form>
);
};
export default ApiTokenForm;

View File

@ -1,5 +1,5 @@
import { useContext, useState } from 'react';
import { Link } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';
import {
Button,
IconButton,
@ -14,9 +14,7 @@ import useToast from '../../../../hooks/useToast';
import useLoading from '../../../../hooks/useLoading';
import useApiTokens from '../../../../hooks/api/getters/useApiTokens/useApiTokens';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useApiTokensApi, {
IApiTokenCreate,
} from '../../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useApiTokensApi from '../../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import ApiError from '../../../common/ApiError/ApiError';
import PageContent from '../../../common/PageContent';
import HeaderTitle from '../../../common/HeaderTitle';
@ -29,7 +27,6 @@ import { useStyles } from './ApiTokenList.styles';
import { formatDateWithLocale } from '../../../common/util';
import Secret from './secret';
import { Delete, FileCopy } from '@material-ui/icons';
import ApiTokenCreate from '../ApiTokenCreate/ApiTokenCreate';
import Dialogue from '../../../common/Dialogue';
import { CREATE_API_TOKEN_BUTTON } from '../../../../testIds';
import { Alert } from '@material-ui/lab';
@ -54,20 +51,11 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
const { uiConfig } = useUiConfig();
const [showDelete, setShowDelete] = useState(false);
const [delToken, setDeleteToken] = useState<IApiToken>();
const { setToastData, setToastApiError } = useToast();
const { setToastData } = useToast();
const { tokens, loading, refetch, error } = useApiTokens();
const { deleteToken, createToken } = useApiTokensApi();
const { deleteToken } = useApiTokensApi();
const ref = useLoading(loading);
const [showDialog, setDialog] = useState(false);
const openDialog = () => {
setDialog(true);
};
const closeDialog = () => {
setDialog(false);
};
const history = useHistory();
const renderError = () => {
return (
@ -79,26 +67,11 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
);
};
const onCreateToken = async (token: IApiTokenCreate) => {
try {
await createToken(token);
refetch();
setToastData({
type: 'success',
title: 'Created token',
text: 'Successfully created API token',
confetti: true,
});
} catch (e) {
setToastApiError(e.message);
}
};
const copyToken = (value: string) => {
if (copy(value)) {
setToastData({
type: 'success',
title: 'Token copied',
confetti: true,
text: `Token is copied to clipboard`,
});
}
@ -271,7 +244,11 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
<Button
variant="contained"
color="primary"
onClick={openDialog}
onClick={() =>
history.push(
'/admin/api/create-token'
)
}
data-test={CREATE_API_TOKEN_BUTTON}
>
Create API token
@ -312,11 +289,6 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
/>
</div>
<ApiTokenCreate
showDialog={showDialog}
createToken={onCreateToken}
closeDialog={closeDialog}
/>
<Dialogue
open={showDelete}
onClick={onDeleteToken}

View File

@ -0,0 +1,90 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
import { useHistory } from 'react-router-dom';
import ApiTokenForm from '../ApiTokenForm/ApiTokenForm';
import useApiTokenForm from '../hooks/useApiTokenForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast';
import useApiTokensApi from '../../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
const CreateApiToken = () => {
/* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();
const {
getApiTokenPayload,
username,
type,
project,
environment,
setUsername,
setTokenType,
setProject,
setEnvironment,
isValid,
errors,
clearErrors,
} = useApiTokenForm();
const { createToken, loading } = useApiTokensApi();
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!isValid()) {
return;
}
try {
await createToken(getApiTokenPayload());
history.push('/admin/api');
setToastData({
type: 'success',
title: 'Created token',
text: 'Successfully created API token',
confetti: true,
});
} catch (e: any) {
setToastApiError(e.toString());
}
};
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/api/admin/api-tokens' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getApiTokenPayload(), undefined, 2)}'`;
};
const handleCancel = () => {
history.goBack();
};
return (
<FormTemplate
loading={loading}
title="Create Api Token"
description="In order to connect to Unleash clients will need an API token to grant access. A client SDK will need to token with 'client privileges', which allows them to fetch feature toggle configuration and post usage metrics back."
documentationLink="https://docs.getunleash.io/user_guide/api-token"
formatApiCode={formatApiCode}
>
<ApiTokenForm
username={username}
type={type}
project={project}
environment={environment}
setEnvironment={setEnvironment}
setTokenType={setTokenType}
setUsername={setUsername}
setProject={setProject}
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
submitButtonText="Create"
clearErrors={clearErrors}
/>
</FormTemplate>
);
};
export default CreateApiToken;

View File

@ -0,0 +1,86 @@
import { useEffect, useState } from 'react';
const useApiToken = (
initialUserName = '',
initialtype = 'CLIENT',
initialProject = '*',
initialEnvironment = 'default'
) => {
const [username, setUsername] = useState(initialUserName);
const [type, setType] = useState(initialtype);
const [project, setProject] = useState(initialtype);
const [environment, setEnvironment] = useState(initialEnvironment);
const [errors, setErrors] = useState({});
useEffect(() => {
setUsername(initialUserName);
}, [initialUserName]);
useEffect(() => {
setType(initialtype);
if (type === 'ADMIN') {
setProject('*');
setEnvironment('*')
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialtype]);
useEffect(() => {
setProject(initialProject);
}, [initialProject]);
useEffect(() => {
setEnvironment(initialEnvironment);
}, [initialEnvironment]);
const setTokenType = (value: string) => {
if (value === 'ADMIN') {
setType(value)
setProject('*');
setEnvironment('*');
} else {
setType(value)
setEnvironment(initialEnvironment);
}
};
const getApiTokenPayload = () => {
return {
username: username,
type: type,
project: project,
environment: environment,
};
};
const isValid = () => {
if (!username) {
setErrors({ username: 'Username is required.' });
return false;
} else {
setErrors({});
return true;
}
};
const clearErrors = () => {
setErrors({});
};
return {
username,
type,
project,
environment,
setUsername,
setTokenType,
setProject,
setEnvironment,
getApiTokenPayload,
isValid,
clearErrors,
errors,
};
};
export default useApiToken;

View File

@ -14,6 +14,7 @@ const FeatureProjectSelect = ({
value,
onChange,
filter,
...rest
}: IFeatureProjectSelect) => {
const { projects } = useProjects();
@ -50,6 +51,7 @@ const FeatureProjectSelect = ({
options={options}
value={value}
onChange={onChange}
{...rest}
/>
);
};

View File

@ -330,6 +330,15 @@ Array [
"title": "Archived Toggles",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"menu": Object {},
"parent": "/admin",
"path": "/admin/api/create-token",
"title": "API access",
"type": "protected",
},
Object {
"component": [Function],
"flag": "RE",

View File

@ -43,6 +43,7 @@ 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 CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken';
export const routes = [
// Project
@ -378,6 +379,15 @@ export const routes = [
},
// Admin
{
path: '/admin/api/create-token',
parent: '/admin',
title: 'API access',
component: CreateApiToken,
type: 'protected',
layout: 'main',
menu: {},
},
{
path: '/admin/create-project-role',
title: 'Create',

View File

@ -8,7 +8,7 @@ export interface IApiTokenCreate {
}
const useApiTokensApi = () => {
const { makeRequest, createRequest, errors } = useAPI({
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
@ -38,7 +38,7 @@ const useApiTokensApi = () => {
}
};
return { deleteToken, createToken, errors };
return { deleteToken, createToken, errors, loading };
};
export default useApiTokensApi;