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

fix: decouple forms (#3162)

This PR decouples the forms for creating API tokens and project level
API tokens. The point of having a hook that provides the functionality
for the form is that we can create specific forms that take care of
implementing the logic needed for that form instead of having one form
serving multiple use cases.
This commit is contained in:
Fredrik Strand Oseberg 2023-02-21 12:46:29 +01:00 committed by GitHub
parent 4f475548ba
commit 045973a432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 210 additions and 123 deletions

View File

@ -26,8 +26,6 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli
import { Search } from 'component/common/Search/Search';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { Route, Routes } from 'react-router-dom';
import { ProjectApiTokenCreate } from './ProjectApiTokenCreate';
const hiddenColumnsSmall = ['Icon', 'createdAt'];
const hiddenColumnsCompact = ['Icon', 'project', 'seenAt'];
@ -67,11 +65,11 @@ export const ApiTokenTable = ({
Cell: ({
value,
}: {
value: keyof typeof tokenDescriptions;
value: 'client' | 'admin' | 'frontend';
}) => (
<HighlightCell
value={tokenDescriptions[value].label}
subtitle={tokenDescriptions[value].title}
value={tokenDescriptions[value.toLowerCase()].label}
subtitle={tokenDescriptions[value.toLowerCase()].title}
/>
),
minWidth: 280,
@ -246,44 +244,21 @@ export const ApiTokenTable = ({
/>
}
/>
<ConditionallyRender
condition={Boolean(filterForProject)}
show={
<Routes>
<Route
path="create"
element={<ProjectApiTokenCreate />}
/>
</Routes>
}
/>
</PageContent>
);
};
//TODO fix me - remove duplicate keys
const tokenDescriptions = {
client: {
label: 'CLIENT',
title: 'Connect server-side SDK or Unleash Proxy',
},
frontend: {
label: 'FRONTEND',
title: 'Connect web and mobile SDK',
},
admin: {
label: 'ADMIN',
title: 'Full access for managing Unleash',
},
CLiENT: {
label: 'CLIENT',
title: 'Connect server-side SDK or Unleash Proxy',
},
FRONTEND: {
label: 'FRONTEND',
title: 'Connect web and mobile SDK',
},
ADMIN: {
label: 'ADMIN',
title: 'Full access for managing Unleash',
},
};
const tokenDescriptions: { [index: string]: { label: string; title: string } } =
{
client: {
label: 'CLIENT',
title: 'Connect server-side SDK or Unleash Proxy',
},
frontend: {
label: 'FRONTEND',
title: 'Connect web and mobile SDK',
},
admin: {
label: 'ADMIN',
title: 'Full access for managing Unleash',
},
};

View File

@ -1,29 +0,0 @@
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useProjectAccess from 'hooks/api/getters/useProjectAccess/useProjectAccess';
import { useAccess } from 'hooks/api/getters/useAccess/useAccess';
import { GO_BACK } from 'constants/navigate';
import { CreateApiToken } from '../CreateApiToken/CreateApiToken';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { useNavigate } from 'react-router-dom';
export const ProjectApiTokenCreate = () => {
const projectId = useRequiredPathParam('projectId');
const navigate = useNavigate();
const { access } = useProjectAccess(projectId);
const { users, serviceAccounts, groups } = useAccess();
if (!access || !users || !serviceAccounts || !groups) {
return null;
}
return (
<SidebarModal
open
onClose={() => navigate(GO_BACK)}
label={`Create API token`}
>
<CreateApiToken modal={true} project={projectId} />
</SidebarModal>
);
};

View File

@ -7,32 +7,23 @@ import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { useApiTokenForm } from 'component/admin/apiToken/ApiTokenForm/useApiTokenForm';
import {
CREATE_API_TOKEN,
CREATE_PROJECT_API_TOKEN,
} from 'component/providers/AccessProvider/permissions';
import { CREATE_API_TOKEN } from 'component/providers/AccessProvider/permissions';
import { ConfirmToken } from '../ConfirmToken/ConfirmToken';
import { scrollToTop } from 'component/common/util';
import { formatUnknownError } from 'utils/formatUnknownError';
import { usePageTitle } from 'hooks/usePageTitle';
import { GO_BACK } from 'constants/navigate';
import { useApiTokens } from 'hooks/api/getters/useApiTokens/useApiTokens';
import useProjectApiTokensApi from 'hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi';
import { TokenInfo } from '../ApiTokenForm/TokenInfo/TokenInfo';
import { TokenTypeSelector } from '../ApiTokenForm/TokenTypeSelector/TokenTypeSelector';
import { ProjectSelector } from '../ApiTokenForm/ProjectSelector/ProjectSelector';
import { EnvironmentSelector } from '../ApiTokenForm/EnvironmentSelector/EnvironmentSelector';
const pageTitle = 'Create API token';
interface ICreateApiTokenProps {
modal?: boolean;
project?: string;
}
export const CreateApiToken = ({
modal = false,
project,
}: ICreateApiTokenProps) => {
export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const navigate = useNavigate();
@ -52,22 +43,16 @@ export const CreateApiToken = ({
isValid,
errors,
clearErrors,
} = useApiTokenForm(project);
} = useApiTokenForm();
const { createToken, loading: globalLoading } = useApiTokensApi();
const { createToken: createProjectToken, loading: projectLoading } =
useProjectApiTokensApi();
const { createToken, loading } = useApiTokensApi();
const { refetch } = useApiTokens();
usePageTitle(pageTitle);
const PATH = Boolean(project)
? `api/admin/project/${project}/api-tokens`
: 'api/admin/api-tokens';
const permission = Boolean(project)
? CREATE_PROJECT_API_TOKEN
: CREATE_API_TOKEN;
const loading = globalLoading || projectLoading;
const PATH = `api/admin/api-tokens`;
const permission = CREATE_API_TOKEN;
const handleSubmit = async (e: Event) => {
e.preventDefault();
@ -76,23 +61,15 @@ export const CreateApiToken = ({
}
try {
const payload = getApiTokenPayload();
if (project) {
await createProjectToken(payload, project)
.then(res => res.json())
.then(api => {
scrollToTop();
setToken(api.secret);
setShowConfirm(true);
});
} else {
await createToken(payload)
.then(res => res.json())
.then(api => {
scrollToTop();
setToken(api.secret);
setShowConfirm(true);
});
}
await createToken(payload)
.then(res => res.json())
.then(api => {
scrollToTop();
setToken(api.secret);
setShowConfirm(true);
refetch();
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
@ -100,7 +77,6 @@ export const CreateApiToken = ({
const closeConfirm = () => {
setShowConfirm(false);
refetch();
navigate(GO_BACK);
};
@ -131,13 +107,7 @@ export const CreateApiToken = ({
handleSubmit={handleSubmit}
handleCancel={handleCancel}
mode="Create"
actions={
<CreateButton
name="token"
permission={permission}
projectId={project}
/>
}
actions={<CreateButton name="token" permission={permission} />}
>
<TokenInfo
username={username}

View File

@ -17,6 +17,7 @@ export const CreateApiTokenButton = () => {
const permission = Boolean(project)
? CREATE_PROJECT_API_TOKEN
: CREATE_API_TOKEN;
return (
<ResponsiveButton
Icon={Add}

View File

@ -0,0 +1,18 @@
import { GO_BACK } from 'constants/navigate';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { useNavigate } from 'react-router-dom';
import { CreateProjectApiTokenForm } from './CreateProjectApiTokenForm';
export const CreateProjectApiToken = () => {
const navigate = useNavigate();
return (
<SidebarModal
open
onClose={() => navigate(GO_BACK)}
label={`Create API token`}
>
<CreateProjectApiTokenForm />
</SidebarModal>
);
};

View File

@ -0,0 +1,144 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { useApiTokenForm } from 'component/admin/apiToken/ApiTokenForm/useApiTokenForm';
import { CREATE_PROJECT_API_TOKEN } from 'component/providers/AccessProvider/permissions';
import { scrollToTop } from 'component/common/util';
import { formatUnknownError } from 'utils/formatUnknownError';
import { usePageTitle } from 'hooks/usePageTitle';
import { GO_BACK } from 'constants/navigate';
import useProjectApiTokensApi from 'hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import ApiTokenForm from 'component/admin/apiToken/ApiTokenForm/ApiTokenForm';
import { EnvironmentSelector } from 'component/admin/apiToken/ApiTokenForm/EnvironmentSelector/EnvironmentSelector';
import { TokenInfo } from 'component/admin/apiToken/ApiTokenForm/TokenInfo/TokenInfo';
import { TokenTypeSelector } from 'component/admin/apiToken/ApiTokenForm/TokenTypeSelector/TokenTypeSelector';
import { ConfirmToken } from 'component/admin/apiToken/ConfirmToken/ConfirmToken';
import { useProjectApiTokens } from 'hooks/api/getters/useProjectApiTokens/useProjectApiTokens';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const pageTitle = 'Create project API token';
export const CreateProjectApiTokenForm = () => {
const project = useRequiredPathParam('projectId');
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const navigate = useNavigate();
const [showConfirm, setShowConfirm] = useState(false);
const [token, setToken] = useState('');
const {
getApiTokenPayload,
username,
type,
environment,
setUsername,
setTokenType,
setEnvironment,
isValid,
errors,
clearErrors,
} = useApiTokenForm(project);
const { createToken: createProjectToken, loading } =
useProjectApiTokensApi();
const { refetch: refetchProjectTokens } = useProjectApiTokens(project);
const { trackEvent } = usePlausibleTracker();
usePageTitle(pageTitle);
const PATH = `api/admin/project/${project}/api-tokens`;
const permission = CREATE_PROJECT_API_TOKEN;
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!isValid()) {
return;
}
try {
const payload = getApiTokenPayload();
await createProjectToken(payload, project)
.then(res => res.json())
.then(api => {
scrollToTop();
setToken(api.secret);
setShowConfirm(true);
trackEvent('project_api_tokens', {
props: { eventType: 'api_key_created' },
});
refetchProjectTokens();
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const closeConfirm = () => {
setShowConfirm(false);
navigate(GO_BACK);
};
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/${PATH}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getApiTokenPayload(), undefined, 2)}'`;
};
const handleCancel = () => {
navigate(GO_BACK);
};
return (
<FormTemplate
loading={loading}
title={pageTitle}
modal
description="Unleash SDKs use API tokens to authenticate to the Unleash API. Client SDKs need a token with 'client privileges', which allows them to fetch feature toggle configurations and post usage metrics."
documentationLink="https://docs.getunleash.io/reference/api-tokens-and-client-keys"
documentationLinkLabel="API tokens documentation"
formatApiCode={formatApiCode}
>
<ApiTokenForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
mode="Create"
actions={
<CreateButton
name="token"
permission={permission}
projectId={project}
/>
}
>
<TokenInfo
username={username}
setUsername={setUsername}
errors={errors}
clearErrors={clearErrors}
/>
<TokenTypeSelector type={type} setType={setTokenType} />
<EnvironmentSelector
type={type}
environment={environment}
setEnvironment={setEnvironment}
/>
</ApiTokenForm>
<ConfirmToken
open={showConfirm}
closeConfirm={closeConfirm}
token={token}
type={type}
/>
</FormTemplate>
);
};

View File

@ -7,8 +7,10 @@ import { READ_PROJECT_API_TOKEN } from 'component/providers/AccessProvider/permi
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import { ApiTokenTable } from '../../../admin/apiToken/ApiTokenTable/ApiTokenTable';
import { useProjectApiTokens } from '../../../../hooks/api/getters/useProjectApiTokens/useProjectApiTokens';
import { CreateProjectApiToken } from 'component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiToken';
import { Routes, Route } from 'react-router-dom';
import { ApiTokenTable } from 'component/admin/apiToken/ApiTokenTable/ApiTokenTable';
import { useProjectApiTokens } from 'hooks/api/getters/useProjectApiTokens/useProjectApiTokens';
export const ProjectApiAccess = () => {
const projectId = useRequiredPathParam('projectId');
@ -37,6 +39,10 @@ export const ProjectApiAccess = () => {
compact
filterForProject={projectId}
/>
<Routes>
<Route path="create" element={<CreateProjectApiToken />} />
</Routes>
</div>
);
};

View File

@ -9,7 +9,7 @@ import { ITab, VerticalTabs } from 'component/common/VerticalTabs/VerticalTabs';
import { ProjectAccess } from 'component/project/ProjectAccess/ProjectAccess';
import ProjectEnvironmentList from 'component/project/ProjectEnvironment/ProjectEnvironment';
import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeRequestConfiguration';
import { ProjectApiAccess } from './ProjectApiAccess';
import { ProjectApiAccess } from './ProjectApiAccess/ProjectApiAccess';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
export const ProjectSettings = () => {

View File

@ -19,7 +19,8 @@ type CustomEvents =
| 'project_overview'
| 'suggest_tags'
| 'unknown_ui_error'
| 'export_import';
| 'export_import'
| 'project_api_tokens';
export const usePlausibleTracker = () => {
const plausible = useContext(PlausibleContext);

View File

@ -42,6 +42,7 @@ process.nextTick(async () => {
featuresExportImport: true,
newProjectOverview: true,
projectStatusApi: true,
showProjectApiAccess: true,
},
},
authentication: {