mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +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>
317 lines
8.9 KiB
Svelte
317 lines
8.9 KiB
Svelte
<script lang="ts">
|
|
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
|
import { WorkspaceService } from '$lib/gen'
|
|
import Button from './common/button/Button.svelte'
|
|
import Drawer from './common/drawer/Drawer.svelte'
|
|
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
|
import Select from './select/Select.svelte'
|
|
import {
|
|
ArrowLeft,
|
|
Copy,
|
|
Download,
|
|
Expand,
|
|
LoaderCircle,
|
|
Minimize,
|
|
RefreshCcw,
|
|
Upload
|
|
} from 'lucide-svelte'
|
|
import DBManagerContent from './DBManagerContent.svelte'
|
|
import DataTableMigrationsButton from './workspaceSettings/DataTableMigrationsButton.svelte'
|
|
import { resource } from 'runed'
|
|
import { untrack } from 'svelte'
|
|
import type { DbManagerUriState } from './dbManagerDrawerModel.svelte'
|
|
import ResourcePicker from './ResourcePicker.svelte'
|
|
import Alert from './common/alert/Alert.svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { isCloudHosted } from '$lib/cloud'
|
|
|
|
interface Props {
|
|
uriState: DbManagerUriState
|
|
/** Z-index offset for the drawer, useful when opening from within modals */
|
|
offset?: number
|
|
}
|
|
|
|
let { uriState, offset = 0 }: Props = $props()
|
|
|
|
let open = $derived(uriState.open)
|
|
|
|
// The workspace the drawer's DB operations run against — the acting workspace of
|
|
// the editor that opened it (set via openDrawer), else the nav workspace.
|
|
let ws = $derived(uriState.workspace ?? $workspaceStore)
|
|
|
|
// Load available datatables when drawer opens with datatable input
|
|
const datatables = resource<string[]>([], async () => {
|
|
if (!ws) return []
|
|
try {
|
|
return (await WorkspaceService.listDataTables({ workspace: ws })).map((d) => d.name)
|
|
} catch (e) {
|
|
console.error('Failed to load datatables:', e)
|
|
return []
|
|
}
|
|
})
|
|
|
|
const datatableItems = $derived(
|
|
datatables.current.map((dt) => ({
|
|
value: dt,
|
|
label: dt
|
|
}))
|
|
)
|
|
|
|
// Refetch datatables when switching to a datatable input
|
|
$effect(() => {
|
|
if (uriState.isDatatableInput) {
|
|
untrack(() => datatables.refetch())
|
|
}
|
|
})
|
|
|
|
function handleClose() {
|
|
uriState.closeDrawer()
|
|
dbManagerContent?.clearReplResult()
|
|
}
|
|
|
|
let windowWidth = $state(window.innerWidth)
|
|
let expand = $state(false)
|
|
|
|
$effect(() => {
|
|
if (!open) {
|
|
expand = false
|
|
uriState.closeDrawer()
|
|
}
|
|
})
|
|
|
|
let dbManagerContent: DBManagerContent | undefined = $state()
|
|
|
|
let hasReplResult = $state(false)
|
|
|
|
// Export/Import state
|
|
let exportDrawerOpen = $state(false)
|
|
let exportResult = $state('')
|
|
let importDrawerOpen = $state(false)
|
|
let importLoading = $state(false)
|
|
let importSource = $state<string | undefined>(undefined)
|
|
let importBehavior = $state<'schema_only' | 'schema_and_data'>('schema_only')
|
|
|
|
let isPostgresqlInput = $derived(
|
|
uriState.isDatatableInput ||
|
|
(uriState.input?.type === 'database' && uriState.input.resourceType === 'postgresql')
|
|
)
|
|
let enableImportExport = $derived(isPostgresqlInput)
|
|
|
|
function toSourceIdentifier(raw: string): string {
|
|
if (raw.startsWith('datatable://') || raw.startsWith('$res:')) return raw
|
|
return `$res:${raw}`
|
|
}
|
|
|
|
function currentSourceIdentifier(): string | undefined {
|
|
const input = uriState.effectiveInput
|
|
if (!input || input.type !== 'database') return undefined
|
|
return toSourceIdentifier(input.resourcePath)
|
|
}
|
|
|
|
function refreshManager() {
|
|
dbManagerContent?.refresh()
|
|
dbManagerContent?.dbManager()?.dbTable()?.refresh()
|
|
}
|
|
|
|
async function handleExportSchema() {
|
|
const source = currentSourceIdentifier()
|
|
if (!source || !ws) return
|
|
try {
|
|
exportResult = await WorkspaceService.exportPgSchema({
|
|
workspace: ws,
|
|
requestBody: { source }
|
|
})
|
|
exportDrawerOpen = true
|
|
} catch (e) {
|
|
sendUserToast(`Failed to export schema: ${e}`, true)
|
|
}
|
|
}
|
|
|
|
async function handleImportDatabase() {
|
|
if (!importSource || !ws) return
|
|
const target = currentSourceIdentifier()
|
|
if (!target) return
|
|
importLoading = true
|
|
try {
|
|
await WorkspaceService.importPgDatabase({
|
|
workspace: ws,
|
|
requestBody: {
|
|
source: toSourceIdentifier(importSource),
|
|
target,
|
|
fork_behavior: importBehavior
|
|
}
|
|
})
|
|
sendUserToast('Database import completed successfully')
|
|
importDrawerOpen = false
|
|
importSource = undefined
|
|
dbManagerContent?.refresh()
|
|
} catch (e) {
|
|
sendUserToast(`Failed to import database: ${e}`, true)
|
|
} finally {
|
|
importLoading = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window bind:innerWidth={windowWidth} />
|
|
|
|
<Drawer
|
|
bind:open
|
|
size={expand ? `${windowWidth}px` : '1200px'}
|
|
preventEscape
|
|
{offset}
|
|
on:close={handleClose}
|
|
>
|
|
<DrawerContent
|
|
title={hasReplResult ? 'Query Result' : 'Database Manager'}
|
|
on:close={() => {
|
|
if (hasReplResult) {
|
|
dbManagerContent?.clearReplResult()
|
|
} else {
|
|
handleClose()
|
|
}
|
|
}}
|
|
CloseIcon={hasReplResult ? ArrowLeft : undefined}
|
|
noPadding
|
|
id="db-manager-drawer"
|
|
>
|
|
{#if uriState.effectiveInput && ws}
|
|
{#key uriState.selectedDatatable}
|
|
<DBManagerContent
|
|
bind:this={dbManagerContent}
|
|
input={uriState.effectiveInput}
|
|
workspace={uriState.workspace}
|
|
bind:hasReplResult
|
|
bind:selectedSchemaKey={uriState.selectedSchema}
|
|
bind:selectedTableKey={uriState.selectedTable}
|
|
onImport={enableImportExport
|
|
? (mode) => ((importDrawerOpen = true), (importBehavior = mode))
|
|
: undefined}
|
|
>
|
|
{#snippet dbSelector()}
|
|
{#if uriState.isDatatableInput}
|
|
{#if datatables.loading}
|
|
<div class="flex items-center gap-2 text-tertiary ml-2">
|
|
<LoaderCircle size={14} class="animate-spin" />
|
|
<span class="text-sm">Loading...</span>
|
|
</div>
|
|
{:else if datatables.current.length >= 1}
|
|
<Select
|
|
transformInputSelectedText={(s) => `Datatable: ${s}`}
|
|
items={datatableItems}
|
|
bind:value={uriState.selectedDatatable}
|
|
placeholder="Select data table"
|
|
size="md"
|
|
/>
|
|
{/if}
|
|
{/if}
|
|
{/snippet}
|
|
</DBManagerContent>
|
|
{/key}
|
|
{/if}
|
|
{#snippet actions()}
|
|
{#if uriState.isDatatableInput && uriState.selectedDatatable && ws}
|
|
<DataTableMigrationsButton
|
|
workspace={ws}
|
|
datatable={uriState.selectedDatatable}
|
|
onSchemaChanged={refreshManager}
|
|
/>
|
|
{/if}
|
|
{#if enableImportExport}
|
|
<Button startIcon={{ icon: Download }} onClick={handleExportSchema}>Export</Button>
|
|
<Button startIcon={{ icon: Upload }} onClick={() => (importDrawerOpen = true)}>
|
|
Import
|
|
</Button>
|
|
{/if}
|
|
<Button
|
|
loading={dbManagerContent?.isLoading() ?? false}
|
|
on:click={refreshManager}
|
|
startIcon={{ icon: RefreshCcw }}
|
|
iconOnly
|
|
title="Refresh"
|
|
size="xs"
|
|
color="light"
|
|
/>
|
|
|
|
<Button
|
|
on:click={() => (expand = !expand)}
|
|
startIcon={{ icon: expand ? Minimize : Expand }}
|
|
size="xs"
|
|
color="light"
|
|
/>
|
|
{/snippet}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<Drawer bind:open={exportDrawerOpen} size="800px" offset={offset + 1}>
|
|
<DrawerContent title="Export Schemas" on:close={() => (exportDrawerOpen = false)}>
|
|
{#if exportResult}
|
|
<div class="flex flex-col gap-2 h-full relative">
|
|
<pre class="overflow-auto text-xs bg-surface-secondary p-4 rounded flex-1"
|
|
>{exportResult}</pre
|
|
>
|
|
<Button
|
|
size="xs"
|
|
color="light"
|
|
startIcon={{ icon: Copy }}
|
|
wrapperClasses="absolute top-2 right-2"
|
|
btnClasses="bg-surface-tertiary"
|
|
on:click={() => {
|
|
navigator.clipboard.writeText(exportResult)
|
|
sendUserToast('Copied to clipboard')
|
|
}}
|
|
>
|
|
Copy
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<Drawer bind:open={importDrawerOpen} size="600px" offset={offset + 1}>
|
|
<DrawerContent title="Import Database" on:close={() => (importDrawerOpen = false)}>
|
|
<div class="flex flex-col gap-4">
|
|
<Alert type="warning" title="Warning">
|
|
This will import the schemas from the selected source into the current database. Existing
|
|
tables with the same names may be affected.
|
|
</Alert>
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-sm font-medium">Source database</span>
|
|
<ResourcePicker
|
|
datatableAsPgResource
|
|
bind:value={importSource}
|
|
resourceType="postgresql"
|
|
workspace={ws}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-sm font-medium">Import mode</span>
|
|
<Select
|
|
items={[
|
|
{ value: 'schema_only', label: 'Schema only' },
|
|
...(isCloudHosted() || (!$superadmin && !$userStore?.is_admin)
|
|
? []
|
|
: [{ value: 'schema_and_data', label: 'Schema and data' }])
|
|
]}
|
|
bind:value={importBehavior}
|
|
/>
|
|
</div>
|
|
{#if importBehavior === 'schema_and_data'}
|
|
<Alert type="warning" title="Heavy operation">
|
|
Importing schema and data will copy all rows from every table in the source database. This
|
|
may take a long time and use significant storage space depending on the size of the
|
|
source.
|
|
</Alert>
|
|
{/if}
|
|
<Button
|
|
disabled={!importSource}
|
|
loading={importLoading}
|
|
color="red"
|
|
on:click={handleImportDatabase}
|
|
>
|
|
Import {importBehavior === 'schema_and_data' ? 'schemas and data' : 'schemas'} into current database
|
|
</Button>
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|