mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
This reverts 3881e4d8ea. Making default and subtle destructive buttons red at rest changes
every existing caller of the prop -- the workspace integrations, AI skills, workspace
creation and the instance database drop -- so it is a design-system change, and the call
sites it fixed are the migrations list and the database manager. None of that is the setup
wizard.
Nothing on this branch passes destructive any more, so it leaves with no loose ends.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
334 lines
9.4 KiB
Svelte
334 lines
9.4 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'
|
|
import { useDbManagerTag } from './dbManagerTag.svelte'
|
|
import DbWorkerTagButton from './DbWorkerTagButton.svelte'
|
|
|
|
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()
|
|
|
|
// Per-database worker tag override, remembered across drawer opens.
|
|
const workerTag = useDbManagerTag(
|
|
() => ws,
|
|
() => uriState.effectiveInput
|
|
)
|
|
|
|
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:workerTag={() => workerTag.tag, (v) => (workerTag.tag = v)}
|
|
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}
|
|
{#if uriState.effectiveInput && ws}
|
|
<DbWorkerTagButton
|
|
bind:tag={() => workerTag.tag, (v) => (workerTag.tag = v)}
|
|
input={uriState.effectiveInput}
|
|
workspace={ws}
|
|
/>
|
|
{/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>
|