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..492585620 100644 --- a/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts +++ b/ui/src/data-services/hooks/processing-services/useProcessingServiceDetails.ts @@ -10,17 +10,23 @@ 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..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 @@ -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' @@ -14,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) + const { processingService, isLoading, error } = 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..aad87537f 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,83 @@ 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 ( + + + + ) +}