mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-06 16:02:19 +00:00
feat: data table schemas (#7353)
* data tables settings ui * install runed * zod 4 fixes * use new toJSONSchema * Migrate ducklake catalogs to more generic custom instance databases * fix compilation * Safety conversion for old duckdb ffi * data tables settings * ts client basis * inline run works * datatables work * Revert "datatables work" This reverts commit6e1588d59e. * datatables work (without leaking pg credentials) * println * separate sqlUtils.ts * nit * Separate custom instance db Select and Wizard components * nit * nit wording * add tags to custom instance dbs * error when trying to use ducklake as datatable or opposite * show status in dropdown * data table instance setup works * sqk function for ducklake * factorize logic * fix temp reactivity * Data table assetexplore * Migrate S3 permissions to modal * Revert "Migrate S3 permissions to modal" This reverts commit0631d03cb0. * nit query -> fetch * Custom instance setup new look * run_language_executor separate fn * run_inline param * nit wording * Better typed client * Data tables display as assets in frontend * asset db icon * nit * cleaner errors * nit * Fix sed calls in mac * run_inline_script_preview in python client * basic python datatable client * datatable and datalake parser in python * ducklake client python * nit fix * Fix migration producing NULL instead of {} when no custom databases * merge conflict fail * python ducklake client arg fix * parse or infer sql types in ts client * ts asset parser, detect datatable & ducklake R/W * fix sql repl for other read ops than select * export type SqlTemplateFunction * rename list_custom_instance_pg_databases * typecheck datatable and ducklake name in Typescript * Fix typecheck datatable and ducklake in TS * declare module overriding instead of extending * infer_sql_type in python client * SqlQuery object in python * fix merge conflicts * update const_format * CI fix * factor out to var_identifiers * sqlx prepare * unnecessary security (admin is required) * clearer comment * ee repo ref * nit snake case * claude step 1: detect var declarations * move detect_sql_access_type to common mod * claude step 2: detect when saved vars are queried * Revert "claude step 2: detect when saved vars are queried" This reverts commit1e1f930568. * Revert "claude step 1: detect var declarations" This reverts commitf866f4819d. * remove ducklake/datatable and default * detect data table assigns in var_identifiers * Python parser successfully infers R/W/RW from ducklake / datatable * still register ducklake/datatable if not used as unknown R/W * Go to settings button in Assets Dropdown on not found * nit * sqlx prepare fail * manual fix, somehow sqlx prepare won't do it * fix frontend ci * ee repo ref * ducklake_user doesnt exist in unit tests * nit fix * ui nit * nit * nit missing clone * fork ducklakes and datatables * fix surface hover bug * stupid mistake * better deeply reactive mutable derived * Ducklake picker * Editor bar data tables * DuckDB supports datatables * datatable in duckdb asset parser * duckdb asset parser var_identifiers * Revert "duckdb asset parser var_identifiers" This reverts commit88068b1a77. * sqlx prepare * Box pin in test_workflow_as_code to fix stack overflow * stash * sql asset parser parses most s3 literals * nit * Detect attach + handle returning RW * detect assets used with dot notation * detect implicit access with USE dl; syntax * Add assets as unknown if var was never used * Support default ducklake/datatable main in parser * ignore asset parsing errors in frontend (avoid flow layout shift) * super weird duplication (merge conflict ?) * nits * fix duckdb parser detecting too much as asset when RW ctx is unknown * fix transparent assets btn * missing arg * nit styling * asset parser specific table parsing * fix resource specific table parsing * More concise asset display in flows + better icons * fix assets page filtering out resources with added table * Fix frontend to support specific table assets * Open DB Manager to specific table * Specific table parser in Python and TS + unit tests * Fix UPDATE setting access to None * fix flow edge rendering on top of output picker * python parser fix var override bug * add ts test * fix compilation * sqlx prepare * update parsers version * fix missing schema key onDelete * Grant permission to create schemas in custom instance databases * Update pg query to return empty schemas * Create schema * Select nits * support schemas in sql parser * ts parser handle schema with sql parser result * detect .schema() syntax * detect schema syntax in python * support .schema() in ts and py SDK * open db manager to specific schema * support reassignment in ts parser * nit better unitest * : syntax in ts * datatable:schema syntax in python * fix client py * nit select dropdown darkmode * object | null fetchOne * ts client nits * parse_sql_client_name fn * getImportWmillTsStatement refactor in EditorBar * text to json() in python client * update parser versions * pkg lock
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { type DBSchema } from '$lib/stores'
|
||||
import { MoreVertical, Plus, Table2, Trash2Icon } from 'lucide-svelte'
|
||||
import { ChevronDownIcon, MoreVertical, Plus, Table2, Trash2Icon } from 'lucide-svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { ClearableInput, Drawer, DrawerContent } from './common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -13,6 +13,8 @@
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
dbType: DbType
|
||||
@@ -22,6 +24,7 @@
|
||||
dbTableOpsFactory: (params: { colDefs: ColumnDef[]; tableKey: string }) => IDbTableOps
|
||||
dbSchemaOps: IDbSchemaOps
|
||||
refresh?: () => void
|
||||
initialSchemaKey?: string
|
||||
initialTableKey?: string
|
||||
}
|
||||
let {
|
||||
@@ -32,6 +35,7 @@
|
||||
getColDefs,
|
||||
dbSupportsSchemas,
|
||||
refresh,
|
||||
initialSchemaKey,
|
||||
initialTableKey
|
||||
}: Props = $props()
|
||||
|
||||
@@ -45,7 +49,8 @@
|
||||
$effect(() => {
|
||||
if (!selected.schemaKey && schemaKeys.length) {
|
||||
let schemaKey =
|
||||
'public' in dbSchema.schema ? 'public' : 'dbo' in dbSchema.schema ? 'dbo' : schemaKeys[0]
|
||||
initialSchemaKey ??
|
||||
('public' in dbSchema.schema ? 'public' : 'dbo' in dbSchema.schema ? 'dbo' : schemaKeys[0])
|
||||
let tableKey =
|
||||
initialTableKey && dbSchema.schema?.[schemaKey]?.[initialTableKey]
|
||||
? initialTableKey
|
||||
@@ -90,20 +95,40 @@
|
||||
|
||||
<Splitpanes>
|
||||
<Pane size={24} class="relative flex flex-col">
|
||||
<div class="mx-3 mt-3">
|
||||
<div class="mx-3 mt-3 flex flex-col gap-2">
|
||||
{#if dbSupportsSchemas}
|
||||
<select
|
||||
value={selected.schemaKey}
|
||||
onchange={(e) => {
|
||||
selected = { schemaKey: e.currentTarget.value }
|
||||
<Select
|
||||
bind:value={selected.schemaKey}
|
||||
items={safeSelectItems(schemaKeys)}
|
||||
transformInputSelectedText={(s) => `Schema: ${s}`}
|
||||
RightIcon={ChevronDownIcon}
|
||||
placeholder="Search or create schema..."
|
||||
showPlaceholderOnOpen
|
||||
onCreateItem={(schema) => {
|
||||
schema = schema
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-zA-Z0-9_]/g, '')
|
||||
askingForConfirmation = {
|
||||
confirmationText: `Create ${schema}`,
|
||||
type: 'reload',
|
||||
title: `This will run 'CREATE SCHEMA ${schema}' on your database. Are you sure ?`,
|
||||
open: true,
|
||||
onConfirm: async () => {
|
||||
askingForConfirmation && (askingForConfirmation.loading = true)
|
||||
try {
|
||||
await dbSchemaOps.onCreateSchema({ schema })
|
||||
refresh?.()
|
||||
selected.schemaKey = schema
|
||||
} finally {
|
||||
askingForConfirmation = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each schemaKeys as schemaKey}
|
||||
<option value={schemaKey}>{schemaKey}</option>
|
||||
{/each}
|
||||
</select>
|
||||
/>
|
||||
{/if}
|
||||
<ClearableInput wrapperClass="mt-3" bind:value={search} placeholder="Search table..." />
|
||||
<ClearableInput bind:value={search} placeholder="Search table..." />
|
||||
</div>
|
||||
<div class="overflow-x-clip overflow-y-auto relative mt-3 border-y flex-1">
|
||||
{#each filteredTableKeys as tableKey}
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
workspace: $workspaceStore
|
||||
})}
|
||||
initialTableKey={input.specificTable}
|
||||
initialSchemaKey={input.type == 'database' ? input.specificSchema : undefined}
|
||||
{dbType}
|
||||
{refresh}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
<script module lang="ts">
|
||||
export const EDITOR_BAR_WIDTH_THRESHOLD = 1420
|
||||
|
||||
function getImportWmillTsStatement(lang: string | undefined) {
|
||||
if (lang === 'deno') {
|
||||
return `import * as wmill from "npm:windmill-client@1"\n`
|
||||
} else if (lang === 'bun' || lang === 'bunnative') {
|
||||
return `import * as wmill from "windmill-client"\n`
|
||||
} else if (lang === 'nativets') {
|
||||
return `import * as wmill from "./windmill.ts"\n`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -516,15 +527,9 @@
|
||||
bind:this={variablePicker}
|
||||
pickCallback={(path, name) => {
|
||||
if (!editor) return
|
||||
if (lang == 'deno') {
|
||||
if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
|
||||
if (!editor.getCode().includes('import * as wmill from')) {
|
||||
editor.insertAtBeginning(`import * as wmill from "npm:windmill-client@1"\n`)
|
||||
}
|
||||
editor.insertAtCursor(`(await wmill.getVariable('${path}'))`)
|
||||
} else if (lang === 'bun' || lang === 'bunnative') {
|
||||
const code = editor.getCode()
|
||||
if (!code.includes(`import * as wmill from`)) {
|
||||
editor.insertAtBeginning(`import * as wmill from "windmill-client"\n`)
|
||||
editor.insertAtBeginning(getImportWmillTsStatement(lang))
|
||||
}
|
||||
editor.insertAtCursor(`(await wmill.getVariable('${path}'))`)
|
||||
} else if (lang == 'python3') {
|
||||
@@ -546,12 +551,6 @@
|
||||
editor.insertAtCursor(
|
||||
`\nInvoke-RestMethod -Headers $Headers -Uri "$Env:BASE_INTERNAL_URL/api/w/$Env:WM_WORKSPACE/variables/get_value/${path}"`
|
||||
)
|
||||
} else if (lang == 'nativets') {
|
||||
const code = editor.getCode()
|
||||
if (!code.includes(`import * as wmill from`)) {
|
||||
editor.insertAtBeginning(`import * as wmill from "./windmill.ts"\n`)
|
||||
}
|
||||
editor.insertAtCursor(`(await wmill.getVariable('${path}'))`)
|
||||
} else if (lang == 'php') {
|
||||
editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/variables/get_value/${path}');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN')));
|
||||
@@ -605,15 +604,9 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
|
||||
bind:this={resourcePicker}
|
||||
pickCallback={(path, _, resType) => {
|
||||
if (!editor) return
|
||||
if (lang == 'deno') {
|
||||
if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
|
||||
if (!editor.getCode().includes('import * as wmill from')) {
|
||||
editor.insertAtBeginning(`import * as wmill from "npm:windmill-client@1"\n`)
|
||||
}
|
||||
editor.insertAtCursor(`(await wmill.getResource('${path}'))`)
|
||||
} else if (lang === 'bun' || lang === 'bunnative') {
|
||||
const code = editor.getCode()
|
||||
if (!code.includes(`import * as wmill from`)) {
|
||||
editor.insertAtBeginning(`import * as wmill from "windmill-client"\n`)
|
||||
editor.insertAtBeginning(getImportWmillTsStatement(lang))
|
||||
}
|
||||
editor.insertAtCursor(`(await wmill.getResource('${path}'))`)
|
||||
} else if (lang == 'python3') {
|
||||
@@ -635,12 +628,6 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
|
||||
editor.insertAtCursor(
|
||||
`\nInvoke-RestMethod -Headers $Headers -Uri "$Env:BASE_INTERNAL_URL/api/w/$Env:WM_WORKSPACE/resources/get_value_interpolated/${path}"`
|
||||
)
|
||||
} else if (lang == 'nativets') {
|
||||
const code = editor.getCode()
|
||||
if (!code.includes(`import * as wmill from`)) {
|
||||
editor.insertAtBeginning(`import * as wmill from "./windmill.ts"\n`)
|
||||
}
|
||||
editor.insertAtCursor(`(await wmill.getResource('${path}'))`)
|
||||
} else if (lang == 'php') {
|
||||
editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/resources/get_value_interpolated/${path}');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN')));
|
||||
@@ -735,7 +722,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
editor?.insertAtCursor(`dl = wmill.ducklake(${name == 'main' ? '' : `'${name}'`})\n`)
|
||||
} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
|
||||
if (!editor?.getCode().includes('import * as wmill from')) {
|
||||
editor?.insertAtBeginning(`import * as wmill from "npm:windmill-client@1"\n`)
|
||||
editor?.insertAtBeginning(getImportWmillTsStatement(lang))
|
||||
}
|
||||
editor?.insertAtCursor(`let sql = wmill.ducklake(${name == 'main' ? '' : `'${name}'`})\n`)
|
||||
}
|
||||
@@ -769,7 +756,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
pickCallback={async (_, name) => {
|
||||
if (lang === 'duckdb') {
|
||||
const connStr = name == 'main' ? 'datatable' : `datatable://${name}`
|
||||
editor?.insertAtCursor(`ATTACH '${connStr}' AS dt;\n`)
|
||||
editor?.insertAtCursor(`ATTACH '${connStr}' AS dt; USE dt;\n`)
|
||||
} else if (lang === 'python3') {
|
||||
if (!editor?.getCode().includes('import wmill')) {
|
||||
editor?.insertAtBeginning('import wmill\n')
|
||||
@@ -777,7 +764,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
editor?.insertAtCursor(`db = wmill.datatable(${name == 'main' ? '' : `'${name}'`})\n`)
|
||||
} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
|
||||
if (!editor?.getCode().includes('import * as wmill from')) {
|
||||
editor?.insertAtBeginning(`import * as wmill from "npm:windmill-client@1"\n`)
|
||||
editor?.insertAtBeginning(getImportWmillTsStatement(lang))
|
||||
}
|
||||
editor?.insertAtCursor(`let sql = wmill.datatable(${name == 'main' ? '' : `'${name}'`})\n`)
|
||||
}
|
||||
@@ -844,7 +831,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
editor?.insertAtCursor(`wmill.load_s3_file(${s})`)
|
||||
} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
|
||||
if (!editor?.getCode().includes('import * as wmill from')) {
|
||||
editor?.insertAtBeginning(`import * as wmill from "npm:windmill-client@1"\n`)
|
||||
editor?.insertAtBeginning(getImportWmillTsStatement(lang))
|
||||
}
|
||||
editor?.insertAtCursor(`wmill.loadS3File(${s})`)
|
||||
}
|
||||
|
||||
@@ -74,12 +74,17 @@
|
||||
dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificTable })
|
||||
} else if (asset.kind === 'datatable') {
|
||||
let datatable = asset.path.split('/')[0]
|
||||
let specificTable = asset.path.split('/')[1] as string | undefined
|
||||
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
|
||||
let [specificSchema, specificTable] =
|
||||
specificTableSplit?.length === 2
|
||||
? [specificTableSplit[0], specificTableSplit[1]]
|
||||
: [undefined, specificTableSplit?.[0]]
|
||||
dbManagerDrawer?.openDrawer({
|
||||
type: 'database',
|
||||
resourceType: 'postgresql',
|
||||
resourcePath: `datatable://${datatable}`,
|
||||
specificTable
|
||||
specificTable,
|
||||
specificSchema
|
||||
})
|
||||
}
|
||||
onClick?.()
|
||||
|
||||
@@ -206,7 +206,6 @@
|
||||
inputClass={selectInputClass}
|
||||
placeholder={placeholder ?? `${resourceType ?? 'any'} resource`}
|
||||
itemLabelWrapperClasses="flex-1"
|
||||
itemButtonWrapperClasses="flex items-center"
|
||||
>
|
||||
{#snippet endSnippet({ item, close })}
|
||||
<Button
|
||||
|
||||
@@ -238,7 +238,8 @@
|
||||
dbPath.split('$res:')[1],
|
||||
$workspaceStore,
|
||||
dbSchemas,
|
||||
() => {}
|
||||
() => {},
|
||||
{ useLegacyScripts: true }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import { JobService, ResourceService } from '$lib/gen'
|
||||
|
||||
import { runScriptAndPollResult } from '$lib/components/jobs/utils'
|
||||
import type { DbInput } from '$lib/components/dbTypes'
|
||||
import { getLanguageByResourceType, resourceTypeToLang, scripts, type TableMetadata } from './utils'
|
||||
import {
|
||||
getLanguageByResourceType,
|
||||
resourceTypeToLang,
|
||||
legacyScripts,
|
||||
scriptsV2,
|
||||
type TableMetadata
|
||||
} from './utils'
|
||||
|
||||
import { type Preview } from '$lib/gen'
|
||||
import type { DBSchema, DBSchemas, GraphqlSchema, SQLSchema } from '$lib/stores'
|
||||
@@ -289,8 +295,13 @@ export async function getDbSchemas(
|
||||
resourcePath: string,
|
||||
workspace: string | undefined,
|
||||
dbSchemas: DBSchemas,
|
||||
errorCallback: (message: string) => void
|
||||
errorCallback: (message: string) => void,
|
||||
options: {
|
||||
useLegacyScripts?: boolean // To avoid breaking app policies
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
let scripts = options.useLegacyScripts ? legacyScripts : scriptsV2
|
||||
|
||||
if (!scripts[resourceType]) return
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
@@ -60,7 +60,7 @@ export function resourceTypeToLang(rt: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const scripts: Record<
|
||||
const legacyScripts: Record<
|
||||
string,
|
||||
{
|
||||
code: string
|
||||
@@ -76,7 +76,7 @@ const scripts: Record<
|
||||
const table_schema = a.table_schema
|
||||
delete a.table_schema
|
||||
acc[table_schema] = acc[table_schema] || []
|
||||
acc[table_schema].push(a)
|
||||
if (a.table_name || a.column_name) acc[table_schema].push(a)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
@@ -266,7 +266,19 @@ return schema
|
||||
}
|
||||
}
|
||||
|
||||
export { scripts }
|
||||
// We cannot modify the original legacyScripts because they are used to calculate app policies in AppDbExplorer
|
||||
// TODO: Refactor the app policy system to avoid this
|
||||
const scriptsV2: typeof legacyScripts = {
|
||||
...legacyScripts,
|
||||
postgresql: {
|
||||
...legacyScripts.postgresql,
|
||||
code: `
|
||||
SELECT table_name, column_name, udt_name, column_default, is_nullable, nsp.nspname AS table_schema FROM information_schema.columns
|
||||
RIGHT JOIN pg_namespace nsp ON table_schema = nsp.nspname WHERE nsp.nspname NOT IN ('information_schema', 'pg_toast', 'pg_catalog')`
|
||||
}
|
||||
}
|
||||
|
||||
export { legacyScripts, scriptsV2 }
|
||||
|
||||
export function formatSchema(dbSchema: {
|
||||
lang: SQLSchema['lang']
|
||||
|
||||
@@ -115,6 +115,7 @@ export type IDbSchemaOps = {
|
||||
onDelete: (params: { tableKey: string; schema?: string }) => Promise<void>
|
||||
onCreate: (params: { values: CreateTableValues; schema?: string }) => Promise<void>
|
||||
previewCreateSql: (params: { values: CreateTableValues; schema?: string }) => string
|
||||
onCreateSchema: (params: { schema: string }) => Promise<void>
|
||||
}
|
||||
|
||||
export function dbSchemaOpsWithPreviewScripts({
|
||||
@@ -144,7 +145,16 @@ export function dbSchemaOpsWithPreviewScripts({
|
||||
requestBody: { args: dbArg, content: query, language }
|
||||
})
|
||||
},
|
||||
previewCreateSql: ({ values, schema }) => makeCreateTableQuery(values, dbType, schema)
|
||||
previewCreateSql: ({ values, schema }) => makeCreateTableQuery(values, dbType, schema),
|
||||
onCreateSchema: async ({ schema }) => {
|
||||
let createSchemaQuery = `CREATE SCHEMA ${schema};`
|
||||
if (input.type === 'ducklake')
|
||||
createSchemaQuery = wrapDucklakeQuery(createSchemaQuery, input.ducklake)
|
||||
await runScriptAndPollResult({
|
||||
workspace,
|
||||
requestBody: { args: { ...dbArg }, language, content: createSchemaQuery }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ export type DbInput =
|
||||
type: 'database'
|
||||
resourceType: DbType
|
||||
resourcePath: string
|
||||
specificSchema?: string
|
||||
specificTable?: string
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
itemLabelWrapperClasses,
|
||||
itemButtonWrapperClasses,
|
||||
size = 'md',
|
||||
showPlaceholderOnOpen = false,
|
||||
transformInputSelectedText,
|
||||
groupBy,
|
||||
sortBy,
|
||||
onFocus,
|
||||
@@ -74,6 +76,8 @@
|
||||
itemLabelWrapperClasses?: string
|
||||
itemButtonWrapperClasses?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
showPlaceholderOnOpen?: boolean
|
||||
transformInputSelectedText?: (text: string) => string
|
||||
groupBy?: (item: Item) => string
|
||||
sortBy?: (a: Item, b: Item) => number
|
||||
onFocus?: () => void
|
||||
@@ -119,6 +123,11 @@
|
||||
if (onClear) onClear()
|
||||
else value = undefined
|
||||
}
|
||||
|
||||
let inputText = $derived.by(() => {
|
||||
let text = valueEntry?.label ?? getLabel({ value }) ?? ''
|
||||
return transformInputSelectedText?.(text) ?? text
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -151,15 +160,12 @@
|
||||
{autofocus}
|
||||
{disabled}
|
||||
type="text"
|
||||
bind:value={
|
||||
() => (open ? filterText : (valueEntry?.label ?? getLabel({ value }) ?? '')),
|
||||
(v) => {
|
||||
if (open) {
|
||||
filterText = v
|
||||
}
|
||||
}
|
||||
}
|
||||
placeholder={loading && !value ? 'Loading...' : value ? valueEntry?.label : placeholder}
|
||||
bind:value={() => (open ? filterText : inputText), (v) => open && (filterText = v)}
|
||||
placeholder={loading && !value
|
||||
? 'Loading...'
|
||||
: value && !showPlaceholderOnOpen
|
||||
? inputText
|
||||
: placeholder}
|
||||
style={containerStyle}
|
||||
class={twMerge(
|
||||
inputBaseClass,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { untrack, type Snippet } from 'svelte'
|
||||
import type { ProcessedItem } from './utils.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { PlusIcon } from 'lucide-svelte'
|
||||
|
||||
let {
|
||||
processedItems: _processedItems,
|
||||
@@ -169,7 +170,7 @@
|
||||
)}
|
||||
style="height: {uiState.visible ? dropdownPos.height : 0}px;"
|
||||
>
|
||||
<div bind:this={listEl} class="flex flex-col max-h-64 rounded-md bg-surface-tertiary">
|
||||
<div bind:this={listEl} class="flex flex-col max-h-64 rounded-md bg-surface-input">
|
||||
{@render header?.()}
|
||||
{#if processedItems?.length === 0}
|
||||
<div class="py-8 px-4 text-center text-primary text-xs">{noItemsMsg}</div>
|
||||
@@ -190,11 +191,10 @@
|
||||
<button
|
||||
class={twMerge(
|
||||
'py-2 px-4 w-full font-normal text-left text-primary text-xs',
|
||||
itemIndex === keyArrowPos
|
||||
? 'bg-surface-secondary'
|
||||
: item.value === value
|
||||
? 'bg-surface-secondary'
|
||||
: 'hover:bg-surface-hover',
|
||||
itemIndex === keyArrowPos || item.value === value
|
||||
? 'bg-surface-secondary dark:bg-surface-tertiary'
|
||||
: 'hover:bg-surface-hover',
|
||||
endSnippet || item.__is_create ? 'flex items-center justify-between gap-2' : '',
|
||||
itemButtonWrapperClasses,
|
||||
item.disabled ? 'cursor-not-allowed text-disabled' : ''
|
||||
)}
|
||||
@@ -207,7 +207,11 @@
|
||||
<span class={itemLabelWrapperClasses}>
|
||||
{item.label || '\xa0'}
|
||||
</span>
|
||||
{@render endSnippet?.({ item, close: () => (open = false) })}
|
||||
{#if item.__is_create}
|
||||
<PlusIcon class="inline ml-auto" size={16} />
|
||||
{:else}
|
||||
{@render endSnippet?.({ item, close: () => (open = false) })}
|
||||
{/if}
|
||||
{#if item.subtitle}
|
||||
<div class="text-2xs text-secondary">{item.subtitle}</div>
|
||||
{/if}
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
class="flex-1"
|
||||
bind:value
|
||||
onCreateItem={(i) => (value = i)}
|
||||
placeholder="PostgreSQL database name"
|
||||
placeholder="Search or create..."
|
||||
showPlaceholderOnOpen
|
||||
items={onlySelectedTags}
|
||||
disabled={!$isCustomInstanceDbEnabled}
|
||||
itemButtonWrapperClasses="flex items-center justify-between gap-2"
|
||||
>
|
||||
{#snippet endSnippet({ item })}
|
||||
{#if !customInstanceDbs.current?.[item.value]?.success}
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
`GRANT CONNECT ON DATABASE "${dbname}" TO custom_instance_user;\n` +
|
||||
'GRANT USAGE ON SCHEMA public TO custom_instance_user;\n' +
|
||||
'GRANT CREATE ON SCHEMA public TO custom_instance_user;\n' +
|
||||
`GRANT CREATE ON DATABASE "${dbname}" TO custom_instance_user;\n` +
|
||||
'ALTER DEFAULT PRIVILEGES IN SCHEMA public \n' +
|
||||
' GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES\n TO custom_instance_user;'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user