|
@@ -6,6 +6,9 @@ import { useTheme } from '../../contexts/ThemeContext'
|
|
|
import { ConfiguratorFields, ConfigTarget } from './ConfiguratorFields'
|
|
import { ConfiguratorFields, ConfigTarget } from './ConfiguratorFields'
|
|
|
import { NodeOptionsSelect } from './NodeOptionsSelect'
|
|
import { NodeOptionsSelect } from './NodeOptionsSelect'
|
|
|
import { groupFields, isCredentialKey } from './fieldGroups'
|
|
import { groupFields, isCredentialKey } from './fieldGroups'
|
|
|
|
|
+import { CredentialSelect } from './CredentialSelect'
|
|
|
|
|
+import { EnumSelect } from './EnumSelect'
|
|
|
|
|
+import { KeyValueEditor } from './KeyValueEditor'
|
|
|
import { ArrayFieldEditor } from './ArrayFieldEditor'
|
|
import { ArrayFieldEditor } from './ArrayFieldEditor'
|
|
|
import { NodeDefinition, workflowsApi, nodesApi } from '../../api/workflows'
|
|
import { NodeDefinition, workflowsApi, nodesApi } from '../../api/workflows'
|
|
|
import { credentialsApi, CredentialInfo } from '../../api/credentials'
|
|
import { credentialsApi, CredentialInfo } from '../../api/credentials'
|
|
@@ -480,6 +483,7 @@ export function NodeConfigModal({
|
|
|
targets={configTargets}
|
|
targets={configTargets}
|
|
|
nodeDefs={nodeDefs}
|
|
nodeDefs={nodeDefs}
|
|
|
credentials={credentials}
|
|
credentials={credentials}
|
|
|
|
|
+ credentialTypeLabels={credentialTypeLabels}
|
|
|
editingConfig={editingConfig}
|
|
editingConfig={editingConfig}
|
|
|
onConfigChange={onConfigChange}
|
|
onConfigChange={onConfigChange}
|
|
|
/>
|
|
/>
|
|
@@ -716,59 +720,24 @@ export function NodeConfigModal({
|
|
|
</option>
|
|
</option>
|
|
|
))}
|
|
))}
|
|
|
</select>
|
|
</select>
|
|
|
- ) : prop.dynamicOptions?.source === 'credentials' ? (
|
|
|
|
|
- /* Dynamic credential selector */
|
|
|
|
|
- <select
|
|
|
|
|
- value={editingConfig[key] ?? prop.default ?? ''}
|
|
|
|
|
- onChange={(e) =>
|
|
|
|
|
- onConfigChange({ ...editingConfig, [key]: e.target.value })
|
|
|
|
|
- }
|
|
|
|
|
- className="w-full px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
|
|
|
- >
|
|
|
|
|
- <option value="">Select credential...</option>
|
|
|
|
|
- {credentials
|
|
|
|
|
- .filter(cred => {
|
|
|
|
|
- // Filter by credential type if specified. A node that
|
|
|
|
|
- // works with more than one type lists them all, so an
|
|
|
|
|
- // account stored once can serve both.
|
|
|
|
|
- // A filter naming a registered type also accepts
|
|
|
|
|
- // the plain credential it is stored as, and the
|
|
|
|
|
- // other way round: "sdcpp" and "basic" hold exactly
|
|
|
|
|
- // the same fields, so refusing one would only make
|
|
|
|
|
- // an existing credential unusable for no reason.
|
|
|
|
|
- const typeFilter = prop.dynamicOptions?.filter?.type
|
|
|
|
|
- if (!typeFilter) return true
|
|
|
|
|
- const wanted = Array.isArray(typeFilter) ? typeFilter : [typeFilter]
|
|
|
|
|
- return (
|
|
|
|
|
- wanted.includes(cred.type) ||
|
|
|
|
|
- (!!cred.declaredType && wanted.includes(cred.declaredType))
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
- .map((cred) => (
|
|
|
|
|
- <option key={cred.id} value={cred.id}>
|
|
|
|
|
- {cred.name} ({credentialTypeLabels[cred.declaredType || cred.type] || cred.type})
|
|
|
|
|
- </option>
|
|
|
|
|
- ))}
|
|
|
|
|
- </select>
|
|
|
|
|
|
|
+ ) : isCredentialKey(key, prop) ? (
|
|
|
|
|
+ /* Any setting that holds a stored credential, whether or
|
|
|
|
|
+ not the node asked for a picker - an id in a text box
|
|
|
|
|
+ is unreadable, and forgetting the declaration should
|
|
|
|
|
+ not be how that happens. */
|
|
|
|
|
+ <CredentialSelect
|
|
|
|
|
+ prop={prop}
|
|
|
|
|
+ value={editingConfig[key]}
|
|
|
|
|
+ credentials={credentials}
|
|
|
|
|
+ typeLabels={credentialTypeLabels}
|
|
|
|
|
+ onChange={(v) => onConfigChange({ ...editingConfig, [key]: v })}
|
|
|
|
|
+ />
|
|
|
) : prop.enum ? (
|
|
) : prop.enum ? (
|
|
|
- <select
|
|
|
|
|
- value={editingConfig[key] ?? prop.default ?? ''}
|
|
|
|
|
- onChange={(e) =>
|
|
|
|
|
- onConfigChange({ ...editingConfig, [key]: e.target.value })
|
|
|
|
|
- }
|
|
|
|
|
- className="w-full px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
|
|
|
- >
|
|
|
|
|
- {/* Only when the schema has not given "" a meaning of
|
|
|
|
|
- its own. Several settings use it for "auto" or
|
|
|
|
|
- "server default", and a placeholder above that would
|
|
|
|
|
- take its place and hide the label explaining it. */}
|
|
|
|
|
- {!prop.enum.includes('') && <option value="">Select...</option>}
|
|
|
|
|
- {prop.enum.map((opt: string, idx: number) => (
|
|
|
|
|
- <option key={opt} value={opt}>
|
|
|
|
|
- {prop.enumLabels?.[idx] || opt}
|
|
|
|
|
- </option>
|
|
|
|
|
- ))}
|
|
|
|
|
- </select>
|
|
|
|
|
|
|
+ <EnumSelect
|
|
|
|
|
+ prop={prop}
|
|
|
|
|
+ value={editingConfig[key]}
|
|
|
|
|
+ onChange={(v) => onConfigChange({ ...editingConfig, [key]: v })}
|
|
|
|
|
+ />
|
|
|
) : prop.type === 'array' ? (
|
|
) : prop.type === 'array' ? (
|
|
|
<ArrayFieldEditor
|
|
<ArrayFieldEditor
|
|
|
fieldKey={key}
|
|
fieldKey={key}
|
|
@@ -777,9 +746,18 @@ export function NodeConfigModal({
|
|
|
schema={nodeDef}
|
|
schema={nodeDef}
|
|
|
onChange={(next) => onConfigChange({ ...editingConfig, [key]: next })}
|
|
onChange={(next) => onConfigChange({ ...editingConfig, [key]: next })}
|
|
|
/>
|
|
/>
|
|
|
|
|
+ ) : prop.type === 'object' && prop.additionalProperties?.type === 'string' ? (
|
|
|
|
|
+ <KeyValueEditor
|
|
|
|
|
+ prop={prop}
|
|
|
|
|
+ value={editingConfig[key]}
|
|
|
|
|
+ onChange={(next) => onConfigChange({ ...editingConfig, [key]: next })}
|
|
|
|
|
+ />
|
|
|
) : prop.type === 'object' ? (
|
|
) : prop.type === 'object' ? (
|
|
|
|
|
+ /* Anything else in here carries numbers, booleans or
|
|
|
|
|
+ nesting - an options passthrough, a JSON Schema - and a
|
|
|
|
|
+ row of text boxes would quietly turn 7 into "7". */
|
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-slate-900 px-3 py-2 rounded">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-slate-900 px-3 py-2 rounded">
|
|
|
- Object configuration: Use JSON format
|
|
|
|
|
|
|
+ Written as JSON
|
|
|
<textarea
|
|
<textarea
|
|
|
value={JSON.stringify(editingConfig[key] || {}, null, 2)}
|
|
value={JSON.stringify(editingConfig[key] || {}, null, 2)}
|
|
|
onChange={(e) => {
|
|
onChange={(e) => {
|