|
@@ -1,5 +1,6 @@
|
|
|
import { useState } from 'react'
|
|
import { useState } from 'react'
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
|
|
+import { nodesApi } from '../api/workflows'
|
|
|
import { format } from 'date-fns'
|
|
import { format } from 'date-fns'
|
|
|
|
|
|
|
|
// Helper to safely format timestamps that might be invalid or too large
|
|
// Helper to safely format timestamps that might be invalid or too large
|
|
@@ -41,6 +42,17 @@ import {
|
|
|
UpdateCredentialRequest,
|
|
UpdateCredentialRequest,
|
|
|
} from '../api/credentials'
|
|
} from '../api/credentials'
|
|
|
|
|
|
|
|
|
|
+// A credential type a node registered: a name over one of the built-in shapes,
|
|
|
|
|
+// so someone creating a credential can pick "SD.cpp Server" instead of guessing
|
|
|
|
|
+// that it means "basic".
|
|
|
|
|
+interface RegisteredType {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ label: string
|
|
|
|
|
+ baseType: CredentialType
|
|
|
|
|
+ description?: string
|
|
|
|
|
+ registeredBy: string[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const CREDENTIAL_TYPE_INFO: Record<
|
|
const CREDENTIAL_TYPE_INFO: Record<
|
|
|
CredentialType,
|
|
CredentialType,
|
|
|
{ label: string; icon: React.FC<{ className?: string }>; description: string }
|
|
{ label: string; icon: React.FC<{ className?: string }>; description: string }
|
|
@@ -344,6 +356,27 @@ function CredentialModal({ credential, onClose, onSuccess }: CredentialModalProp
|
|
|
const [name, setName] = useState(credential?.name || '')
|
|
const [name, setName] = useState(credential?.name || '')
|
|
|
const [description, setDescription] = useState(credential?.description || '')
|
|
const [description, setDescription] = useState(credential?.description || '')
|
|
|
const [type, setType] = useState<CredentialType>(credential?.type || 'basic')
|
|
const [type, setType] = useState<CredentialType>(credential?.type || 'basic')
|
|
|
|
|
+ // A node-registered type such as "sdcpp". It rides alongside the built-in
|
|
|
|
|
+ // type rather than replacing it - the shape is what gets stored.
|
|
|
|
|
+ const [declaredType, setDeclaredType] = useState<string>(credential?.declaredType || '')
|
|
|
|
|
+
|
|
|
|
|
+ const { data: nodesForTypes } = useQuery({
|
|
|
|
|
+ queryKey: ['node-registered-credential-types'],
|
|
|
|
|
+ queryFn: () => nodesApi.list(),
|
|
|
|
|
+ staleTime: 5 * 60 * 1000,
|
|
|
|
|
+ })
|
|
|
|
|
+ const registeredTypes: RegisteredType[] = (() => {
|
|
|
|
|
+ const found = new Map<string, RegisteredType>()
|
|
|
|
|
+ for (const def of (nodesForTypes?.nodes || [])) {
|
|
|
|
|
+ for (const ct of ((def as any).credentialTypes || [])) {
|
|
|
|
|
+ if (!ct?.id || !ct?.baseType) continue
|
|
|
|
|
+ const existing = found.get(ct.id)
|
|
|
|
|
+ if (existing) existing.registeredBy.push(def.name)
|
|
|
|
|
+ else found.set(ct.id, { ...ct, registeredBy: [def.name] })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Array.from(found.values())
|
|
|
|
|
+ })()
|
|
|
const [showSecrets, setShowSecrets] = useState(false)
|
|
const [showSecrets, setShowSecrets] = useState(false)
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
@@ -500,6 +533,7 @@ function CredentialModal({ credential, onClose, onSuccess }: CredentialModalProp
|
|
|
createMutation.mutate({
|
|
createMutation.mutate({
|
|
|
name: name.trim(),
|
|
name: name.trim(),
|
|
|
description: description.trim() || undefined,
|
|
description: description.trim() || undefined,
|
|
|
|
|
+ declaredType: declaredType || undefined,
|
|
|
type,
|
|
type,
|
|
|
data,
|
|
data,
|
|
|
})
|
|
})
|
|
@@ -569,6 +603,57 @@ function CredentialModal({ credential, onClose, onSuccess }: CredentialModalProp
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
Type *
|
|
Type *
|
|
|
</label>
|
|
</label>
|
|
|
|
|
+ {/* Types a node asked for, listed first. Someone creating a
|
|
|
|
|
+ credential is usually here because a node told them to, and
|
|
|
|
|
+ "SD.cpp Server" is findable in a way that "Basic Auth" is
|
|
|
|
|
+ not. Choosing one still stores the shape it is built on. */}
|
|
|
|
|
+ {registeredTypes.length > 0 && (
|
|
|
|
|
+ <div className="mb-3">
|
|
|
|
|
+ <div className="text-[11px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500 mb-1.5">
|
|
|
|
|
+ For a specific service
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-2">
|
|
|
|
|
+ {registeredTypes.map((rt) => (
|
|
|
|
|
+ <button
|
|
|
|
|
+ key={rt.id}
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setDeclaredType(rt.id)
|
|
|
|
|
+ setType(rt.baseType)
|
|
|
|
|
+ }}
|
|
|
|
|
+ className={`flex items-center gap-3 p-3 rounded-lg border-2 text-left transition-colors ${
|
|
|
|
|
+ declaredType === rt.id
|
|
|
|
|
+ ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/30'
|
|
|
|
|
+ : 'border-gray-200 dark:border-slate-600 hover:border-gray-300 dark:hover:border-slate-500'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <KeyRound
|
|
|
|
|
+ className={`w-5 h-5 ${
|
|
|
|
|
+ declaredType === rt.id ? 'text-primary-600 dark:text-primary-400' : 'text-gray-400'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`font-medium text-sm ${
|
|
|
|
|
+ declaredType === rt.id
|
|
|
|
|
+ ? 'text-primary-700 dark:text-primary-300'
|
|
|
|
|
+ : 'text-gray-900 dark:text-gray-100'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ {rt.label}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ {rt.description || `Stored as ${rt.baseType}`}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-[11px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500 mt-3 mb-1.5">
|
|
|
|
|
+ Or a general one
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
{(Object.keys(CREDENTIAL_TYPE_INFO) as CredentialType[]).map((t) => {
|
|
{(Object.keys(CREDENTIAL_TYPE_INFO) as CredentialType[]).map((t) => {
|
|
|
const info = CREDENTIAL_TYPE_INFO[t]
|
|
const info = CREDENTIAL_TYPE_INFO[t]
|
|
@@ -577,9 +662,12 @@ function CredentialModal({ credential, onClose, onSuccess }: CredentialModalProp
|
|
|
<button
|
|
<button
|
|
|
key={t}
|
|
key={t}
|
|
|
type="button"
|
|
type="button"
|
|
|
- onClick={() => setType(t)}
|
|
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setType(t)
|
|
|
|
|
+ setDeclaredType('')
|
|
|
|
|
+ }}
|
|
|
className={`flex items-center gap-3 p-3 rounded-lg border-2 text-left transition-colors ${
|
|
className={`flex items-center gap-3 p-3 rounded-lg border-2 text-left transition-colors ${
|
|
|
- type === t
|
|
|
|
|
|
|
+ type === t && !declaredType
|
|
|
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/30'
|
|
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/30'
|
|
|
: 'border-gray-200 dark:border-slate-600 hover:border-gray-300 dark:hover:border-slate-500'
|
|
: 'border-gray-200 dark:border-slate-600 hover:border-gray-300 dark:hover:border-slate-500'
|
|
|
}`}
|
|
}`}
|