-
Notifications
You must be signed in to change notification settings - Fork 12
feat(ui): API key management and client info for processing services #1201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<GenerateAPIKeyResponse>( | ||
| `${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, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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,83 @@ export const PopulateProcessingService = ({ | |||||||||||||||||||||||||||||||||||||||
| </BasicTooltip> | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+60
|
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling for clipboard API.
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (apiKey) { | ||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-2 p-3 border rounded-md bg-muted/50"> | ||||||||||||||||||||||||||||||||||||||||
| <p className="text-sm font-medium"> | ||||||||||||||||||||||||||||||||||||||||
| API Key (shown once, copy it now): | ||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||||||||||||
| <code className="text-xs bg-background px-2 py-1 rounded border break-all flex-1 font-mono"> | ||||||||||||||||||||||||||||||||||||||||
| {visible ? apiKey : '\u2022'.repeat(20)} | ||||||||||||||||||||||||||||||||||||||||
| </code> | ||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||
| aria-label={visible ? 'Hide API key' : 'Show API key'} | ||||||||||||||||||||||||||||||||||||||||
| onClick={() => setVisible((v) => !v)} | ||||||||||||||||||||||||||||||||||||||||
| size="small" | ||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| {visible ? ( | ||||||||||||||||||||||||||||||||||||||||
| <EyeOff className="w-4 h-4" /> | ||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||
| <Eye className="w-4 h-4" /> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||
| <Button onClick={handleCopy} size="small" variant="outline"> | ||||||||||||||||||||||||||||||||||||||||
| {copied ? 'Copied' : 'Copy'} | ||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <BasicTooltip | ||||||||||||||||||||||||||||||||||||||||
| asChild | ||||||||||||||||||||||||||||||||||||||||
| content={ | ||||||||||||||||||||||||||||||||||||||||
| error | ||||||||||||||||||||||||||||||||||||||||
| ? 'Could not generate API key.' | ||||||||||||||||||||||||||||||||||||||||
| : processingService.apiKeyPrefix | ||||||||||||||||||||||||||||||||||||||||
| ? `Current key prefix: ${processingService.apiKeyPrefix}. Generating a new key will revoke the current one.` | ||||||||||||||||||||||||||||||||||||||||
| : 'Generate an API key for this service to authenticate with.' | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||
| className={classNames({ 'text-destructive': error })} | ||||||||||||||||||||||||||||||||||||||||
| disabled={isLoading} | ||||||||||||||||||||||||||||||||||||||||
| onClick={() => generateAPIKey(processingService.id)} | ||||||||||||||||||||||||||||||||||||||||
| size="small" | ||||||||||||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| {error ? <AlertCircleIcon className="w-4 h-4" /> : null} | ||||||||||||||||||||||||||||||||||||||||
| <KeyRound className="w-4 h-4" /> | ||||||||||||||||||||||||||||||||||||||||
| <span> | ||||||||||||||||||||||||||||||||||||||||
| {processingService.apiKeyPrefix | ||||||||||||||||||||||||||||||||||||||||
| ? 'Regenerate API Key' | ||||||||||||||||||||||||||||||||||||||||
| : 'Generate API Key'} | ||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||
| {isLoading ? <Loader2 className="w-4 h-4 ml-2 animate-spin" /> : null} | ||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||
| </BasicTooltip> | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This hook hand-builds the details URL and always introduces a trailing slash when
projectIdis undefined (.../${processingServiceId}/). Elsewhere the UI usesgetFetchDetailsUrl()which omits the trailing slash when there are no query params and adds/?...only when needed. Please switch togetFetchDetailsUrl({ collection, itemId, projectId })(or mirror its behavior) to keep URL formatting consistent and avoid possible redirect/404 differences between/idvs/id/.