mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
feat(datatables): navigate the database manager with a data table tree
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
Loader2,
|
||||
Plus,
|
||||
Table2,
|
||||
Database as DatabaseIcon,
|
||||
Folder as FolderIcon,
|
||||
Trash2Icon,
|
||||
UploadIcon
|
||||
} from 'lucide-svelte'
|
||||
@@ -21,18 +23,16 @@
|
||||
import DbTableEditor from './DBTableEditor.svelte'
|
||||
import type { DbType } from './dbTypes'
|
||||
import Portal from './Portal.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
import {
|
||||
dbSupportsTransactionalDdl,
|
||||
diffTableEditorValues
|
||||
} from './apps/components/display/dbtable/queries/alterTable'
|
||||
import { resource } from 'runed'
|
||||
import type { Snippet } from 'svelte'
|
||||
import { capitalize, onlyAlphaNumAndUnderscore, pluralize } from '$lib/utils'
|
||||
import type { DbFeatures } from './apps/components/display/dbtable/dbFeatures'
|
||||
import Star from './Star.svelte'
|
||||
import type { Asset } from '$lib/gen'
|
||||
import type { Asset, DataTableTables } from '$lib/gen'
|
||||
|
||||
/** Represents a selected table with its schema */
|
||||
export interface SelectedTable {
|
||||
@@ -53,7 +53,15 @@
|
||||
initialTableKey?: string
|
||||
selectedSchemaKey?: string | undefined
|
||||
selectedTableKey?: string | undefined
|
||||
/** Every data table with its schemas and tables. Present only when the manager
|
||||
* is on a data table — that is what puts a data-table level at the top of the
|
||||
* tree; otherwise the tree starts at schemas. */
|
||||
/** Multi-select pickers still choose their data table with a Select above the
|
||||
* list; the tree below is the navigator for the manager's normal mode. */
|
||||
dbSelector?: Snippet<[]>
|
||||
datatableTree?: DataTableTables[]
|
||||
datatableTreeLoading?: boolean
|
||||
onSelectDatatable?: (datatable: string) => void
|
||||
/** Enable multi-select mode with checkboxes in sidebar */
|
||||
multiSelectMode?: boolean
|
||||
/** Selected tables in multi-select mode */
|
||||
@@ -78,6 +86,9 @@
|
||||
selectedSchemaKey = $bindable(undefined),
|
||||
selectedTableKey = $bindable(undefined),
|
||||
dbSelector,
|
||||
datatableTree,
|
||||
datatableTreeLoading,
|
||||
onSelectDatatable,
|
||||
multiSelectMode = false,
|
||||
selectedTables = $bindable([]),
|
||||
disabledTables = [],
|
||||
@@ -152,6 +163,87 @@
|
||||
}
|
||||
|
||||
let schemaKeys = $derived(Object.keys(dbSchema.schema ?? {}))
|
||||
|
||||
// --- Left-pane tree ---------------------------------------------------------
|
||||
// Levels: data table -> schema -> table. The top two collapse away on their own
|
||||
// terms: no `datatableTree` means this is not a data table, and a database
|
||||
// without schemas has nothing to put between a data table and its tables.
|
||||
const currentDatatable = $derived(
|
||||
asset?.kind === 'datatable' ? asset.path : undefined
|
||||
)
|
||||
|
||||
/** Tables per schema for a data table, as `schema -> table[]`. */
|
||||
function schemasOf(datatable: string | undefined): Record<string, string[]> {
|
||||
// The open data table reads from `dbSchema`, which is refetched after a DDL;
|
||||
// the tree snapshot is not, so using it here would hide a table until the
|
||||
// next full reload.
|
||||
if (datatable === undefined || datatable === currentDatatable) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(dbSchema.schema ?? {}).map(([sk, tables]) => [sk, Object.keys(tables ?? {})])
|
||||
)
|
||||
}
|
||||
return datatableTree?.find((d) => d.datatable_name === datatable)?.schemas ?? {}
|
||||
}
|
||||
|
||||
function errorOf(datatable: string): string | undefined {
|
||||
return datatableTree?.find((d) => d.datatable_name === datatable)?.error
|
||||
}
|
||||
|
||||
const matchesSearch = (t: string) => t.toLowerCase().includes(search.trim().toLowerCase())
|
||||
|
||||
/** The tree as rendered: only nodes with a matching descendant survive a search. */
|
||||
let treeRoots = $derived.by(() => {
|
||||
const datatables = datatableTree
|
||||
? datatableTree.map((d) => d.datatable_name)
|
||||
: [undefined as string | undefined]
|
||||
return datatables.map((dt) => {
|
||||
const schemas = Object.entries(schemasOf(dt))
|
||||
.map(([schemaKey, tables]) => ({
|
||||
schemaKey,
|
||||
tables: tables.filter(matchesSearch).sort()
|
||||
}))
|
||||
.filter((sc) => search.trim() === '' || sc.tables.length > 0)
|
||||
schemas.sort((a, b) => a.schemaKey.localeCompare(b.schemaKey))
|
||||
return { datatable: dt, schemas, error: dt ? errorOf(dt) : undefined }
|
||||
}).filter(
|
||||
// A search narrows the tree to what matched; a data table with no match
|
||||
// left in it would otherwise sit there as an empty row.
|
||||
(root) => search.trim() === '' || root.schemas.length > 0
|
||||
)
|
||||
})
|
||||
|
||||
let expanded = $state<Set<string>>(new Set())
|
||||
const nodeKey = (dt: string | undefined, schemaKey?: string) =>
|
||||
`${dt ?? ''}${schemaKey === undefined ? '' : `/${schemaKey}`}`
|
||||
|
||||
function toggle(key: string) {
|
||||
const next = new Set(expanded)
|
||||
next.has(key) ? next.delete(key) : next.add(key)
|
||||
expanded = next
|
||||
}
|
||||
|
||||
// A node is open when explicitly expanded, when it holds the current selection,
|
||||
// or when a search is narrowing the tree to what matched.
|
||||
function isExpanded(dt: string | undefined, schemaKey?: string): boolean {
|
||||
if (search.trim() !== '') return true
|
||||
if (expanded.has(nodeKey(dt, schemaKey))) return true
|
||||
if (dt !== undefined && dt !== currentDatatable) return false
|
||||
return schemaKey === undefined || schemaKey === selected.schemaKey
|
||||
}
|
||||
|
||||
function selectTable(dt: string | undefined, schemaKey: string, tableKey: string) {
|
||||
if (dt !== undefined && dt !== currentDatatable) {
|
||||
// Switching data table re-mounts this component against the new one, so
|
||||
// the target has to travel through the bound keys the parent keeps —
|
||||
// local state here is about to be thrown away.
|
||||
selectedSchemaKey = schemaKey
|
||||
selectedTableKey = tableKey
|
||||
onSelectDatatable?.(dt)
|
||||
return
|
||||
}
|
||||
selected = { schemaKey, tableKey }
|
||||
}
|
||||
|
||||
let search = $state('')
|
||||
let selected: {
|
||||
schemaKey?: undefined | string
|
||||
@@ -259,41 +351,9 @@
|
||||
<Splitpanes>
|
||||
<Pane size={24} class="relative flex flex-col">
|
||||
<div class="mx-3 mt-3 flex flex-col gap-2">
|
||||
{#if dbSelector}
|
||||
{#if multiSelectMode && dbSelector}
|
||||
{@render dbSelector()}
|
||||
{/if}
|
||||
{#if dbSupportsSchemas && !multiSelectMode}
|
||||
<Select
|
||||
bind:value={selected.schemaKey}
|
||||
items={safeSelectItems(schemaKeys)}
|
||||
id="db-schema-select"
|
||||
transformInputSelectedText={(s) => `Schema: ${s}`}
|
||||
RightIcon={ChevronDownIcon}
|
||||
placeholder="Search or create schema..."
|
||||
showPlaceholderOnOpen
|
||||
onCreateItem={(schema) => {
|
||||
schema = schema.trim().replace(/[^a-zA-Z0-9_]/g, '')
|
||||
if (dbType === 'snowflake') schema = schema.toUpperCase()
|
||||
askingForConfirmation = {
|
||||
confirmationText: `Create ${schema}`,
|
||||
type: 'reload',
|
||||
title: `This will run 'CREATE SCHEMA ${schema}' on your database. Are you sure ?`,
|
||||
open: true,
|
||||
id: 'db-create-schema-confirmation-modal',
|
||||
onConfirm: async () => {
|
||||
askingForConfirmation && (askingForConfirmation.loading = true)
|
||||
try {
|
||||
await dbSchemaOps.onCreateSchema({ schema })
|
||||
refresh?.()
|
||||
selected.schemaKey = schema
|
||||
} finally {
|
||||
askingForConfirmation = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<ClearableInput bind:value={search} placeholder="Search table..." />
|
||||
</div>
|
||||
<div class="overflow-x-clip overflow-y-auto relative mt-3 border-y flex-1">
|
||||
@@ -462,69 +522,157 @@
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<!-- Normal mode: show tables for selected schema -->
|
||||
{#each filteredTableKeys as tableKey}
|
||||
<!-- PLACEHOLDER -->
|
||||
<button
|
||||
class={'w-full text-sm font-normal flex gap-2 items-center h-10 cursor-pointer pl-3 pr-1 ' +
|
||||
(selected.tableKey === tableKey ? 'bg-surface-secondary' : 'hover:bg-surface-hover')}
|
||||
onclick={() => (selected.tableKey = tableKey)}
|
||||
>
|
||||
{#if asset}
|
||||
<Star
|
||||
kind="asset"
|
||||
path={`${asset.kind}://${asset.path == 'main' ? '' : asset.path}/${selected.schemaKey}.${tableKey}`}
|
||||
/>
|
||||
{:else}
|
||||
<Table2 class="text-primary shrink-0" size={14} />
|
||||
{/if}
|
||||
|
||||
<p
|
||||
class="db-manager-table-key truncate text-ellipsis grow text-left text-emphasis text-xs"
|
||||
<!-- Normal mode: data table -> schema -> table, each level dropping out
|
||||
when it has nothing to say (no data table / no schemas). -->
|
||||
{#if datatableTreeLoading && (datatableTree?.length ?? 0) === 0}
|
||||
<div class="flex items-center gap-2 text-tertiary p-3">
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
<span class="text-xs">Loading...</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each treeRoots as root (root.datatable ?? '')}
|
||||
{@const dtOpen = isExpanded(root.datatable)}
|
||||
{#if root.datatable !== undefined}
|
||||
<button
|
||||
class={'w-full text-sm font-medium flex gap-2 items-center h-9 cursor-pointer pl-2 pr-1 hover:bg-gray-500/10 border-b border-surface-secondary ' +
|
||||
(root.datatable === currentDatatable ? 'text-emphasis' : 'text-secondary')}
|
||||
onclick={() => toggle(nodeKey(root.datatable))}
|
||||
>
|
||||
{tableKey}
|
||||
</p>
|
||||
<DropdownV2
|
||||
items={() => [
|
||||
{
|
||||
displayName: 'Delete table',
|
||||
icon: Trash2Icon,
|
||||
action: () =>
|
||||
(askingForConfirmation = {
|
||||
title: `Are you sure you want to delete ${tableKey} ? This action is irreversible`,
|
||||
confirmationText: 'Delete permanently',
|
||||
open: true,
|
||||
id: 'db-manager-delete-table-confirmation-modal',
|
||||
onConfirm: async () => {
|
||||
askingForConfirmation && (askingForConfirmation.loading = true)
|
||||
try {
|
||||
await dbSchemaOps.onDelete({ tableKey, schema: selected.schemaKey })
|
||||
refresh?.()
|
||||
sendUserToast(`Table '${tableKey}' deleted successfully`)
|
||||
} catch (e) {
|
||||
let msg: string | undefined = (e as any).body ?? (e as Error).message
|
||||
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : undefined
|
||||
sendUserToast(msg ?? 'Action failed!', true)
|
||||
}
|
||||
askingForConfirmation = undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
displayName: 'Alter table',
|
||||
icon: EditIcon,
|
||||
action: () => {
|
||||
dbTableEditorState = {
|
||||
open: true,
|
||||
alterTableKey: tableKey
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
class="w-fit"
|
||||
btnId={'db-manager-table-actions-' + onlyAlphaNumAndUnderscore(tableKey)}
|
||||
/>
|
||||
</button>
|
||||
<ChevronDownIcon
|
||||
class={'shrink-0 transition-transform ' + (dtOpen ? '' : '-rotate-90')}
|
||||
size={14}
|
||||
/>
|
||||
<DatabaseIcon class="shrink-0" size={14} />
|
||||
<span class="truncate text-ellipsis grow text-left text-xs">{root.datatable}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if dtOpen}
|
||||
{#if root.error}
|
||||
<p class="text-xs text-red-400 px-3 py-2">{root.error}</p>
|
||||
{/if}
|
||||
{#each root.schemas as sc (sc.schemaKey)}
|
||||
{@const schemaOpen = isExpanded(root.datatable, sc.schemaKey)}
|
||||
{@const indent = root.datatable !== undefined ? 'pl-6' : 'pl-2'}
|
||||
{#if dbSupportsSchemas}
|
||||
<button
|
||||
class={'w-full text-sm font-medium flex gap-2 items-center h-9 cursor-pointer pr-1 hover:bg-gray-500/10 border-b border-surface-secondary ' +
|
||||
indent}
|
||||
onclick={() => toggle(nodeKey(root.datatable, sc.schemaKey))}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
class={'shrink-0 transition-transform ' + (schemaOpen ? '' : '-rotate-90')}
|
||||
size={14}
|
||||
/>
|
||||
<FolderIcon class="shrink-0 text-tertiary" size={14} />
|
||||
<span class="truncate text-ellipsis grow text-left text-xs">{sc.schemaKey}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if schemaOpen || !dbSupportsSchemas}
|
||||
{@const tableIndent = dbSupportsSchemas
|
||||
? root.datatable !== undefined
|
||||
? 'pl-12'
|
||||
: 'pl-8'
|
||||
: root.datatable !== undefined
|
||||
? 'pl-8'
|
||||
: 'pl-3'}
|
||||
{#each sc.tables as tableKey (tableKey)}
|
||||
{@const isSelected =
|
||||
root.datatable === currentDatatable &&
|
||||
selected.schemaKey === sc.schemaKey &&
|
||||
selected.tableKey === tableKey}
|
||||
<button
|
||||
class={'w-full text-sm font-normal flex gap-2 items-center h-10 cursor-pointer pr-1 ' +
|
||||
tableIndent +
|
||||
' ' +
|
||||
(isSelected ? 'bg-surface-secondary' : 'hover:bg-surface-hover')}
|
||||
onclick={() => selectTable(root.datatable, sc.schemaKey, tableKey)}
|
||||
>
|
||||
{#if asset}
|
||||
<Star
|
||||
kind="asset"
|
||||
path={`${asset.kind}://${asset.path == 'main' ? '' : asset.path}/${sc.schemaKey}.${tableKey}`}
|
||||
/>
|
||||
{:else}
|
||||
<Table2 class="text-primary shrink-0" size={14} />
|
||||
{/if}
|
||||
<p
|
||||
class="db-manager-table-key truncate text-ellipsis grow text-left text-emphasis text-xs"
|
||||
>
|
||||
{tableKey}
|
||||
</p>
|
||||
{#if root.datatable === currentDatatable || root.datatable === undefined}
|
||||
<DropdownV2
|
||||
items={() => [
|
||||
{
|
||||
displayName: 'Delete table',
|
||||
icon: Trash2Icon,
|
||||
action: () =>
|
||||
(askingForConfirmation = {
|
||||
title: `Are you sure you want to delete ${tableKey} ? This action is irreversible`,
|
||||
confirmationText: 'Delete permanently',
|
||||
open: true,
|
||||
id: 'db-manager-delete-table-confirmation-modal',
|
||||
onConfirm: async () => {
|
||||
askingForConfirmation && (askingForConfirmation.loading = true)
|
||||
try {
|
||||
await dbSchemaOps.onDelete({
|
||||
tableKey,
|
||||
schema: sc.schemaKey
|
||||
})
|
||||
refresh?.()
|
||||
sendUserToast(`Table '${tableKey}' deleted successfully`)
|
||||
} catch (e) {
|
||||
let msg: string | undefined =
|
||||
(e as any).body ?? (e as Error).message
|
||||
if (typeof msg !== 'string')
|
||||
msg = e ? JSON.stringify(e) : undefined
|
||||
sendUserToast(msg ?? 'Action failed!', true)
|
||||
}
|
||||
askingForConfirmation = undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
displayName: 'Alter table',
|
||||
icon: EditIcon,
|
||||
action: () => {
|
||||
selected = { schemaKey: sc.schemaKey, tableKey }
|
||||
dbTableEditorState = { open: true, alterTableKey: tableKey }
|
||||
}
|
||||
}
|
||||
]}
|
||||
class="w-fit"
|
||||
btnId={'db-manager-table-actions-' + onlyAlphaNumAndUnderscore(tableKey)}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{#if root.datatable === currentDatatable || root.datatable === undefined}
|
||||
<button
|
||||
class={'w-full text-sm font-normal flex gap-2 items-center h-8 cursor-pointer pr-1 hover:bg-gray-500/10 text-tertiary ' +
|
||||
tableIndent}
|
||||
onclick={() => {
|
||||
selected = { schemaKey: sc.schemaKey, tableKey: undefined }
|
||||
dbTableEditorState = { open: true }
|
||||
}}
|
||||
>
|
||||
<Plus class="shrink-0" size={14} />
|
||||
<span class="text-xs">New table</span>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{#if dbSupportsSchemas && (root.datatable === currentDatatable || root.datatable === undefined) && search.trim() === ''}
|
||||
<button
|
||||
class={'w-full text-sm font-normal flex gap-2 items-center h-8 cursor-pointer pr-1 hover:bg-gray-500/10 text-tertiary ' +
|
||||
(root.datatable !== undefined ? 'pl-6' : 'pl-2')}
|
||||
onclick={() => (newSchemaDialogOpen = true)}
|
||||
>
|
||||
<Plus class="shrink-0" size={14} />
|
||||
<span class="text-xs">New schema</span>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { dbSchemas, workspaceStore, type DBSchema } from '$lib/stores'
|
||||
import type { Snippet } from 'svelte'
|
||||
import type { DataTableTables } from '$lib/gen'
|
||||
import { sortArray } from '$lib/utils'
|
||||
import { Loader2, RefreshCcw } from 'lucide-svelte'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
@@ -18,7 +20,6 @@
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import SqlRepl from './SqlRepl.svelte'
|
||||
import SimpleAgTable from './SimpleAgTable.svelte'
|
||||
import { type Snippet } from 'svelte'
|
||||
import type { DbInput } from './dbTypes'
|
||||
import { getDbSchemas, loadAllTablesMetaData } from './apps/components/display/dbtable/metadata'
|
||||
|
||||
@@ -36,7 +37,13 @@
|
||||
hasReplResult?: boolean
|
||||
selectedSchemaKey?: string | undefined
|
||||
selectedTableKey?: string | undefined
|
||||
/** Every data table with its schemas and tables, for the left-pane tree.
|
||||
* Undefined when the drawer is not on a data table, which is what collapses
|
||||
* the tree's top level away. */
|
||||
dbSelector?: Snippet<[]>
|
||||
datatableTree?: DataTableTables[]
|
||||
datatableTreeLoading?: boolean
|
||||
onSelectDatatable?: (datatable: string) => void
|
||||
/** Enable multi-select mode with checkboxes in sidebar */
|
||||
multiSelectMode?: boolean
|
||||
/** Selected tables in multi-select mode */
|
||||
@@ -60,6 +67,9 @@
|
||||
selectedSchemaKey = $bindable(undefined),
|
||||
selectedTableKey = $bindable(undefined),
|
||||
dbSelector,
|
||||
datatableTree,
|
||||
datatableTreeLoading,
|
||||
onSelectDatatable,
|
||||
multiSelectMode = false,
|
||||
selectedTables = $bindable([]),
|
||||
disabledTables = [],
|
||||
@@ -309,6 +319,9 @@
|
||||
{dbType}
|
||||
refresh={() => refresh()}
|
||||
{dbSelector}
|
||||
{datatableTree}
|
||||
{datatableTreeLoading}
|
||||
{onSelectDatatable}
|
||||
{onImport}
|
||||
bind:selectedSchemaKey
|
||||
bind:selectedTableKey
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { WorkspaceService, type DataTableTables } from '$lib/gen'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
@@ -10,7 +10,6 @@
|
||||
Copy,
|
||||
Download,
|
||||
Expand,
|
||||
LoaderCircle,
|
||||
Minimize,
|
||||
RefreshCcw,
|
||||
Upload
|
||||
@@ -42,24 +41,19 @@
|
||||
// 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 () => {
|
||||
// Every data table with its schemas and tables, in one call: this is what the
|
||||
// left pane's tree navigates, so it has to cover the data tables the user is
|
||||
// not currently on, not just the selected one.
|
||||
const datatables = resource<DataTableTables[]>([], async () => {
|
||||
if (!ws) return []
|
||||
try {
|
||||
return (await WorkspaceService.listDataTables({ workspace: ws })).map((d) => d.name)
|
||||
return await WorkspaceService.listDataTableTables({ workspace: ws })
|
||||
} catch (e) {
|
||||
console.error('Failed to load datatables:', e)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const datatableItems = $derived(
|
||||
datatables.current.map((dt) => ({
|
||||
value: dt,
|
||||
label: dt
|
||||
}))
|
||||
)
|
||||
|
||||
// Roles the *caller* may use, so the picker never offers one that would be
|
||||
// refused. Absent/disabled permissions yield no roles and hide the picker.
|
||||
const usableRoles = resource(
|
||||
@@ -67,7 +61,10 @@
|
||||
async ([workspace, datatable]) => {
|
||||
if (!workspace || !datatable) return undefined
|
||||
try {
|
||||
return await WorkspaceService.listUsableDatatableRoles({ workspace, datatableName: datatable })
|
||||
return await WorkspaceService.listUsableDatatableRoles({
|
||||
workspace,
|
||||
datatableName: datatable
|
||||
})
|
||||
} catch (e) {
|
||||
// Never leave the drawer waiting on this: fall back to the
|
||||
// unpermissioned shape so it opens and the server picks the role.
|
||||
@@ -88,12 +85,6 @@
|
||||
uriState.selectedRole !== undefined))
|
||||
)
|
||||
|
||||
const roleItems = $derived(
|
||||
(usableRoles.current?.roles ?? []).map((r) => ({
|
||||
value: r,
|
||||
label: r === usableRoles.current?.default_role ? `${r} (default)` : r
|
||||
}))
|
||||
)
|
||||
|
||||
// Settle the role before anything queries the data table: the schema and
|
||||
// metadata fetches run as whatever role the input carries, so leaving it unset
|
||||
@@ -102,9 +93,7 @@
|
||||
$effect(() => {
|
||||
const roles = usableRoles.current
|
||||
if (!roles?.enabled || uriState.selectedRole !== undefined) return
|
||||
const effective = roles.roles.includes(roles.default_role)
|
||||
? roles.default_role
|
||||
: roles.roles[0]
|
||||
const effective = roles.roles.includes(roles.default_role) ? roles.default_role : roles.roles[0]
|
||||
if (effective) untrack(() => (uriState.selectedRole = effective))
|
||||
})
|
||||
|
||||
@@ -238,6 +227,9 @@
|
||||
bind:this={dbManagerContent}
|
||||
input={uriState.effectiveInput}
|
||||
workspace={uriState.workspace}
|
||||
datatableTree={uriState.isDatatableInput ? datatables.current : undefined}
|
||||
datatableTreeLoading={datatables.loading}
|
||||
onSelectDatatable={(dt) => (uriState.selectedDatatable = dt)}
|
||||
bind:workerTag={() => workerTag.tag, (v) => (workerTag.tag = v)}
|
||||
bind:hasReplResult
|
||||
bind:selectedSchemaKey={uriState.selectedSchema}
|
||||
@@ -246,35 +238,6 @@
|
||||
? (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}
|
||||
<!-- A single usable role is not a choice: the picker would only restate
|
||||
what the connection already is. -->
|
||||
{#if usableRoles.current?.enabled && roleItems.length > 1}
|
||||
<Select
|
||||
transformInputSelectedText={(s) => `Role: ${s}`}
|
||||
items={roleItems}
|
||||
bind:value={uriState.selectedRole}
|
||||
placeholder="Role"
|
||||
size="md"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DBManagerContent>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user