|
@@ -0,0 +1,139 @@
|
|
|
|
|
+import { Lock, Plus, ShieldAlert, AlertTriangle } from 'lucide-react'
|
|
|
|
|
+
|
|
|
|
|
+export interface StorageCollection {
|
|
|
|
|
+ name: string
|
|
|
|
|
+ access: 'none' | 'read-only' | 'read-write'
|
|
|
|
|
+ system?: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface CollectionSelectProps {
|
|
|
|
|
+ prop: any
|
|
|
|
|
+ value: any
|
|
|
|
|
+ // Every collection in the database, each carrying the access this workflow
|
|
|
|
|
+ // has to it.
|
|
|
|
|
+ collections: StorageCollection[]
|
|
|
|
|
+ // Grant this workflow access to a collection. Absent when granting is not
|
|
|
|
|
+ // possible from here - viewing an execution, say.
|
|
|
|
|
+ onGrant?: (name: string, access: 'read-only' | 'read-write') => void
|
|
|
|
|
+ onChange: (value: string) => void
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * The picker for a setting that names a storage collection.
|
|
|
|
|
+ *
|
|
|
|
|
+ * A workflow can only reach collections it has been granted, which is what
|
|
|
|
|
+ * stops one workflow quietly reading another's data. The consequence is a
|
|
|
|
|
+ * dropdown that is often empty, and the fix used to live somewhere else
|
|
|
|
|
+ * entirely - workflow Settings, Storage, add the name, come back. So the
|
|
|
|
|
+ * collections this workflow cannot reach are listed here too, with the grant
|
|
|
|
|
+ * offered in place.
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+function neededAccess(prop: any): 'read-only' | 'read-write' {
|
|
|
|
|
+ return prop?.dynamicOptions?.filter?.access === 'read-write' ? 'read-write' : 'read-only'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function satisfies(collection: StorageCollection, wanted: 'read-only' | 'read-write'): boolean {
|
|
|
|
|
+ if (collection.access === 'none') return false
|
|
|
|
|
+ return wanted === 'read-write' ? collection.access === 'read-write' : true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function CollectionSelect({
|
|
|
|
|
+ prop,
|
|
|
|
|
+ value,
|
|
|
|
|
+ collections,
|
|
|
|
|
+ onGrant,
|
|
|
|
|
+ onChange,
|
|
|
|
|
+}: CollectionSelectProps) {
|
|
|
|
|
+ const wanted = neededAccess(prop)
|
|
|
|
|
+ const current = String(value ?? prop?.default ?? '')
|
|
|
|
|
+
|
|
|
|
|
+ const usable = collections.filter((c) => satisfies(c, wanted))
|
|
|
|
|
+ // Real collections this workflow has not been given, or has been given too
|
|
|
|
|
+ // little of. Offering them is the point of this control.
|
|
|
|
|
+ const grantable = collections.filter((c) => !c.system && !satisfies(c, wanted))
|
|
|
|
|
+ // Never offerable, and saying so beats leaving someone hunting for why a
|
|
|
|
|
+ // collection they can see is not in the list.
|
|
|
|
|
+ const locked = collections.filter((c) => c.system)
|
|
|
|
|
+
|
|
|
|
|
+ // A name that is set but is not among the usable ones - granted and then
|
|
|
|
|
+ // revoked, or typed before the grant existed. A select whose value is not an
|
|
|
|
|
+ // option renders blank and the next save writes the blank away.
|
|
|
|
|
+ const orphaned = current !== '' && !usable.some((c) => c.name === current)
|
|
|
|
|
+ const orphanIsGrantable = grantable.some((c) => c.name === current)
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="space-y-1.5">
|
|
|
|
|
+ <select
|
|
|
|
|
+ value={current}
|
|
|
|
|
+ onChange={(e) => onChange(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 collection...</option>
|
|
|
|
|
+ {usable.map((c) => (
|
|
|
|
|
+ <option key={c.name} value={c.name}>
|
|
|
|
|
+ {c.name} ({c.access})
|
|
|
|
|
+ </option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ {orphaned && <option value={current}>{current} - not available here</option>}
|
|
|
|
|
+ </select>
|
|
|
|
|
+
|
|
|
|
|
+ {orphaned && (
|
|
|
|
|
+ <div className="flex items-start gap-1.5 text-[11px] text-amber-600 dark:text-amber-400">
|
|
|
|
|
+ <AlertTriangle className="w-3.5 h-3.5 shrink-0 mt-px" />
|
|
|
|
|
+ {orphanIsGrantable
|
|
|
|
|
+ ? `This workflow cannot ${wanted === 'read-write' ? 'write to' : 'read'} "${current}" yet. Give it access below, or the node will fail when it runs.`
|
|
|
|
|
+ : `"${current}" is not a collection this workflow can use. Kept so it is not lost.`}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {usable.length === 0 && !orphaned && (
|
|
|
|
|
+ <div className="flex items-start gap-1.5 text-[11px] text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ <Lock className="w-3.5 h-3.5 shrink-0 mt-px" />
|
|
|
|
|
+ This workflow has no collection it can {wanted === 'read-write' ? 'write to' : 'read'} yet.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {onGrant && grantable.length > 0 && (
|
|
|
|
|
+ <div className="rounded-lg border border-gray-200 dark:border-slate-700 p-2 space-y-1">
|
|
|
|
|
+ <div className="text-[11px] text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ Not available to this workflow. Granting one adds it to the workflow's
|
|
|
|
|
+ storage settings, and takes effect when the workflow is saved.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex flex-wrap gap-1.5">
|
|
|
|
|
+ {grantable.map((c) => (
|
|
|
|
|
+ <button
|
|
|
|
|
+ key={c.name}
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ onGrant(c.name, wanted)
|
|
|
|
|
+ onChange(c.name)
|
|
|
|
|
+ }}
|
|
|
|
|
+ title={
|
|
|
|
|
+ c.access === 'none'
|
|
|
|
|
+ ? `Give this workflow ${wanted} access to ${c.name}`
|
|
|
|
|
+ : `${c.name} is ${c.access} here, and this needs ${wanted}`
|
|
|
|
|
+ }
|
|
|
|
|
+ className="flex items-center gap-1 px-2 py-1 text-[11px] rounded border border-primary-200 dark:border-primary-800 text-primary-700 dark:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-900/20"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Plus className="w-3 h-3" />
|
|
|
|
|
+ {c.name}
|
|
|
|
|
+ {c.access !== 'none' && (
|
|
|
|
|
+ <span className="text-gray-400 dark:text-gray-500">({c.access})</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {locked.length > 0 && (
|
|
|
|
|
+ <div className="flex items-start gap-1.5 text-[11px] text-gray-400 dark:text-gray-500">
|
|
|
|
|
+ <ShieldAlert className="w-3.5 h-3.5 shrink-0 mt-px" />
|
|
|
|
|
+ {locked.map((c) => c.name).join(', ')} {locked.length === 1 ? 'is' : 'are'} kept
|
|
|
|
|
+ for the system and cannot be given to a workflow.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|