From acd441ac54c8f17f6285ba5cf9683298317f140f Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Thu, 1 May 2025 10:33:42 +0200 Subject: [PATCH] feat: Database Manager (#5586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * hack fix dnd with tick * DBExplorer table left table selector * kinda works * correct table metadata * separated columnDefs creation logic * Removed dependency on AppDbExplorer * (tweak) loadTableMetaData much faster * nit for darkmode * DBExplorerDrawerButton * footer * count footer * reload * update * fix height * db explorer btn in resources table * delete row * InsertRowDrawerButton * insert * refresh on insert and delete * moved db logic to ts file * better update ux * moved all IO upwards * fix: Remaining svelte 5 bugs (#5563) * hack fix dnd with tick * fix: infinite loading in CodeDisplay after update to svelte 5 * regen package-lock * fix tutorial (#5562) * fix tutorial first part * fix tutorial --------- Co-authored-by: Ruben Fiszel * feat: Implement sending diff to ai (#5510) * allow mentioning specific files in instructions * remove not working highlight implementation * make highlighted text work * fix tooltip position * clean code * cleaning * use lib for tooltip positioning * fix logic * draft for db in context * use tools for db in context * fixes * cleaning and bug fixes * fix * cleaning * fix when script is db type * simplify logic * put schema in context if already here * fix imports * fix tooltip position and make it scrollable * remove console logs * check if selected is in available * fix tooltip list * add back lost logic * last fix * fix type errors * use loaded schema from dbSchemas * fix typing, content and lang are always there * remove from context if not available anymore * add not loaded yet mention if schema not loaded * add missing callback logic * fix prompt * fix usage of updateselectedContext function * fix styling for white theme * handle tab and arrows * fix schemas not being refreshed on contexts * also refresh displayMessages when dbschemas change * fix duplicate available contexts * fix logic for new scripts * fix new lines inside text area * implement sending diff in context * add button in deploy options to ask ai about diff * also visualize change when asking for diff * better prompt * add limit to diff size * put diff mode toggle in editor bar * add button to see history from editor * adjustements * put see diff button in dropdown * fixes * better styling * highlight if diff mode * format files * change buttons based on diffmode * remove diff after sending message * fix type error * smaller buttons * draft * use existing editor in diff editor * fix number of db resources fetches * fix apply and add buttons on diff mode * cleaning * undo ai gen button show * better buttons * styling asjustements + show diff in badge * styling * fix deployed code check * cleaning and styling * better quick actions * dont send code when analyzing * remove apply in chat if only code and no diff * fix bad code refactor --------- Co-authored-by: HugoCasa Co-authored-by: Ruben Fiszel * fix: prevent invalid returned ai completion object errors (#5564) * fix(frontend): app builder - force json configuration in rich result (#5565) * feat: make azure a standalone AI provider (#5558) * feat: make azure a standalone AI provider * oups * nit * fix: openai/azure oauth * nit * nits * feat(frontend) add flow step result viewer (#5398) * fix(cli): properly handle enabled/disabled updates of schedules * fix benchmarks * feat: handle sending selected lines to ai context (#5527) * allow mentioning specific files in instructions * remove not working highlight implementation * make highlighted text work * fix tooltip position * clean code * cleaning * use lib for tooltip positioning * fix logic * draft for db in context * use tools for db in context * fixes * cleaning and bug fixes * fix * cleaning * fix when script is db type * simplify logic * put schema in context if already here * fix imports * fix tooltip position and make it scrollable * remove console logs * check if selected is in available * fix tooltip list * add back lost logic * last fix * fix type errors * use loaded schema from dbSchemas * fix typing, content and lang are always there * remove from context if not available anymore * add not loaded yet mention if schema not loaded * add missing callback logic * fix prompt * fix usage of updateselectedContext function * fix styling for white theme * handle tab and arrows * fix schemas not being refreshed on contexts * also refresh displayMessages when dbschemas change * fix duplicate available contexts * fix logic for new scripts * fix new lines inside text area * implement sending diff in context * add button in deploy options to ask ai about diff * also visualize change when asking for diff * better prompt * add limit to diff size * put diff mode toggle in editor bar * add button to see history from editor * adjustements * put see diff button in dropdown * fixes * better styling * handle adding code piece to context * add code piece in context * draft start end markers * adapt code * draft * apply code pieces before sending request * cleaning * highlight if diff mode * format files * change buttons based on diffmode * remove diff after sending message * fix type error * smaller buttons * draft * use existing editor in diff editor * fix number of db resources fetches * fix apply and add buttons on diff mode * cleaning * undo ai gen button show * better buttons * better prompt * remove console log * fix merge * avoid duplicates * fix merge * fix * fix apply logic * remove useless if * focus text area + close chat if no selected lines --------- Co-authored-by: HugoCasa * fix: flow editor svelte 5 issues (#5567) * feat: add diff toggle to flow inline scripts (#5550) * draft flow diff * add missing import * cleaning * code cleaning * fix for recursive renderings * fix typo * cleaning --------- Co-authored-by: HugoCasa * fix(frontend): proper each block binding + better app settings reactivity (#5568) * fix: properly bind to array elements in Svelte each loops This commit fixes an issue where binding directly to loop variables in Svelte's #each loops doesn't properly update the original array. Instead of binding directly to the loop variable, we now bind to the array elements using index variables. The pattern used is: - Change: {#each arr as el} -> {#each arr as _, index} - Change: bind:value={el} -> bind:value={arr[index]} Modified files: - frontend/src/lib/components/ArrayTypeNarrowing.svelte - frontend/src/lib/components/apps/editor/AppInputs.svelte - frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte * better app settings panel reactivity --------- Co-authored-by: Ruben Fiszel Co-authored-by: HugoCasa * fix: app editor svelte 5 fixes (#5570) * fix: properly bind to array elements in Svelte each loops This commit fixes an issue where binding directly to loop variables in Svelte's #each loops doesn't properly update the original array. Instead of binding directly to the loop variable, we now bind to the array elements using index variables. The pattern used is: - Change: {#each arr as el} -> {#each arr as _, index} - Change: bind:value={el} -> bind:value={arr[index]} Modified files: - frontend/src/lib/components/ArrayTypeNarrowing.svelte - frontend/src/lib/components/apps/editor/AppInputs.svelte - frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte * better app settings panel reactivity * fix: app editor table svelte 5 fixes --------- Co-authored-by: Guilhem Co-authored-by: Ruben Fiszel * select border (#5571) * fix: properly bind to array elements in Svelte each loops This commit fixes an issue where binding directly to loop variables in Svelte's #each loops doesn't properly update the original array. Instead of binding directly to the loop variable, we now bind to the array elements using index variables. The pattern used is: - Change: {#each arr as el} -> {#each arr as _, index} - Change: bind:value={el} -> bind:value={arr[index]} Modified files: - frontend/src/lib/components/ArrayTypeNarrowing.svelte - frontend/src/lib/components/apps/editor/AppInputs.svelte - frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte * better app settings panel reactivity * fix: app editor table svelte 5 fixes * fix: select border --------- Co-authored-by: Guilhem Co-authored-by: Ruben Fiszel * feat: add windmill context to autocomplete (#5548) * add windmill context to autocomplete * fix formatting * remove console log * do not mention tool call for autocomplete * apply logic to php --------- Co-authored-by: HugoCasa * fix tabs selected behavior change from svelte 5 * fix: fix list jobs by tag * fix: tenant id to never be undefined on teams (#5572) * fix: tenant id to never be undefined * simplify azure oauth * simplify azure oauth * update ee ref * sqlx prepare * sqlx prepare * fix: legacy script gen model selection (#5574) * feat: add wildcards filter for worker/label/tags * fix: Dynamic select does not work with tag //native (#5576) closes #5490 * function takes 13 arguments but 14 arguments were supplied (#5577) * fix(frontend): prevent deploy popover to show if deploy dropdown is open (#5542) * prevent deploy popover to show if deploy dropdown is open * wip * Revert "wip" This reverts commit 85434654afa9cea7643e77cdc3c76f243d9d29f1. * Revert "prevent deploy popover to show if deploy dropdown is open" This reverts commit edd9eda15631b72c14d50b6f02c4ce6fcf47c4b7. * add prop to hide popup fro dropdown * feat: button can have tooltip * improve deploy tooltip and dropdown behavior * rename tooltip to tooltipPopover * nit * add deploy button component * use svelte 5 runes * use new deploy button for script builder * add delay to deploy popover when dropdown is open * add delay to deploy popover when dropdown is open # Conflicts: # frontend/src/lib/components/DeployButton.svelte # frontend/src/lib/components/common/button/Button.svelte * Update frontend/src/lib/components/common/button/Button.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * remove unsused field * nit --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * chore: update hub sync script (#5579) * fix monaco suggestion z-index (#5578) * fix: validate saved module before passing to flow module editor (#5580) * fix: freeze when clicking script history diff button (#5581) * fix: validate saved module before passing to flow module editor * fix: freeze when clicking script history diff button * add space (#5582) * fix: binding not working in nested array script arg (#5585) * fix: mssql ca_cert deserializing (#5587) * fix: improve app image picker UX (#5589) * DBTableAction * delete table * fix intempestive error toasts * fullscreen mode * rename db explorer to db manager * use drawer open state instead of oo open method * create table btn * factor away sucess text * basic table creation form * uniq check * better select * better add btn * extra settings * create table works in pgsql * MySQL kinda works * CRUD works in mysql * fix lowercase * allow create table with no schema * handle default value * sql repl pane * execute sql queries * db ops opt in * SQL Repl v0 * ux * UX * better refresh * better placeholder sql * sql code clipboard copy * handle multiple primary keys in table creation * fix all fields being required on insert * fix postgres enum not properly converted * use InsertRowDrawerButton in App db studio * insert shortcut * fix mysql datetime parser for non tz dates * npm run check * Revert "fix all fields being required on insert" This reverts commit 6bec952fb73a9f9f401f64640ae300c7072b8268. * fk ui * don't commit .env... * tweak * fk ui bindings * fk dropdowns right values * schema notation fixes * handle on delete / update cascade etc * better loading button without flicker * fix infinite loop caused by getDbSchemas * foreign key error validation * type error * cache col defs * fix label hover * fix fk select overflow * Fix Select styling * mssql fixes * fix wrong typecast failing with mssql * extract makeLoadTableMetaDataQuery * Fetch all col defs in one go (mysql) * loadAllTablesMetaData for postgres * for some reason factoring transformColumnDefs broke ag infinite table * mssql loads all coldefs at once * snowflake preload all col defs * filter out information schema snowflake * default schema select * fix original pg col def logic broken * Fix ugly flickers * fix updateGrid before grid ready * better auto sizing * smoother CRUD UI refreshes * fix col defs qury for bigquery * bigquery works * nits * do not change queries used in policies ! * fix runPreviewJobAndPollResult on WINDMILL_TOO_BIG * select padding * ellipsis typos * fix mysterious ugly red flash when mounting ag grid in dark mode only * Load on click * Remove schema explorer mode * repl min size * fix flash on dark mode * dirty fix ag grid not refreshing sometimes * sql repl history * fix ag theme * close sql repl result viewer by clicking outside * Select styling fix in dark mode * better default query sqlrepl * Buttons less aggressive --------- Co-authored-by: Ruben Fiszel Co-authored-by: centdix <40307056+centdix@users.noreply.github.com> Co-authored-by: HugoCasa Co-authored-by: Alexander Petric Co-authored-by: Guilhem Co-authored-by: Piyush मिश्र Co-authored-by: Piyush मिश्र Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- frontend/package-lock.json | 2 - .../src/lib/components/BoundedInputNumber | 0 frontend/src/lib/components/DBManager.svelte | 206 +++++ .../components/DBManagerDrawerButton.svelte | 272 ++++++ .../lib/components/DBSchemaExplorer.svelte | 127 +-- frontend/src/lib/components/DBTable.svelte | 206 +++++ .../src/lib/components/DBTableEditor.svelte | 435 +++++++++ .../src/lib/components/ResourcePicker.svelte | 7 +- .../src/lib/components/SimpleAgTable.svelte | 94 ++ frontend/src/lib/components/SqlRepl.svelte | 178 ++++ .../display/InsertRowDrawerButton.svelte | 61 ++ .../display/dbtable/AppDbExplorer.svelte | 41 +- .../display/dbtable/queries/count.ts | 2 +- .../display/dbtable/queries/createTable.ts | 93 ++ .../display/dbtable/queries/delete.ts | 4 +- .../display/dbtable/queries/deleteTable.ts | 6 + .../display/dbtable/queries/select.ts | 5 +- .../display/dbtable/queries/update.ts | 13 +- .../apps/components/display/dbtable/utils.ts | 254 ++++-- .../apps/components/display/table/utils.ts | 143 ++- .../lib/components/common/CloseButton.svelte | 3 +- .../common/drawer/DrawerContent.svelte | 3 +- frontend/src/lib/components/dbOps.ts | 169 ++++ .../flows/propPicker/StepHistory.svelte | 19 +- frontend/src/lib/components/jobs/utils.ts | 39 + frontend/src/lib/consts.ts | 10 + frontend/src/lib/stores.ts | 13 + frontend/src/lib/utils.ts | 6 + .../(root)/(logged)/resources/+page.svelte | 839 +++++++++--------- frontend/tailwind.config.cjs | 7 +- 30 files changed, 2612 insertions(+), 645 deletions(-) delete mode 100644 frontend/src/lib/components/BoundedInputNumber create mode 100644 frontend/src/lib/components/DBManager.svelte create mode 100644 frontend/src/lib/components/DBManagerDrawerButton.svelte create mode 100644 frontend/src/lib/components/DBTable.svelte create mode 100644 frontend/src/lib/components/DBTableEditor.svelte create mode 100644 frontend/src/lib/components/SimpleAgTable.svelte create mode 100644 frontend/src/lib/components/SqlRepl.svelte create mode 100644 frontend/src/lib/components/apps/components/display/InsertRowDrawerButton.svelte create mode 100644 frontend/src/lib/components/apps/components/display/dbtable/queries/createTable.ts create mode 100644 frontend/src/lib/components/apps/components/display/dbtable/queries/deleteTable.ts create mode 100644 frontend/src/lib/components/dbOps.ts create mode 100644 frontend/src/lib/components/jobs/utils.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d58336161f..9c3d0635a6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11220,8 +11220,6 @@ }, "node_modules/zod": { "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/frontend/src/lib/components/BoundedInputNumber b/frontend/src/lib/components/BoundedInputNumber deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte new file mode 100644 index 0000000000..cfca62c71b --- /dev/null +++ b/frontend/src/lib/components/DBManager.svelte @@ -0,0 +1,206 @@ + + + + +
+ {#if dbSupportsSchemas} + + {/if} + +
+
+ {#each filteredTableKeys as tableKey} + + {/each} +
+ +
+ + {#if tableKey} + {#await getColDefs(tableKey) then colDefs} + {#if colDefs && colDefs?.length} + {@const dbTableOps = dbTableOpsFactory({ colDefs, tableKey })} + + {/if} + {/await} + {/if} + +
+ + (askingForConfirmation = undefined)} + on:confirmed={askingForConfirmation?.onConfirm ?? (() => {})} +/> + +{#if dbTableEditorProps} + (dbTableEditorState = { open: false })} + > + (dbTableEditorState = { open: false })} + title="Create a new table" + > + { + await dbTableEditorProps.onConfirm(values) + dbTableEditorState = { open: false } + }} + /> + + +{/if} diff --git a/frontend/src/lib/components/DBManagerDrawerButton.svelte b/frontend/src/lib/components/DBManagerDrawerButton.svelte new file mode 100644 index 0000000000..1120f4211f --- /dev/null +++ b/frontend/src/lib/components/DBManagerDrawerButton.svelte @@ -0,0 +1,272 @@ + + + { + if (e.key === 'Escape') { + if (replResultData) { + replResultData = undefined + } + } + }} +/> + +{#if shouldDisplayError} + + Schema could not be loaded. Please check the permissions of the resource. + +{:else} + + + { + if (replResultData) { + replResultData = undefined + } else { + isDrawerOpen = false + } + }} + CloseIcon={replResultData ? ArrowLeft : undefined} + noPadding + > + {#if dbSchema && $workspaceStore} + + + + +
{ + // Only proceed if the click is directly on this div and not on the child elements + if (e.target === e.currentTarget) { + replResultData = undefined + } + }} + > + {#if replResultData} + {#key replResultData} + + {/key} + {/if} +
+ + dbTableOpsWithPreviewScripts({ + colDefs, + tableKey, + resourcePath, + resourceType, + workspace: $workspaceStore + })} + dbTableActionsFactory={[ + dbDeleteTableActionWithPreviewScript({ + resourcePath, + resourceType, + workspace: $workspaceStore + }) + ]} + {refresh} + dbTableEditorPropsFactory={({ selectedSchemaKey }) => ({ + resourceType, + previewSql: (values) => + makeCreateTableQuery(values, resourceType, selectedSchemaKey), + async onConfirm(values) { + await runPreviewJobAndPollResult({ + workspace: $workspaceStore, + requestBody: { + args: { database: '$res:' + resourcePath }, + content: makeCreateTableQuery(values, resourceType, selectedSchemaKey), + language: getLanguageByResourceType(resourceType) + } + }) + refresh() + } + })} + /> +
+ + { + replResultData = data + }} + placeholderTableName={sortArray( + Object.keys( + dbSchema?.schema[ + 'public' in dbSchema?.schema + ? 'public' + : 'dbo' in dbSchema?.schema + ? 'dbo' + : Object.keys(dbSchema?.schema)?.[0] + ] + ) + )?.[0]} + /> + +
+ {:else} + + + + + + {/if} + + + + - - - - - - {#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC || dbSchema.schema?.dbo)} - - - - - {/if} - {#if dbSchema.lang === 'graphql'} - {#await import('$lib/components/GraphqlSchemaViewer.svelte')} - - {:then Module} - - {/await} - {:else} - - {/if} - - -{:else if shouldDisplayError} - - Schema could not be loaded. Please check the permissions of the resource. - + + + +{/if} +{#if dbSchema.lang === 'graphql'} + {#await import('$lib/components/GraphqlSchemaViewer.svelte')} + + {:then Module} + + {/await} +{:else} + {/if} diff --git a/frontend/src/lib/components/DBTable.svelte b/frontend/src/lib/components/DBTable.svelte new file mode 100644 index 0000000000..337f66b0fd --- /dev/null +++ b/frontend/src/lib/components/DBTable.svelte @@ -0,0 +1,206 @@ + + + + +
+
+ + {#if dbTableOps.onInsert} + { + if (!$workspaceStore) return + dbTableOps.onInsert?.({ values }).then((result) => { + refresh?.() + sendUserToast('Row inserted') + }) + }} + /> + {/if} +
+
+
+ +
{ + if ((e.ctrlKey || e.metaKey) && e.key === 'c') { + const selectedCell = api?.getFocusedCell() + if (selectedCell) { + const rowIndex = selectedCell.rowIndex + const colId = selectedCell.column?.getId() + const rowNode = api?.getDisplayedRowAtIndex(rowIndex) + const selectedValue = rowNode?.data?.[colId] + navigator.clipboard.writeText(selectedValue) + sendUserToast('Copied cell value to clipboard', false) + } + } + }} + >
+
+ +
+
+ + Download +
+ {#if rowCount} + {firstRow}{'->'}{lastRow + 1} of {rowCount} rows + {:else} + {firstRow}{'->'}{lastRow + 1} + {/if} +
+
+
diff --git a/frontend/src/lib/components/DBTableEditor.svelte b/frontend/src/lib/components/DBTableEditor.svelte new file mode 100644 index 0000000000..8381b60c45 --- /dev/null +++ b/frontend/src/lib/components/DBTableEditor.svelte @@ -0,0 +1,435 @@ + + + + + + +
+
+ + +
+ + + + + + Name + Type + Primary + + + + {#each values.columns as column, i} + + + + + + + + {#snippet trigger()} + + {/snippet} + {#snippet content()} + {#if datatypeHasLength(column.datatype)} + + {/if} + + {#if !column.primaryKey} + + {/if} + {/snippet} + + + + + + +
+
+ + + + + + Table + Columns + + + + {#each values.foreignKeys as foreignKey, foreignKeyIndex} + {@const fkErrors = errors?.foreignKeys?.[foreignKeyIndex]} + + + (column.sourceColumn = e.detail.value)} + items={values.columns.map((c) => c.name)} + clearable={false} + /> +
+ +
+ + + + + + + + ON UPDATE + + {/snippet} + + {/if} +
+
+ {/each} + +
+ + {/each} + + + + + + + + + + + + + (askingForConfirmation = undefined)} + on:confirmed={askingForConfirmation?.onConfirm ?? (() => {})} +> + {#if askingForConfirmation?.codeContent} +
+ + {askingForConfirmation.codeContent} + + +
+ {/if} +
diff --git a/frontend/src/lib/components/ResourcePicker.svelte b/frontend/src/lib/components/ResourcePicker.svelte index f82dd888dc..4e50c13911 100644 --- a/frontend/src/lib/components/ResourcePicker.svelte +++ b/frontend/src/lib/components/ResourcePicker.svelte @@ -8,10 +8,11 @@ import ResourceEditorDrawer from './ResourceEditorDrawer.svelte' import { Button } from './common' - import DBSchemaExplorer from './DBSchemaExplorer.svelte' + import DBManagerDrawerButton from './DBManagerDrawerButton.svelte' import DarkModeObserver from './DarkModeObserver.svelte' import { Pen, Plus, RotateCw } from 'lucide-svelte' import { sendUserToast } from '$lib/toast' + import { isDbType } from './apps/components/display/dbtable/utils' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' const dispatch = createEventDispatcher() @@ -221,7 +222,7 @@ iconOnly /> - {#if showSchemaExplorer} - + {#if showSchemaExplorer && isDbType(resourceType) && value} + {/if} diff --git a/frontend/src/lib/components/SimpleAgTable.svelte b/frontend/src/lib/components/SimpleAgTable.svelte new file mode 100644 index 0000000000..47da07fd0b --- /dev/null +++ b/frontend/src/lib/components/SimpleAgTable.svelte @@ -0,0 +1,94 @@ + + + + +
+
+ +
{ + if ((e.ctrlKey || e.metaKey) && e.key === 'c') { + const selectedCell = api?.getFocusedCell() + if (selectedCell) { + const rowIndex = selectedCell.rowIndex + const colId = selectedCell.column?.getId() + const rowNode = api?.getDisplayedRowAtIndex(rowIndex) + const selectedValue = rowNode?.data?.[colId] + navigator.clipboard.writeText(selectedValue) + sendUserToast('Copied cell value to clipboard', false) + } + } + }} + > +
+
+
diff --git a/frontend/src/lib/components/SqlRepl.svelte b/frontend/src/lib/components/SqlRepl.svelte new file mode 100644 index 0000000000..727334cf1d --- /dev/null +++ b/frontend/src/lib/components/SqlRepl.svelte @@ -0,0 +1,178 @@ + + + + + + + + + + + { + const data = e.detail as (typeof runHistory)[number] + editor?.setCode(data.code) + onData(data.result) + }} + /> + + diff --git a/frontend/src/lib/components/apps/components/display/InsertRowDrawerButton.svelte b/frontend/src/lib/components/apps/components/display/InsertRowDrawerButton.svelte new file mode 100644 index 0000000000..fb7d1aaf6e --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/InsertRowDrawerButton.svelte @@ -0,0 +1,61 @@ + + + { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + onConfirm() + } + }} +/> + + + + + + + + + + + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte b/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte index 9148ef6524..32b0ea7993 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte +++ b/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte @@ -21,11 +21,7 @@ import { getContext, tick } from 'svelte' import UpdateCell from './UpdateCell.svelte' import { workspaceStore, type DBSchemas } from '$lib/stores' - import Button from '$lib/components/common/button/Button.svelte' - import { Plus } from 'lucide-svelte' - import { Drawer, DrawerContent } from '$lib/components/common' - import InsertRow from './InsertRow.svelte' - import Portal from '$lib/components/Portal.svelte' + import { Drawer } from '$lib/components/common' import { sendUserToast } from '$lib/toast' import type { AppInput, StaticInput } from '$lib/components/apps/inputType' @@ -41,6 +37,7 @@ import { CancelablePromise } from '$lib/gen' import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte' import RunnableWrapper from '../../helpers/RunnableWrapper.svelte' + import InsertRowDrawerButton from '../InsertRowDrawerButton.svelte' export let id: string export let configuration: RichConfigurations @@ -173,8 +170,6 @@ ) } - let args: Record = {} - let outputs = initOutput($worldStore, id, { selectedRowIndex: 0, selectedRow: {}, @@ -189,7 +184,7 @@ let lastResource: string | undefined = undefined - function updateOneOfConfiguration( + function updateOneOfConfiguration( oneOfConfiguration: OneOfConfiguration, resolvedConfig: { configuration: Record @@ -249,7 +244,7 @@ resolvedConfig.type.configuration[resolvedConfig?.type?.selected].resource.split(':')[1], $workspaceStore, dbSchemas, - (message: string) => {} + () => {} ) updateOneOfConfiguration( @@ -263,7 +258,7 @@ resolvedConfig?.type?.selected, resource.split(':')[1], $workspaceStore! - ) + ) : [], loading: false } @@ -479,8 +474,6 @@ }, 1500) } - let isInsertable: boolean = false - $: $worldStore && render && connectToComponents() function connectToComponents() { @@ -521,7 +514,7 @@ } } - async function insert() { + async function insert(args: object) { try { const selected = resolvedConfig.type.selected @@ -539,8 +532,6 @@ } catch (e) { sendUserToast(e.message, true) } - - args = {} } let runnableComponent: RunnableComponent @@ -651,17 +642,11 @@
{#if hideInsert !== true} - + insert(args)} + /> {/if}
@@ -696,7 +681,7 @@ - + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts index 2c1960a80a..482752b844 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts @@ -2,7 +2,7 @@ import type { AppInput, RunnableByName } from '$lib/components/apps/inputType' import { buildParameters, type DbType } from '../utils' import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils' -function makeCountQuery( +export function makeCountQuery( dbType: DbType, table: string, whereClause: string | undefined = undefined, diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/createTable.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/createTable.ts new file mode 100644 index 0000000000..06a952312a --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/createTable.ts @@ -0,0 +1,93 @@ +import { dbSupportsSchemas, type DbType } from '../utils' + +export type CreateTableValues = { + name: string + columns: CreateTableValuesColumn[] + foreignKeys: { + targetTable?: string + columns: { + sourceColumn?: string + targetColumn?: string + }[] + onDelete: 'CASCADE' | 'SET NULL' | 'NO ACTION' + onUpdate: 'CASCADE' | 'SET NULL' | 'NO ACTION' + }[] +} + +type CreateTableValuesColumn = { + name: string + datatype: string + primaryKey?: boolean + defaultValue?: string + not_null?: boolean + datatype_length?: number // e.g varchar(255) +} + +export function makeCreateTableQuery( + values: CreateTableValues, + resourceType: DbType, + schema?: string +) { + const pkCount = values.columns.reduce((p, c) => p + (c.primaryKey ? 1 : 0), 0) + + function transformColumn(c: CreateTableValuesColumn): string { + const datatype = c.datatype_length ? `${c.datatype}(${c.datatype_length})` : c.datatype + const defValue = c.defaultValue && formatDefaultValue(c.defaultValue, datatype, resourceType) + + let str = ` ${c.name} ${datatype}` + if (c.not_null) str += ' NOT NULL' + if (defValue) str += ` DEFAULT ${defValue}` + if (pkCount === 1 && c.primaryKey) str += ' PRIMARY KEY' + return str + } + + function transformFk(fk: CreateTableValues['foreignKeys'][number]): string { + const sourceColumns = fk.columns.map((c) => c.sourceColumn).filter(Boolean) + const targetColumns = fk.columns.map((c) => c.targetColumn).filter(Boolean) + const targetTable = + useSchema || !fk.targetTable?.includes('.') + ? fk.targetTable + : fk.targetTable?.split('.').pop() + + let l = ` FOREIGN KEY (${sourceColumns.join(', ')}) REFERENCES ${targetTable} (${targetColumns.join( + ', ' + )})` + if (fk.onDelete !== 'NO ACTION') l += ` ON DELETE ${fk.onDelete}` + if (fk.onUpdate !== 'NO ACTION') l += ` ON UPDATE ${fk.onUpdate}` + return l + } + + const useSchema = dbSupportsSchemas(resourceType) + + const lines = values.columns.map(transformColumn) + lines.push(...values.foreignKeys.map(transformFk)) + if (pkCount > 1) { + const pks = values.columns.filter((c) => c.primaryKey) + lines.push(` PRIMARY KEY (${pks.map((c) => c.name).join(', ')})`) + } + + return `CREATE TABLE ${useSchema && schema ? schema.trim() + '.' : ''}${values.name.trim()} ( +${lines.join(',\n')} +);` +} + +function formatDefaultValue(str: string, datatype: string, resourceType: DbType): string { + if (!str) return '' + if (str.startsWith('{') && str.endsWith('}')) { + return str.slice(1, str.length - 1) + } + if (resourceType === 'postgresql') { + return `CAST('${str}' AS ${datatype})` + } + return `'${str}'` +} + +export function datatypeDefaultLength(datatype: string): number { + datatype = datatype.toLowerCase() + if (datatype == 'bit') return 1 + if (['varchar', 'char', 'nvarchar', 'nchar', 'varbinary', 'binary'].includes(datatype)) { + return 255 + } else { + return 10 + } +} diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts index b74be6a3c8..bf343cb974 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts @@ -1,7 +1,7 @@ import type { AppInput, RunnableByName } from '$lib/components/apps/inputType' import { getLanguageByResourceType, type ColumnDef, buildParameters, type DbType } from '../utils' -function deleteWithAllValues(table: string, columns: ColumnDef[], dbType: DbType) { +export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbType) { let query = buildParameters( dbType === 'snowflake' ? columns.flatMap((c) => [c, c]) : columns, dbType @@ -71,7 +71,7 @@ export function getDeleteInput( name: 'AppDbExplorer', type: 'runnableByName', inlineScript: { - content: deleteWithAllValues(table, columns, dbType), + content: makeDeleteQuery(table, columns, dbType), language: getLanguageByResourceType(dbType), schema: { $schema: 'https://json-schema.org/draft/2020-12/schema', diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/deleteTable.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/deleteTable.ts new file mode 100644 index 0000000000..397d9886e0 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/deleteTable.ts @@ -0,0 +1,6 @@ +import type { DbType } from '../utils' + +export function makeDeleteTableQuery(tableKey: string, resourceType: DbType): string { + // same for all sql dbs + return `DROP TABLE ${tableKey};` +} diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts index 75fab13a13..f4453360ec 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts @@ -79,7 +79,7 @@ function makeSnowflakeSelectQuery( return query } -function makeSelectQuery( +export function makeSelectQuery( table: string, columnDefs: ColumnDef[], whereClause: string | undefined, @@ -155,6 +155,9 @@ CASE WHEN :order_by = '${column.field}' AND :is_desc IS true THEN \`${column.fie case 'ms_sql_server': // MSSQL uses CONCAT for string concatenation and supports OFFSET FETCH for pagination // Note: MSSQL does not have a built-in ILIKE function, so we use LIKE with a case-insensitive collation if needed + // + // Note 2: CONCAT in mssql requires 2 to 254 arguments. But we can't change this query without breaking + // existing policies const orderBy = columnDefs .map((column) => { return ` diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts index c11c5c304f..467922b9d7 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts @@ -1,18 +1,15 @@ import type { AppInput, RunnableByName } from '$lib/components/apps/inputType' import { getLanguageByResourceType, type ColumnDef, buildParameters, type DbType } from '../utils' -function updateWithAllValues( +export function makeUpdateQuery( table: string, - column: ColumnDef, - columns: ColumnDef[], + column: { datatype: string; field: string }, + columns: { datatype: string; field: string }[], dbType: DbType ) { let query = buildParameters( [ - { - field: 'value_to_update', - datatype: column.datatype - }, + { field: 'value_to_update', datatype: column.datatype }, ...(dbType === 'snowflake' ? columns.flatMap((c) => [c, c]) : columns) ], dbType @@ -85,7 +82,7 @@ export function getUpdateInput( name: 'AppDbExplorer', type: 'runnableByName', inlineScript: { - content: updateWithAllValues(table, column, columns, dbType), + content: makeUpdateQuery(table, column, columns, dbType), language: getLanguageByResourceType(dbType), schema: { $schema: 'https://json-schema.org/draft/2020-12/schema', diff --git a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts index 226561f667..0c3fce722f 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts @@ -8,6 +8,7 @@ import { } from 'graphql' import { tryEvery } from '$lib/utils' import { stringifySchema } from '$lib/components/copilot/lib' +import { runPreviewJobAndPollResult } from '$lib/components/jobs/utils' export enum ColumnIdentity { ByDefault = 'By Default', @@ -61,15 +62,99 @@ export async function loadTableMetaData( if (!resource || !table || !workspace) { return undefined } + const job = await JobService.runScriptPreview({ + workspace: workspace, + requestBody: { + language: getLanguageByResourceType(resourceType), + content: await makeLoadTableMetaDataQuery(resource, workspace, table, resourceType), + args: { + database: resource + } + } + }) - let code: string = '' + const maxRetries = 8 + let attempts = 0 + while (attempts < maxRetries) { + try { + await new Promise((resolve) => setTimeout(resolve, 1000 * (attempts || 0.6))) + const testResult = (await JobService.getCompletedJob({ + workspace: workspace, + id: job + })) as any + + if (testResult.success) { + attempts = maxRetries + + if (resourceType === 'ms_sql_server') { + return testResult.result[0].map(lowercaseKeys) + } else { + return testResult.result.map(lowercaseKeys) + } + } else { + attempts++ + } + } catch (error) { + attempts++ + } + } + + console.error('Failed to load table metadata after maximum retries.') + return undefined +} + +export async function loadAllTablesMetaData( + resource: string, + workspace: string | undefined, + resourceType: string +): Promise | undefined> { + if (!resource || !workspace) { + return undefined + } + + try { + let result = (await runPreviewJobAndPollResult({ + workspace: workspace, + requestBody: { + language: getLanguageByResourceType(resourceType), + content: await makeLoadTableMetaDataQuery(resource, workspace, undefined, resourceType), + args: { + database: resource + } + } + })) as ({ table_name: string; schema_name?: string } & object)[] + if (resourceType === 'ms_sql_server') { + result = (result as any)[0] + } + const map: Record = {} + + for (const _col of result) { + const col = lowercaseKeys(_col) + const tableKey = col.schema_name ? `${col.schema_name}.${col.table_name}` : col.table_name + if (!(tableKey in map)) { + map[tableKey] = [] + } + map[tableKey].push(col) + } + return map + } catch (e) { + throw new Error('Error loading all tables metadata: ' + e) + } +} + +async function makeLoadTableMetaDataQuery( + resource: string, + workspace: string, + table: string | undefined, + resourceType: string +): Promise { if (resourceType === 'mysql') { const resourceObj = (await ResourceService.getResourceValue({ workspace, path: resource.split(':')[1] })) as any - code = ` + return ` SELECT COLUMN_NAME as field, COLUMN_TYPE as DataType, @@ -77,19 +162,31 @@ export async function loadTableMetaData( CASE WHEN COLUMN_KEY = 'PRI' THEN 'YES' ELSE 'NO' END as IsPrimaryKey, CASE WHEN EXTRA like '%auto_increment%' THEN 'YES' ELSE 'NO' END as IsIdentity, CASE WHEN IS_NULLABLE = 'YES' THEN 'YES' ELSE 'NO' END as IsNullable, - CASE WHEN DATA_TYPE = 'enum' THEN true ELSE false END as IsEnum + CASE WHEN DATA_TYPE = 'enum' THEN true ELSE false END as IsEnum${ + table + ? '' + : `, + TABLE_NAME as table_name` + } FROM - INFORMATION_SCHEMA.COLUMNS - WHERE + INFORMATION_SCHEMA.COLUMNS${ + table + ? ` + WHERE TABLE_NAME = '${table.split('.').reverse()[0]}' AND TABLE_SCHEMA = '${ - table.split('.').reverse()[1] ?? resourceObj?.database ?? '' - }' - ORDER BY + table.split('.').reverse()[1] ?? resourceObj?.database ?? '' + }'` + : ` + WHERE + TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')` + } + ORDER BY + TABLE_NAME, ORDINAL_POSITION; ` } else if (resourceType === 'postgresql') { - code = ` - SELECT + return ` + SELECT a.attname as field, pg_catalog.format_type(a.atttypid, a.atttypmod) as DataType, (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) @@ -110,17 +207,32 @@ export async function loadTableMetaData( END as IsNullable, (SELECT true FROM pg_catalog.pg_enum e - WHERE e.enumtypid = a.atttypid FETCH FIRST ROW ONLY) as IsEnum - FROM pg_catalog.pg_attribute a + WHERE e.enumtypid = a.atttypid FETCH FIRST ROW ONLY) as IsEnum${ + table + ? '' + : `, + ns.nspname AS schema_name, + c.relname AS table_name` + } + FROM pg_catalog.pg_attribute a${ + table + ? ` WHERE a.attrelid = (SELECT c.oid FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace ns ON c.relnamespace = ns.oid WHERE relname = '${ table.split('.').reverse()[0] }' AND ns.nspname = '${table.split('.').reverse()[1] ?? 'public'}') AND a.attnum > 0 AND NOT a.attisdropped - ORDER BY a.attnum; + ` + : ` + JOIN pg_catalog.pg_class c ON a.attrelid = c.oid + JOIN pg_catalog.pg_namespace ns ON c.relnamespace = ns.oid + WHERE c.relkind = 'r' AND a.attnum > 0 AND NOT a.attisdropped + AND ns.nspname != 'pg_catalog' AND ns.nspname != 'information_schema'` + } + ORDER BY ${table ? 'a.attnum' : 'ns.nspname, c.relname, a.attnum'}; ` } else if (resourceType === 'ms_sql_server') { - code = ` + return ` SELECT COLUMN_NAME as field, DATA_TYPE as DataType, @@ -128,32 +240,52 @@ export async function loadTableMetaData( CASE WHEN COLUMNPROPERTY(OBJECT_ID(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 THEN 'By Default' ELSE 'No' END as IsIdentity, CASE WHEN COLUMNPROPERTY(OBJECT_ID(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 THEN 1 ELSE 0 END as IsPrimaryKey, -- This line still needs correction for primary key identification CASE WHEN IS_NULLABLE = 'YES' THEN 'YES' ELSE 'NO' END as IsNullable, - CASE WHEN DATA_TYPE = 'enum' THEN 1 ELSE 0 END as IsEnum + CASE WHEN DATA_TYPE = 'enum' THEN 1 ELSE 0 END as IsEnum${ + table + ? '' + : `, + TABLE_NAME as table_name` + } FROM - INFORMATION_SCHEMA.COLUMNS + INFORMATION_SCHEMA.COLUMNS${ + table + ? ` WHERE - TABLE_NAME = '${table}' + TABLE_NAME = '${table}'` + : '' + } ORDER BY ORDINAL_POSITION; - ` } else if (resourceType === 'snowflake' || resourceType === 'snowflake_oauth') { - code = ` + return ` select COLUMN_NAME as field, DATA_TYPE as DataType, COLUMN_DEFAULT as DefaultValue, CASE WHEN COLUMN_DEFAULT like 'AUTOINCREMENT%' THEN 'By Default' ELSE 'No' END as IsIdentity, CASE WHEN COLUMN_DEFAULT like 'AUTOINCREMENT%' THEN 1 ELSE 0 END as IsPrimaryKey, CASE WHEN IS_NULLABLE = 'YES' THEN 'YES' ELSE 'NO' END as IsNullable, - CASE WHEN DATA_TYPE = 'enum' THEN 1 ELSE 0 END as IsEnum - from information_schema.columns + CASE WHEN DATA_TYPE = 'enum' THEN 1 ELSE 0 END as IsEnum${ + table + ? '' + : `, + table_name as table_name, + table_schema as schema_name` + } + from information_schema.columns${ + table + ? ` where table_name = '${table.split('.').reverse()[0]}' and table_schema = '${ - table.split('.').reverse()[1] ?? 'PUBLIC' - }' + table.split('.').reverse()[1] ?? 'PUBLIC' + }'` + : "\nwhere table_schema <> 'INFORMATION_SCHEMA'\n" + } order by ORDINAL_POSITION; ` } else if (resourceType === 'bigquery') { - code = `SELECT + // TODO: find a solution for this (query uses hardcoded dataset name) + if (!table) throw new Error('Table name is required for BigQuery') + return `SELECT c.COLUMN_NAME as field, DATA_TYPE as DataType, CASE WHEN COLUMN_DEFAULT = 'NULL' THEN '' ELSE COLUMN_DEFAULT END as DefaultValue, @@ -164,58 +296,14 @@ ORDER BY FROM ${table.split('.')[0]}.INFORMATION_SCHEMA.COLUMNS c LEFT JOIN - test_dataset.INFORMATION_SCHEMA.KEY_COLUMN_USAGE p + ${table.split('.')[0]}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE p on c.table_name = p.table_name AND c.column_name = p.COLUMN_NAME WHERE - c.TABLE_NAME = "${table.split('.')[1]}" + c.TABLE_NAME = '${table.split('.')[1]}' order by c.ORDINAL_POSITION;` } else { throw new Error('Unsupported database type:' + resourceType) } - - const maxRetries = 3 - let attempts = 0 - - while (attempts < maxRetries) { - try { - const job = await JobService.runScriptPreview({ - workspace: workspace, - requestBody: { - language: getLanguageByResourceType(resourceType), - content: code, - args: { - database: resource - } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 3000)) - - const testResult = (await JobService.getCompletedJob({ - workspace: workspace, - id: job - })) as any - - if (testResult.success) { - attempts = maxRetries - - if (resourceType === 'ms_sql_server') { - return lowercaseKeys(testResult.result[0]) - } else { - return lowercaseKeys(testResult.result) - } - } else { - attempts++ - } - } catch (error) { - attempts++ - } - // Exponential back-off - await new Promise((resolve) => setTimeout(resolve, 2000 * attempts)) - } - - console.error('Failed to load table metadata after maximum retries.') - return undefined } export function resourceTypeToLang(rt: string) { @@ -226,14 +314,12 @@ export function resourceTypeToLang(rt: string) { } } -function lowercaseKeys(arr: Array>): Array { - return arr.map((obj) => { - const newObj = {} - Object.keys(obj).forEach((key) => { - newObj[key.toLowerCase()] = obj[key] - }) - return newObj +function lowercaseKeys(obj: Record): any { + const newObj = {} + Object.keys(obj).forEach((key) => { + newObj[key.toLowerCase()] = obj[key] }) + return newObj } const scripts: Record< @@ -569,7 +655,9 @@ export function formatGraphqlSchema(schema: IntrospectionQuery): string { return printSchema(buildClientSchema(schema)) } -export type DbType = 'mysql' | 'ms_sql_server' | 'postgresql' | 'snowflake' | 'bigquery' +export type DbType = (typeof dbTypes)[number] +export const dbTypes = ['mysql', 'ms_sql_server', 'postgresql', 'snowflake', 'bigquery'] as const +export const isDbType = (str?: string): str is DbType => !!str && dbTypes.includes(str as DbType) export function buildVisibleFieldList(columnDefs: ColumnDef[], dbType: DbType) { // Filter out hidden columns to avoid counting the wrong number of rows @@ -714,3 +802,13 @@ export async function getTablesByResource( return [] } } + +export function dbSupportsSchemas(dbType: DbType): boolean { + return dbType === 'postgresql' || dbType === 'snowflake' || dbType === 'bigquery' +} + +export function datatypeHasLength(datatype: string): boolean { + datatype = datatype.toLowerCase() + const lengthDataTypes = ['varchar', 'char', 'nvarchar', 'nchar', 'varbinary', 'binary', 'bit'] + return lengthDataTypes.some((type) => datatype === type) +} diff --git a/frontend/src/lib/components/apps/components/display/table/utils.ts b/frontend/src/lib/components/apps/components/display/table/utils.ts index 038a2c5480..f3a2c93bb3 100644 --- a/frontend/src/lib/components/apps/components/display/table/utils.ts +++ b/frontend/src/lib/components/apps/components/display/table/utils.ts @@ -2,7 +2,12 @@ * Base class for embedding a svelte component within an AGGrid call. * See: https://stackoverflow.com/a/72608215 */ -import type { ICellRendererComp, ICellRendererParams } from 'ag-grid-community' +import type { ColDef, ColGroupDef, ICellRendererComp, ICellRendererParams } from 'ag-grid-community' +import { ColumnIdentity, type ColumnDef } from '../dbtable/utils' +import type { TableAction } from '$lib/components/apps/editor/component' +import { mount, unmount } from 'svelte' +import { Button } from '$lib/components/common' +import { Trash2 } from 'lucide-svelte' /** * Class for defining a cell renderer. @@ -110,3 +115,139 @@ export function defaultCellRenderer(cellRendererType: string) { return undefined } } + +export function transformColumnDefs({ + columnDefs, + actions, + customActionsHeader, + wrapActions, + tableActionsFactory, + onDelete, + onInvalidColumnDefs +}: { + columnDefs: ColumnDef[] + actions?: TableAction[] + customActionsHeader?: string + wrapActions?: boolean + tableActionsFactory?: ReturnType + onDelete?: (values: object) => void + onInvalidColumnDefs?: (errors: string[]) => void +}): (ColDef | ColGroupDef)[] { + if (!columnDefs) { + return [] + } + + const { isValid, errors } = validateColumnDefs(columnDefs) + + if (!isValid) { + onInvalidColumnDefs?.(errors) + return [] + } + + let r: any[] = columnDefs?.filter((x) => x && !x.ignored) ?? [] + + if (onDelete) { + r.push({ + field: 'delete', + headerName: 'Delete', + cellRenderer: cellRendererFactory((c, p) => { + const btnComponent = mount(Button, { + target: c.eGui, + props: { + btnClasses: 'w-12', + wrapperClasses: 'flex justify-end items-center h-full', + color: 'light', + size: 'sm', + variant: 'contained', + iconOnly: true, + startIcon: { icon: Trash2 }, + nonCaptureEvent: true + } + }) + return { + destroy: () => { + unmount(btnComponent) + }, + refresh(params) { + // + } + } + }), + cellRendererParams: { + onClick: (e) => { + onDelete?.(e) + } + }, + lockPosition: 'right', + editable: false, + flex: 0, + width: 100 + }) + } + + if (actions?.length) { + r.push({ + headerName: customActionsHeader ?? 'Actions', + cellRenderer: tableActionsFactory, + autoHeight: true, + cellStyle: { textAlign: 'center' }, + cellClass: 'grid-cell-centered', + lockPosition: 'right', + + ...(!wrapActions ? { minWidth: 130 * actions?.length } : {}) + }) + } + + return r.map((fields) => { + let cr = defaultCellRenderer(fields.cellRendererType) + return { + ...fields, + ...(cr ? { cellRenderer: cr } : {}) + } + }) +} + +export function validateColumnDefs(columnDefs: ColumnDef[]): { + isValid: boolean + errors: string[] +} { + let isValid = true + const errors: string[] = [] + + if (!Array.isArray(columnDefs)) { + return { isValid: false, errors: ['Column definitions must be an array.'] } + } + + // Validate each column definition + columnDefs.forEach((colDef, index) => { + let noField = !colDef.field || typeof colDef.field !== 'string' || colDef.field.trim() === '' + + if ( + (colDef.isidentity === ColumnIdentity.ByDefault || + colDef.isidentity === ColumnIdentity.Always) && + colDef.hideInsert == undefined + ) { + colDef.hideInsert = true + } + + // Check if 'field' property exists and is a non-empty string + if (noField && !(colDef.children && Array.isArray(colDef.children))) { + isValid = false + errors.push( + `Column at index ${index} is missing a valid 'field' property nor having any children.` + ) + } + + if (colDef.children && Array.isArray(colDef.children)) { + const { isValid: isChildrenValid, errors: childrenErrors } = validateColumnDefs( + colDef.children + ) + if (!isChildrenValid) { + isValid = false + errors.push(...childrenErrors.map((err) => `Error in children at index ${index}: ${err}`)) + } + } + }) + + return { isValid, errors } +} diff --git a/frontend/src/lib/components/common/CloseButton.svelte b/frontend/src/lib/components/common/CloseButton.svelte index 7bed98a57d..fea765e3ef 100644 --- a/frontend/src/lib/components/common/CloseButton.svelte +++ b/frontend/src/lib/components/common/CloseButton.svelte @@ -6,12 +6,13 @@ export let noBg = false export let small: boolean = false + export let Icon: any | undefined = undefined const dispatch = createEventDispatcher()