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:
parent
4f475548ba
commit
045973a432
@ -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',
|
||||
},
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -17,6 +17,7 @@ export const CreateApiTokenButton = () => {
|
||||
const permission = Boolean(project)
|
||||
? CREATE_PROJECT_API_TOKEN
|
||||
: CREATE_API_TOKEN;
|
||||
|
||||
return (
|
||||
<ResponsiveButton
|
||||
Icon={Add}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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 = () => {
|
||||
|
@ -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);
|
||||
|
@ -42,6 +42,7 @@ process.nextTick(async () => {
|
||||
featuresExportImport: true,
|
||||
newProjectOverview: true,
|
||||
projectStatusApi: true,
|
||||
showProjectApiAccess: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user