From 22326f42dd99aa60d0a36ae3cd649a10a5f839b0 Mon Sep 17 00:00:00 2001 From: Michael Bunsen Date: Wed, 1 Apr 2026 12:06:29 -0700 Subject: [PATCH 1/2] feat(ui): add API key management and client info display for processing services - Add GenerateAPIKey component with copy/reveal/regenerate UX - Add useGenerateAPIKey hook for POST /generate_key/ endpoint - Show "Authentication" section in PS details (key prefix, mode) - Show "Last Known Worker" section with client info (hostname, software, platform, IP) - Add apiKeyPrefix and lastSeenClientInfo getters to PS model - Pass projectId to useProcessingServiceDetails for scoped queries - Make endpoint_url optional for pull-mode services - Fix async service status: show ONLINE when lastSeenLive is true Co-Authored-By: Claude --- .../processing-services/useGenerateAPIKey.ts | 39 +++++++++ .../useProcessingServiceDetails.ts | 8 +- .../models/processing-service.ts | 21 ++++- .../processing-service-details-dialog.tsx | 49 ++++++++++- .../processing-service-details-form.tsx | 6 +- .../processing-services-actions.tsx | 84 ++++++++++++++++++- 6 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 ui/src/data-services/hooks/processing-services/useGenerateAPIKey.ts diff --git a/ui/src/data-services/hooks/processing-services/useGenerateAPIKey.ts b/ui/src/data-services/hooks/processing-services/useGenerateAPIKey.ts new file mode 100644 index 000000000..7997629a5 --- /dev/null +++ b/ui/src/data-services/hooks/processing-services/useGenerateAPIKey.ts @@ -0,0 +1,39 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import axios from 'axios' +import { API_ROUTES, API_URL } from 'data-services/constants' +import { getAuthHeader } from 'data-services/utils' +import { useUser } from 'utils/user/userContext' + +interface GenerateAPIKeyResponse { + api_key: string + prefix: string + message: string +} + +export const useGenerateAPIKey = (projectId?: string) => { + const { user } = useUser() + const queryClient = useQueryClient() + const params = projectId ? `?project_id=${projectId}` : '' + + const { mutateAsync, isLoading, isSuccess, error, data } = useMutation({ + mutationFn: (id: string) => + axios.post( + `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${id}/generate_key/${params}`, + undefined, + { + headers: getAuthHeader(user), + } + ), + onSuccess: () => { + queryClient.invalidateQueries([API_ROUTES.PROCESSING_SERVICES]) + }, + }) + + return { + generateAPIKey: mutateAsync, + isLoading, + isSuccess, + error, + apiKey: data?.data.api_key, + } +} diff --git a/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts b/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts index bda9b3c78..15c731cd1 100644 --- a/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts +++ b/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts @@ -10,17 +10,19 @@ const convertServerRecord = (record: ServerProcessingService) => new ProcessingService(record) export const useProcessingServiceDetails = ( - processingServiceId: string + processingServiceId: string, + projectId?: string ): { processingService?: ProcessingService isLoading: boolean isFetching: boolean error?: unknown } => { + const params = projectId ? `?project_id=${projectId}` : '' const { data, isLoading, isFetching, error } = useAuthorizedQuery({ - queryKey: [API_ROUTES.PROCESSING_SERVICES, processingServiceId], - url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}`, + queryKey: [API_ROUTES.PROCESSING_SERVICES, processingServiceId, projectId], + url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}/${params}`, }) const processingService = useMemo( diff --git a/ui/src/data-services/models/processing-service.ts b/ui/src/data-services/models/processing-service.ts index 627a19616..d805979fa 100644 --- a/ui/src/data-services/models/processing-service.ts +++ b/ui/src/data-services/models/processing-service.ts @@ -69,6 +69,23 @@ export class ProcessingService extends Entity { return this._processingService.last_seen_live ?? false } + get apiKeyPrefix(): string | undefined { + return this._processingService.api_key_prefix ?? undefined + } + + get lastSeenClientInfo(): + | { + hostname?: string + software?: string + version?: string + platform?: string + ip?: string + user_agent?: string + } + | undefined { + return this._processingService.last_seen_client_info ?? undefined + } + get numPiplinesAdded(): number { return this._pipelines.length } @@ -80,7 +97,9 @@ export class ProcessingService extends Entity { color: string } { if (this.isAsync) { - return ProcessingService.getStatusInfo('UNKNOWN') + // Async services derive status from heartbeat + const status_code = this.lastSeenLive ? 'ONLINE' : 'UNKNOWN' + return ProcessingService.getStatusInfo(status_code) } const status_code = this.lastSeenLive ? 'ONLINE' : 'OFFLINE' return ProcessingService.getStatusInfo(status_code) diff --git a/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx b/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx index 895b820c9..9c1f4029a 100644 --- a/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx +++ b/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx @@ -4,6 +4,7 @@ import { ProcessingService } from 'data-services/models/processing-service' import * as Dialog from 'design-system/components/dialog/dialog' import { InputValue } from 'design-system/components/input/input' import _ from 'lodash' +import { GenerateAPIKey } from 'pages/project/processing-services/processing-services-actions' import { useNavigate, useParams } from 'react-router-dom' import { APP_ROUTES } from 'utils/constants' import { getAppRoute } from 'utils/getAppRoute' @@ -15,7 +16,7 @@ export const ProcessingServiceDetailsDialog = ({ id }: { id: string }) => { const navigate = useNavigate() const { projectId } = useParams() const { processingService, isLoading, error } = - useProcessingServiceDetails(id) + useProcessingServiceDetails(id, projectId) return ( - + + + + + + + + {processingService.lastSeenClientInfo && ( + + + + + + + + + + + )} {processingService.pipelines.length > 0 && (
diff --git a/ui/src/pages/project/entities/details-form/processing-service-details-form.tsx b/ui/src/pages/project/entities/details-form/processing-service-details-form.tsx index 2a4bb0649..a4322b2fd 100644 --- a/ui/src/pages/project/entities/details-form/processing-service-details-form.tsx +++ b/ui/src/pages/project/entities/details-form/processing-service-details-form.tsx @@ -28,10 +28,8 @@ const config: FormConfig = { }, endpoint_url: { label: 'Endpoint URL', - description: 'Processing service endpoint.', - rules: { - required: true, - }, + description: + 'Processing service endpoint. Leave empty for pull-mode services that register themselves.', }, description: { label: translate(STRING.FIELD_LABEL_DESCRIPTION), diff --git a/ui/src/pages/project/processing-services/processing-services-actions.tsx b/ui/src/pages/project/processing-services/processing-services-actions.tsx index d14262e1e..64347fcc9 100644 --- a/ui/src/pages/project/processing-services/processing-services-actions.tsx +++ b/ui/src/pages/project/processing-services/processing-services-actions.tsx @@ -1,9 +1,12 @@ import classNames from 'classnames' +import { useGenerateAPIKey } from 'data-services/hooks/processing-services/useGenerateAPIKey' import { usePopulateProcessingService } from 'data-services/hooks/processing-services/usePopulateProcessingService' import { ProcessingService } from 'data-services/models/processing-service' import { BasicTooltip } from 'design-system/components/tooltip/basic-tooltip' -import { AlertCircleIcon, Loader2 } from 'lucide-react' +import { AlertCircleIcon, Eye, EyeOff, KeyRound, Loader2 } from 'lucide-react' import { Button } from 'nova-ui-kit' +import { useState } from 'react' +import { useParams } from 'react-router-dom' import { STRING, translate } from 'utils/language' export const PopulateProcessingService = ({ @@ -37,3 +40,82 @@ export const PopulateProcessingService = ({ ) } + +export const GenerateAPIKey = ({ + processingService, +}: { + processingService: ProcessingService +}) => { + const { projectId } = useParams() + const { generateAPIKey, isLoading, error, apiKey } = useGenerateAPIKey(projectId) + const [copied, setCopied] = useState(false) + const [visible, setVisible] = useState(false) + + const handleCopy = async () => { + if (apiKey) { + await navigator.clipboard.writeText(apiKey) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + + if (apiKey) { + return ( +
+

+ API Key (shown once, copy it now): +

+
+ + {visible ? apiKey : '\u2022'.repeat(20)} + + + +
+
+ ) + } + + return ( + + + + ) +} From 179ba5af227d7a221c5796b76e95810c46ff7ae2 Mon Sep 17 00:00:00 2001 From: Michael Bunsen Date: Wed, 1 Apr 2026 12:14:18 -0700 Subject: [PATCH 2/2] style: fix prettier formatting to match project's prettier 2.8.4 Co-Authored-By: Claude --- .../processing-services/useProcessingServiceDetails.ts | 6 +++++- .../processing-service-details-dialog.tsx | 6 ++++-- .../processing-services/processing-services-actions.tsx | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts b/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts index 15c731cd1..492585620 100644 --- a/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts +++ b/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts @@ -21,7 +21,11 @@ export const useProcessingServiceDetails = ( const params = projectId ? `?project_id=${projectId}` : '' const { data, isLoading, isFetching, error } = useAuthorizedQuery({ - queryKey: [API_ROUTES.PROCESSING_SERVICES, processingServiceId, projectId], + queryKey: [ + API_ROUTES.PROCESSING_SERVICES, + processingServiceId, + projectId, + ], url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}/${params}`, }) diff --git a/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx b/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx index 9c1f4029a..90053d9e0 100644 --- a/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx +++ b/ui/src/pages/processing-service-details/processing-service-details-dialog.tsx @@ -15,8 +15,10 @@ import styles from './styles.module.scss' export const ProcessingServiceDetailsDialog = ({ id }: { id: string }) => { const navigate = useNavigate() const { projectId } = useParams() - const { processingService, isLoading, error } = - useProcessingServiceDetails(id, projectId) + const { processingService, isLoading, error } = useProcessingServiceDetails( + id, + projectId + ) return ( { const { projectId } = useParams() - const { generateAPIKey, isLoading, error, apiKey } = useGenerateAPIKey(projectId) + const { generateAPIKey, isLoading, error, apiKey } = + useGenerateAPIKey(projectId) const [copied, setCopied] = useState(false) const [visible, setVisible] = useState(false)