From 045973a43258321589e674473f64a80e9bf567c2 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 21 Feb 2023 12:46:29 +0100 Subject: [PATCH] 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. --- .../apiToken/ApiTokenTable/ApiTokenTable.tsx | 61 +++----- .../ApiTokenTable/ProjectApiTokenCreate.tsx | 29 ---- .../CreateApiToken/CreateApiToken.tsx | 64 +++----- .../CreateApiTokenButton.tsx | 1 + .../CreateProjectApiToken.tsx | 18 +++ .../CreateProjectApiTokenForm.tsx | 144 ++++++++++++++++++ .../ProjectApiAccess.tsx | 10 +- .../ProjectSettings/ProjectSettings.tsx | 2 +- frontend/src/hooks/usePlausibleTracker.ts | 3 +- src/server-dev.ts | 1 + 10 files changed, 210 insertions(+), 123 deletions(-) delete mode 100644 frontend/src/component/admin/apiToken/ApiTokenTable/ProjectApiTokenCreate.tsx create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiToken.tsx create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx rename frontend/src/component/project/Project/ProjectSettings/{ => ProjectApiAccess}/ProjectApiAccess.tsx (76%) diff --git a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx index 25bdabec38..b63896ac7c 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx +++ b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx @@ -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'; }) => ( ), minWidth: 280, @@ -246,44 +244,21 @@ export const ApiTokenTable = ({ /> } /> - - } - /> - - } - /> ); }; -//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', + }, + }; diff --git a/frontend/src/component/admin/apiToken/ApiTokenTable/ProjectApiTokenCreate.tsx b/frontend/src/component/admin/apiToken/ApiTokenTable/ProjectApiTokenCreate.tsx deleted file mode 100644 index 26eaec1398..0000000000 --- a/frontend/src/component/admin/apiToken/ApiTokenTable/ProjectApiTokenCreate.tsx +++ /dev/null @@ -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 ( - navigate(GO_BACK)} - label={`Create API token`} - > - - - ); -}; diff --git a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx index 9a63d346cd..e4f13fd8d4 100644 --- a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx +++ b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx @@ -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={ - - } + actions={} > { const permission = Boolean(project) ? CREATE_PROJECT_API_TOKEN : CREATE_API_TOKEN; + return ( { + const navigate = useNavigate(); + + return ( + navigate(GO_BACK)} + label={`Create API token`} + > + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx new file mode 100644 index 0000000000..f40a9e3c55 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx @@ -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 ( + + + } + > + + + + + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx similarity index 76% rename from frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess.tsx rename to frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx index 729dbd4a38..11e476761c 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx @@ -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} /> + + + } /> + ); }; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx index 972f48370c..7cef6a9f19 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx @@ -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 = () => { diff --git a/frontend/src/hooks/usePlausibleTracker.ts b/frontend/src/hooks/usePlausibleTracker.ts index 563f2d0aea..5106048404 100644 --- a/frontend/src/hooks/usePlausibleTracker.ts +++ b/frontend/src/hooks/usePlausibleTracker.ts @@ -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); diff --git a/src/server-dev.ts b/src/server-dev.ts index 106462b64d..f203514d45 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -42,6 +42,7 @@ process.nextTick(async () => { featuresExportImport: true, newProjectOverview: true, projectStatusApi: true, + showProjectApiAccess: true, }, }, authentication: {