mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 08:01:35 +00:00
* fix(frontend): scope raw-app/flow/script editors to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow and script editor operations to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review — thread session workspace through flow resource pickers, script fetch, preview cancel/recording and path collision check Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Pi review — show acting workspace in script-not-found message and fetch picked script from it in EditorBar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 2 — thread session workspace into flow step test, raw-app inline runnable, inline editor toolbars and MCP OAuth path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 3 — thread session workspace into dynamic-input helpers and the flow-preview argument side panel (history/saved-inputs/captures) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 4 — thread session workspace into nested flow/script drawers, flow chat inputs and the flow input side tabs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 5 — thread session workspace into script-module fork/reload and key the raw-app schema cache by workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 6 — key the DB manager schema cache by acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 7 — thread session workspace into resource-valued arg pickers and the editor variable/resource helper drawers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through flow asset explore controls Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
224 lines
6.1 KiB
TypeScript
224 lines
6.1 KiB
TypeScript
import { z } from 'zod'
|
||
import { useSearchParams } from '$lib/svelte5UtilsKit.svelte'
|
||
import type { DbInput, DbType } from './dbTypes'
|
||
import { isDbType } from './dbTypes'
|
||
|
||
/**
|
||
* Single URL param `dbm` encodes the full DB manager state:
|
||
* firstSegment~path~schema.table
|
||
*
|
||
* firstSegment:
|
||
* datatable – database with datatable:// resource (resourceType always postgresql)
|
||
* ducklake – ducklake connection
|
||
* postgresql, mysql, … – regular database (the segment IS the resource type)
|
||
*
|
||
* schema.table (third segment, optional):
|
||
* schema.table – both
|
||
* .table – table only
|
||
* schema. – schema only
|
||
* (omitted) – neither
|
||
*
|
||
* Default schemas (omitted from URL, restored on parse):
|
||
* datatable → public
|
||
* ducklake → main
|
||
*
|
||
* Examples:
|
||
* datatable~main~.customers (schema "public" implied)
|
||
* ducklake~main~.orders (schema "main" implied)
|
||
* postgresql~$res:u/user/my_pg~public.customers
|
||
*/
|
||
|
||
const dbManagerSchema = z.object({
|
||
dbm: z.string().nullable()
|
||
})
|
||
|
||
interface ParsedDbm {
|
||
type: 'database' | 'datatable' | 'ducklake'
|
||
path: string
|
||
resType?: string
|
||
schema?: string
|
||
table?: string
|
||
}
|
||
|
||
function parseDbm(raw: unknown): ParsedDbm | null {
|
||
if (!raw || typeof raw !== 'string') return null
|
||
const parts = raw.split('~')
|
||
if (parts.length < 2 || !parts[1]) return null
|
||
|
||
const firstSeg = parts[0]
|
||
const path = parts[1]
|
||
const schemaTable = parts[2] ?? ''
|
||
|
||
let type: ParsedDbm['type']
|
||
let resType: string | undefined
|
||
if (firstSeg === 'datatable') {
|
||
type = 'datatable'
|
||
} else if (firstSeg === 'ducklake') {
|
||
type = 'ducklake'
|
||
} else if (isDbType(firstSeg)) {
|
||
type = 'database'
|
||
resType = firstSeg
|
||
} else {
|
||
return null
|
||
}
|
||
|
||
let schema: string | undefined
|
||
let table: string | undefined
|
||
if (schemaTable) {
|
||
const dotIdx = schemaTable.indexOf('.')
|
||
if (dotIdx === 0) {
|
||
table = schemaTable.slice(1) || undefined
|
||
} else if (dotIdx === schemaTable.length - 1) {
|
||
schema = schemaTable.slice(0, -1) || undefined
|
||
} else if (dotIdx > 0) {
|
||
schema = schemaTable.slice(0, dotIdx)
|
||
table = schemaTable.slice(dotIdx + 1)
|
||
}
|
||
}
|
||
|
||
// Restore default schema when omitted for datatable/ducklake
|
||
if (!schema && table && type in defaultSchemas) {
|
||
schema = defaultSchemas[type]
|
||
}
|
||
|
||
return { type, path, resType, schema, table }
|
||
}
|
||
|
||
const defaultSchemas: Record<string, string> = { datatable: 'public', ducklake: 'main' }
|
||
|
||
function buildDbm(p: ParsedDbm): string {
|
||
const firstSeg = p.type === 'database' ? p.resType! : p.type
|
||
const schema = p.schema === defaultSchemas[p.type] ? undefined : p.schema
|
||
let schemaTable = ''
|
||
if (schema && p.table) {
|
||
schemaTable = `${schema}.${p.table}`
|
||
} else if (p.table) {
|
||
schemaTable = `.${p.table}`
|
||
} else if (schema) {
|
||
schemaTable = `${schema}.`
|
||
}
|
||
return schemaTable ? `${firstSeg}~${p.path}~${schemaTable}` : `${firstSeg}~${p.path}`
|
||
}
|
||
|
||
export interface DbManagerUriState {
|
||
readonly input: DbInput | undefined
|
||
readonly effectiveInput: DbInput | undefined
|
||
readonly isDatatableInput: boolean
|
||
selectedDatatable: string | undefined
|
||
selectedSchema: string | undefined
|
||
selectedTable: string | undefined
|
||
readonly open: boolean
|
||
/** Workspace the drawer's DB operations run against — the acting workspace of
|
||
* the editor that opened it, else the navigation workspace. */
|
||
workspace: string | undefined
|
||
openDrawer: (nInput: DbInput, workspace?: string) => void
|
||
closeDrawer: () => void
|
||
}
|
||
|
||
export function useDbManagerUriState(): DbManagerUriState {
|
||
const params = useSearchParams(dbManagerSchema)
|
||
|
||
const parsed = $derived(parseDbm(params.dbm))
|
||
|
||
let input: DbInput | undefined = $derived.by(() => {
|
||
if (!parsed) return undefined
|
||
if (parsed.type === 'ducklake') {
|
||
return {
|
||
type: 'ducklake' as const,
|
||
ducklake: parsed.path,
|
||
specificSchema: parsed.schema,
|
||
specificTable: parsed.table
|
||
}
|
||
}
|
||
// datatable or database
|
||
const resType = parsed.type === 'datatable' ? 'postgresql' : parsed.resType
|
||
if (!isDbType(resType ?? undefined)) return undefined
|
||
return {
|
||
type: 'database' as const,
|
||
resourceType: resType as DbType,
|
||
resourcePath: parsed.type === 'datatable' ? `datatable://${parsed.path}` : parsed.path,
|
||
specificSchema: parsed.schema,
|
||
specificTable: parsed.table
|
||
}
|
||
})
|
||
|
||
const isDatatableInput = $derived(parsed?.type === 'datatable')
|
||
|
||
function updateField(updates: Partial<ParsedDbm>) {
|
||
const p = parseDbm(params.dbm)
|
||
if (!p) return
|
||
Object.assign(p, updates)
|
||
params.dbm = buildDbm(p)
|
||
}
|
||
|
||
// Not URL-persisted: the drawer defaults back to the nav workspace on reload,
|
||
// which is the correct fallback outside the session that opened it.
|
||
let workspace = $state<string | undefined>(undefined)
|
||
|
||
function openDrawer(nInput: DbInput, ws?: string) {
|
||
workspace = ws
|
||
if (nInput.type === 'database') {
|
||
const isDatatable = nInput.resourcePath.startsWith('datatable://')
|
||
params.dbm = buildDbm({
|
||
type: isDatatable ? 'datatable' : 'database',
|
||
path: isDatatable ? nInput.resourcePath.slice('datatable://'.length) : nInput.resourcePath,
|
||
resType: isDatatable ? undefined : nInput.resourceType,
|
||
schema: nInput.specificSchema,
|
||
table: nInput.specificTable
|
||
})
|
||
} else {
|
||
params.dbm = buildDbm({
|
||
type: 'ducklake',
|
||
path: nInput.ducklake,
|
||
schema: nInput.specificSchema,
|
||
table: nInput.specificTable
|
||
})
|
||
}
|
||
}
|
||
|
||
function closeDrawer() {
|
||
params.dbm = null
|
||
}
|
||
|
||
return {
|
||
get input() {
|
||
return input
|
||
},
|
||
get effectiveInput() {
|
||
return input
|
||
},
|
||
get isDatatableInput() {
|
||
return isDatatableInput
|
||
},
|
||
get selectedDatatable() {
|
||
return parsed?.type === 'datatable' ? parsed.path : undefined
|
||
},
|
||
set selectedDatatable(v: string | undefined) {
|
||
if (v) updateField({ path: v })
|
||
},
|
||
get selectedSchema() {
|
||
return parsed?.schema
|
||
},
|
||
set selectedSchema(v: string | undefined) {
|
||
updateField({ schema: v })
|
||
},
|
||
get selectedTable() {
|
||
return parsed?.table
|
||
},
|
||
set selectedTable(v: string | undefined) {
|
||
updateField({ table: v })
|
||
},
|
||
get open() {
|
||
return !!input
|
||
},
|
||
get workspace() {
|
||
return workspace
|
||
},
|
||
set workspace(v: string | undefined) {
|
||
workspace = v
|
||
},
|
||
openDrawer,
|
||
closeDrawer
|
||
}
|
||
}
|