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: {