feat(ui): API key management and client info for processing services#1201
feat(ui): API key management and client info for processing services#1201
Conversation
…ng 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 <noreply@anthropic.com>
✅ Deploy Preview for antenna-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for antenna-ssec ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis pull request introduces API key generation functionality for processing services. It adds a new React hook for API key mutations, extends the ProcessingService model with API key and worker information properties, updates the processing service details query to support project filtering, and adds UI components to display and manage generated API keys with show/hide and copy-to-clipboard features. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds UI support for processing-service API key management and worker/client metadata display, aligning the frontend with the new processing-service authentication + heartbeat model introduced in the backend work.
Changes:
- Adds “Generate/Regenerate API Key” UX (reveal + copy) and wires it to a new mutation hook.
- Extends processing-service details to show an Authentication section (prefix + mode) and a Last Known Worker section from
client_info. - Updates processing-service details fetching to support project-scoped queries and adjusts async status display based on heartbeat recency.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/pages/project/processing-services/processing-services-actions.tsx | Adds GenerateAPIKey component (copy/reveal UX) and hooks it into processing service actions. |
| ui/src/pages/project/entities/details-form/processing-service-details-form.tsx | Makes endpoint_url optional in the form config with updated description. |
| ui/src/pages/processing-service-details/processing-service-details-dialog.tsx | Adds new details sections (Authentication, Last Known Worker) and passes projectId into the details hook. |
| ui/src/data-services/models/processing-service.ts | Adds getters for apiKeyPrefix and lastSeenClientInfo; updates async status derivation from heartbeat. |
| ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts | Adds optional projectId parameter and includes it in query key and request URL. |
| ui/src/data-services/hooks/processing-services/useGenerateAPIKey.ts | New mutation hook to call generate_key endpoint and invalidate processing service queries. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const handleCopy = async () => { | ||
| if (apiKey) { | ||
| await navigator.clipboard.writeText(apiKey) | ||
| setCopied(true) | ||
| setTimeout(() => setCopied(false), 2000) | ||
| } |
There was a problem hiding this comment.
handleCopy awaits navigator.clipboard.writeText(apiKey) without any error handling. In non-secure contexts or when clipboard permissions are denied, this will throw and can result in an unhandled promise rejection (and the UI never reflects the failure). Wrap the clipboard call in try/catch (and/or guard on navigator.clipboard) and provide a fallback/error message; also consider clearing the 2s timeout on unmount to avoid setting state after the dialog closes.
| 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}`, |
There was a problem hiding this comment.
This hook hand-builds the details URL and always introduces a trailing slash when projectId is undefined (.../${processingServiceId}/). Elsewhere the UI uses getFetchDetailsUrl() which omits the trailing slash when there are no query params and adds /?... only when needed. Please switch to getFetchDetailsUrl({ collection, itemId, projectId }) (or mirror its behavior) to keep URL formatting consistent and avoid possible redirect/404 differences between /id vs /id/.
| url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}/${params}`, | |
| url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}${params}`, |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
ui/src/pages/project/processing-services/processing-services-actions.tsx (1)
62-90: Timer not cleaned up on unmount.The
setTimeoutinhandleCopycould fire after the component unmounts if the user navigates away quickly, causing a state update on an unmounted component.Consider using useEffect cleanup or a ref
+import { useEffect, useRef, useState } from 'react' 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 timerRef = useRef<ReturnType<typeof setTimeout>>() + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current) + } + }, []) const handleCopy = async () => { if (apiKey) { await navigator.clipboard.writeText(apiKey) setCopied(true) - setTimeout(() => setCopied(false), 2000) + timerRef.current = setTimeout(() => setCopied(false), 2000) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/project/processing-services/processing-services-actions.tsx` around lines 62 - 90, The timeout created in handleCopy can update state after unmount; to fix, store the timeout id in a ref (e.g., copyTimeoutRef) when calling setTimeout inside handleCopy and clearTimeout(copyTimeoutRef.current) in a useEffect cleanup (returning a function that clears it) or clear it in a componentWillUnmount equivalent; update the ProcessingServicesActions component to use that ref and cleanup to prevent setCopied state updates after unmount.ui/src/data-services/models/processing-service.ts (1)
72-74: Redundant nullish coalescing withundefined.The
?? undefinedis a no-op since the nullish coalescing operator already returnsundefinedwhen the left side isnullorundefined.Suggested simplification
get apiKeyPrefix(): string | undefined { - return this._processingService.api_key_prefix ?? undefined + return this._processingService.api_key_prefix }Same applies to
lastSeenClientInfoon line 86.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/data-services/models/processing-service.ts` around lines 72 - 74, Remove the redundant "?? undefined" nullish coalescing in the getter so it simply returns the underlying value; specifically update the apiKeyPrefix getter to return this._processingService.api_key_prefix directly (and apply the same simplification to the lastSeenClientInfo accessor that currently uses "?? undefined"), ensuring the property types remain string | undefined.ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts (1)
21-25: URL has extraneous slash before query parameters.When
projectIdis provided, the URL becomes.../processing-services/${id}/?project_id=...with a trailing slash before the query string. While this typically works, it's inconsistent with standard URL conventions.Suggested fix
- const params = projectId ? `?project_id=${projectId}` : '' + const params = projectId ? `?project_id=${projectId}` : '' const { data, isLoading, isFetching, error } = useAuthorizedQuery<ProcessingService>({ queryKey: [API_ROUTES.PROCESSING_SERVICES, processingServiceId, projectId], - url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}/${params}`, + url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}/${params ? params : ''}`,Or more cleanly:
- const params = projectId ? `?project_id=${projectId}` : '' - const { data, isLoading, isFetching, error } = - useAuthorizedQuery<ProcessingService>({ - queryKey: [API_ROUTES.PROCESSING_SERVICES, processingServiceId, projectId], - url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}/${params}`, + const { data, isLoading, isFetching, error } = + useAuthorizedQuery<ProcessingService>({ + queryKey: [API_ROUTES.PROCESSING_SERVICES, processingServiceId, projectId], + url: `${API_URL}/${API_ROUTES.PROCESSING_SERVICES}/${processingServiceId}/${projectId ? `?project_id=${projectId}` : ''}`,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts` around lines 21 - 25, The URL construction in useProcessingServiceDetails is adding an extra slash before query params (params currently set to include '?' then appended after a trailing slash), so change how params are built and appended: keep params as just the query fragment (e.g., project_id=...), or keep the '?' in params but remove the hard-coded trailing slash before ${params}; update the URL assembly used in useAuthorizedQuery (the line with API_URL/API_ROUTES/processingServiceId) so it conditionally appends the query string without introducing '/?' — reference variables/functions: params, processingServiceId, API_URL, API_ROUTES, and useProcessingServiceDetails.ui/src/pages/processing-service-details/processing-service-details-dialog.tsx (2)
97-109: Hardcoded strings bypass i18n system.The new sections use hardcoded English strings ("Authentication", "API Key Prefix", "Mode", "Pull (async)", "Push (sync)", etc.) while other fields use
translate(STRING.*). This creates an i18n inconsistency.Consider adding these strings to the translation system for consistency, or leave a TODO comment if i18n support for these fields is planned for later.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/processing-service-details/processing-service-details-dialog.tsx` around lines 97 - 109, Replace the hardcoded English strings in the Authentication block with i18n lookups: wrap the FormSection title and each InputValue label and the mode display text in translate(STRING.*) calls (e.g., replace "Authentication", "API Key Prefix", "Mode", "Pull (async)", "Push (sync)" with translate(STRING.YOUR_KEY_*)), add corresponding keys to the STRING enum/translation resource and localized JSON files, and update the mode rendering to choose the translated string based on processingService.isAsync; if translation entries will be added later, add a clear TODO comment near FormSection/InputValue/GenerateAPIKey referencing the need to replace these hardcoded strings with translate(STRING.*).
119-127: Complex ternary for software display.The nested conditional is hard to parse at a glance.
Consider extracting to a helper function for readability
const getSoftwareDisplay = (clientInfo: typeof processingService.lastSeenClientInfo) => { if (!clientInfo) return undefined const { software, version } = clientInfo if (software && version) return `${software} ${version}` if (software) return software if (version) return `v${version}` return undefined }Then use:
<InputValue label="Software" value={getSoftwareDisplay(processingService.lastSeenClientInfo)} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/processing-service-details/processing-service-details-dialog.tsx` around lines 119 - 127, Extract the complex nested ternary that builds the software string into a small helper (e.g., getSoftwareDisplay) and call it from the InputValue; specifically, implement getSoftwareDisplay(clientInfo: typeof processingService.lastSeenClientInfo) that returns `${software} ${version}` when both exist, software when only software exists, `v${version}` when only version exists, and undefined otherwise, then replace the inline ternary in the value prop of InputValue with getSoftwareDisplay(processingService.lastSeenClientInfo) to improve readability and maintain behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui/src/pages/project/processing-services/processing-services-actions.tsx`:
- Around line 54-60: The handleCopy function currently calls
navigator.clipboard.writeText(apiKey) without handling rejections; wrap that
call in try/catch (or attach .catch) inside handleCopy to handle failures, log
or surface the error via an existing logger or UI (e.g., set an error state or
show a toast), and only setCopied(true) when the write succeeds; also keep the
existing timeout to clear the copied flag. Target the handleCopy function and
update its clipboard write invocation accordingly.
---
Nitpick comments:
In
`@ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts`:
- Around line 21-25: The URL construction in useProcessingServiceDetails is
adding an extra slash before query params (params currently set to include '?'
then appended after a trailing slash), so change how params are built and
appended: keep params as just the query fragment (e.g., project_id=...), or keep
the '?' in params but remove the hard-coded trailing slash before ${params};
update the URL assembly used in useAuthorizedQuery (the line with
API_URL/API_ROUTES/processingServiceId) so it conditionally appends the query
string without introducing '/?' — reference variables/functions: params,
processingServiceId, API_URL, API_ROUTES, and useProcessingServiceDetails.
In `@ui/src/data-services/models/processing-service.ts`:
- Around line 72-74: Remove the redundant "?? undefined" nullish coalescing in
the getter so it simply returns the underlying value; specifically update the
apiKeyPrefix getter to return this._processingService.api_key_prefix directly
(and apply the same simplification to the lastSeenClientInfo accessor that
currently uses "?? undefined"), ensuring the property types remain string |
undefined.
In
`@ui/src/pages/processing-service-details/processing-service-details-dialog.tsx`:
- Around line 97-109: Replace the hardcoded English strings in the
Authentication block with i18n lookups: wrap the FormSection title and each
InputValue label and the mode display text in translate(STRING.*) calls (e.g.,
replace "Authentication", "API Key Prefix", "Mode", "Pull (async)", "Push
(sync)" with translate(STRING.YOUR_KEY_*)), add corresponding keys to the STRING
enum/translation resource and localized JSON files, and update the mode
rendering to choose the translated string based on processingService.isAsync; if
translation entries will be added later, add a clear TODO comment near
FormSection/InputValue/GenerateAPIKey referencing the need to replace these
hardcoded strings with translate(STRING.*).
- Around line 119-127: Extract the complex nested ternary that builds the
software string into a small helper (e.g., getSoftwareDisplay) and call it from
the InputValue; specifically, implement getSoftwareDisplay(clientInfo: typeof
processingService.lastSeenClientInfo) that returns `${software} ${version}` when
both exist, software when only software exists, `v${version}` when only version
exists, and undefined otherwise, then replace the inline ternary in the value
prop of InputValue with getSoftwareDisplay(processingService.lastSeenClientInfo)
to improve readability and maintain behavior.
In `@ui/src/pages/project/processing-services/processing-services-actions.tsx`:
- Around line 62-90: The timeout created in handleCopy can update state after
unmount; to fix, store the timeout id in a ref (e.g., copyTimeoutRef) when
calling setTimeout inside handleCopy and clearTimeout(copyTimeoutRef.current) in
a useEffect cleanup (returning a function that clears it) or clear it in a
componentWillUnmount equivalent; update the ProcessingServicesActions component
to use that ref and cleanup to prevent setCopied state updates after unmount.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cfa163b8-1f69-4dbc-bc19-b99ac68ddd46
📒 Files selected for processing (6)
ui/src/data-services/hooks/processing-services/useGenerateAPIKey.tsui/src/data-services/hooks/processing-services/useProcessingServiceDetails.tsui/src/data-services/models/processing-service.tsui/src/pages/processing-service-details/processing-service-details-dialog.tsxui/src/pages/project/entities/details-form/processing-service-details-form.tsxui/src/pages/project/processing-services/processing-services-actions.tsx
| const handleCopy = async () => { | ||
| if (apiKey) { | ||
| await navigator.clipboard.writeText(apiKey) | ||
| setCopied(true) | ||
| setTimeout(() => setCopied(false), 2000) | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing error handling for clipboard API.
navigator.clipboard.writeText() can fail (e.g., in insecure contexts, or if permission is denied). The rejected promise is unhandled.
Suggested fix with error handling
const handleCopy = async () => {
if (apiKey) {
- await navigator.clipboard.writeText(apiKey)
- setCopied(true)
- setTimeout(() => setCopied(false), 2000)
+ try {
+ await navigator.clipboard.writeText(apiKey)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ } catch {
+ // Optionally show an error state or fallback
+ console.error('Failed to copy to clipboard')
+ }
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleCopy = async () => { | |
| if (apiKey) { | |
| await navigator.clipboard.writeText(apiKey) | |
| setCopied(true) | |
| setTimeout(() => setCopied(false), 2000) | |
| } | |
| } | |
| const handleCopy = async () => { | |
| if (apiKey) { | |
| try { | |
| await navigator.clipboard.writeText(apiKey) | |
| setCopied(true) | |
| setTimeout(() => setCopied(false), 2000) | |
| } catch { | |
| // Optionally show an error state or fallback | |
| console.error('Failed to copy to clipboard') | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/pages/project/processing-services/processing-services-actions.tsx`
around lines 54 - 60, The handleCopy function currently calls
navigator.clipboard.writeText(apiKey) without handling rejections; wrap that
call in try/catch (or attach .catch) inside handleCopy to handle failures, log
or surface the error via an existing logger or UI (e.g., set an error state or
show a toast), and only setCopied(true) when the write succeeds; also keep the
existing timeout to clear the copied flag. Target the handleCopy function and
update its clipboard write invocation accordingly.
Co-Authored-By: Claude <noreply@anthropic.com>

Summary
Depends on #1194 (backend auth).
Permission and ownership notes for future UI work
The following considerations were surfaced during E2E testing and should inform the UI design:
API key scoping: The API key is tied to the ProcessingService (PS), not to a specific project. Since a PS has a M2M with projects, one API key grants access to all linked projects. Removing a PS from a project immediately denies access (checked live on each request).
Who can generate/revoke keys: Currently any project member who can list PSes can generate keys. This should probably be restricted to project admins/managers — key revocation affects all projects the PS serves.
User-created vs global PSes: Consider adding a
created_byfield to ProcessingService. Global PSes (admin-created) can be managed by admins; user-created PSes only by their creator + project admins. This prevents unauthorized key revocation.Multi-project PS management: A user who creates a PS and belongs to multiple projects should be able to add the same PS to other projects they're a member of. The UI could show "My Processing Services" for self-service linking.
Org-level ownership: When the organization-level object is added above projects, PSes could also be org-owned, simplifying multi-project sharing within an organization.
Test plan
cd ui && yarn buildCo-Authored-By: Claude noreply@anthropic.com