mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
aa30fd252d
* SQL Query builders in Rust * Remove frontend sql scripts and substitute at execution * fix null value bug * Handle WM_INTERNAL_DB marker for apps deployed prior * Revert policy handling * Fix database studio empty string as where clause * check policy * Revert "check policy" This reverts commit3ea7899979. * Revert "Fix database studio empty string as where clause" This reverts commit432fc87915. * Revert * legacy comments * Move DDL queries to backend * tests * move bigquery bun scripts to backend * expand markers + other nits * fix: escape sql literals in query builders and async preview sql Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: quote all user-supplied identifiers in query builders to prevent SQL injection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: suppress dead_code warnings for deserialization-only fields and test-only helpers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct DDL test assertions and drop_table schema handling for non-schema DBs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * MySQL fix * Fix 0/1 bool * MySQL fix Yes/No casing * Better error toasts * Fix ms sql ntext cast * fix: quote table name in Snowflake SHOW PRIMARY KEYS query Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: quote schema and table in Snowflake SHOW IMPORTED KEYS query Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: quote BigQuery dataset name in metadata query Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove invalid + separator in MSSQL CONCAT for count query Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
210 lines
5.9 KiB
Svelte
210 lines
5.9 KiB
Svelte
<script lang="ts">
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { createGrid, type GridApi, type IDatasource } from 'ag-grid-community'
|
|
import { transformColumnDefs } from './apps/components/display/table/utils'
|
|
import DarkModeObserver from './DarkModeObserver.svelte'
|
|
import { Button } from './common'
|
|
import { Download } from 'lucide-svelte'
|
|
import Popover from './Popover.svelte'
|
|
import DebouncedInput from './apps/components/helpers/DebouncedInput.svelte'
|
|
import InsertRowDrawerButton from './apps/components/display/InsertRowDrawerButton.svelte'
|
|
import type { IDbTableOps } from './dbOps'
|
|
import { deepEqual } from 'fast-equals'
|
|
import 'ag-grid-community/styles/ag-grid.css'
|
|
import 'ag-grid-community/styles/ag-theme-alpine.css'
|
|
import '$lib/components/apps/components/display/table/theme/windmill-theme.css'
|
|
import { untrack } from 'svelte'
|
|
|
|
type Props = {
|
|
dbTableOps: IDbTableOps
|
|
}
|
|
let { dbTableOps }: Props = $props()
|
|
|
|
let [clientHeight, clientWidth, darkMode, firstRow, lastRow] = $state([0, 0, false, -1, -1])
|
|
let quicksearch = $state('')
|
|
let api: GridApi<any> | undefined = $state()
|
|
let eGui: HTMLDivElement | undefined = $state()
|
|
|
|
let datasource: IDatasource = {
|
|
getRows: async function (params) {
|
|
if (!$workspaceStore) return params.failCallback()
|
|
let lastRow = rowCount && rowCount <= params.endRow ? rowCount : -1
|
|
|
|
const items = await dbTableOps.getRows({
|
|
offset: params.startRow,
|
|
limit: params.endRow - params.startRow,
|
|
quicksearch: params.context.quicksearch,
|
|
order_by: params.sortModel?.[0]?.colId ?? dbTableOps.colDefs[0]?.field,
|
|
is_desc: params.sortModel?.[0]?.sort === 'desc'
|
|
})
|
|
|
|
if (items.length < params.endRow - params.startRow) lastRow = params.startRow + items.length
|
|
params.successCallback(items, lastRow)
|
|
}
|
|
}
|
|
let rowCount = $state(0)
|
|
let refreshCount = $state(0)
|
|
export const refresh = () => (refreshCount += 1)
|
|
|
|
$effect(() => eGui && untrack(() => mountGrid()))
|
|
function mountGrid() {
|
|
if (eGui && !api) {
|
|
createGrid(eGui, {
|
|
rowModelType: 'infinite',
|
|
pagination: false,
|
|
...(dbTableOps.onUpdate && {
|
|
defaultColDef: {
|
|
flex: 1,
|
|
minWidth: 150,
|
|
editable: true,
|
|
onCellValueChanged: (e) => {
|
|
if (!$workspaceStore) return
|
|
const colDef = e.colDef as unknown as { field: string; datatype: string }
|
|
dbTableOps
|
|
.onUpdate?.(
|
|
{ values: { ...(e.data as object), [colDef.field]: e.oldValue } },
|
|
colDef,
|
|
e.newValue
|
|
)
|
|
.then(() => {
|
|
sendUserToast('Value updated')
|
|
})
|
|
.catch(() => {
|
|
sendUserToast('Error updating value', true)
|
|
refresh?.()
|
|
})
|
|
}
|
|
}
|
|
}),
|
|
onViewportChanged: (e) => ([firstRow, lastRow] = [e.firstRow, e.lastRow]),
|
|
cacheBlockSize: 100,
|
|
cacheOverflowSize: 10,
|
|
maxBlocksInCache: 20,
|
|
suppressColumnMoveAnimation: true,
|
|
suppressDragLeaveHidesColumns: true,
|
|
onGridReady: (e) => {
|
|
api = e.api
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
let prevUpdateKey: any = undefined
|
|
$effect(() => {
|
|
if (!$workspaceStore || !api) return
|
|
const key = { quicksearch, colDefs: dbTableOps.colDefs, refreshCount }
|
|
if (deepEqual(key, prevUpdateKey)) return
|
|
prevUpdateKey = key
|
|
untrack(() => updateGrid())
|
|
})
|
|
function updateGrid() {
|
|
dbTableOps.getCount({ quicksearch }).then((result) => (rowCount = result))
|
|
|
|
api?.purgeInfiniteCache()
|
|
api?.updateGridOptions({
|
|
datasource,
|
|
columnDefs: transformColumnDefs({
|
|
columnDefs: dbTableOps.colDefs ?? [],
|
|
...(dbTableOps.onDelete && {
|
|
onDelete: (values) => {
|
|
if (!$workspaceStore) return
|
|
dbTableOps
|
|
.onDelete?.({ values })
|
|
.then(() => {
|
|
refresh?.()
|
|
sendUserToast('Row deleted')
|
|
})
|
|
.catch((e) => {
|
|
sendUserToast(`Error deleting row: ${e?.message ?? e}`, true)
|
|
})
|
|
}
|
|
})
|
|
}),
|
|
context: {
|
|
quicksearch
|
|
}
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<DarkModeObserver bind:darkMode />
|
|
|
|
<div class="h-full relative flex flex-col">
|
|
<div class="flex py-2 h-12 justify-between gap-4">
|
|
<DebouncedInput
|
|
class="w-full max-w-[300px]"
|
|
type="text"
|
|
bind:value={quicksearch}
|
|
placeholder="Search..."
|
|
/>
|
|
{#if dbTableOps.onInsert}
|
|
<InsertRowDrawerButton
|
|
columnDefs={dbTableOps.colDefs ?? []}
|
|
dbType={dbTableOps.dbType}
|
|
onInsert={(values) => {
|
|
if (!$workspaceStore) return
|
|
dbTableOps.onInsert?.({ values }).then((result) => {
|
|
refresh?.()
|
|
sendUserToast('Row inserted')
|
|
})
|
|
}}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
<div
|
|
class={'flex flex-col flex-1 component-wrapper divide-y wm-aggrid-container'}
|
|
bind:clientHeight
|
|
bind:clientWidth
|
|
>
|
|
<div
|
|
style:height="{clientHeight}px"
|
|
style:width="{clientWidth}px"
|
|
class="ag-theme-alpine"
|
|
class:ag-theme-alpine-dark={darkMode}
|
|
>
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
bind:this={eGui}
|
|
class={api ? '' : 'opacity-0'}
|
|
style:height="100%"
|
|
onkeydown={(e) => {
|
|
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)
|
|
}
|
|
}
|
|
}}
|
|
></div>
|
|
</div>
|
|
|
|
<div class="flex gap-1 w-full justify-between items-center text-xs text-primary p-2">
|
|
<div>
|
|
<Popover>
|
|
{#snippet text()}
|
|
Download
|
|
{/snippet}
|
|
<Button
|
|
startIcon={{ icon: Download }}
|
|
color="light"
|
|
size="xs2"
|
|
on:click={() => api?.exportDataAsCsv()}
|
|
iconOnly
|
|
/>
|
|
</Popover>
|
|
</div>
|
|
{#if rowCount}
|
|
{firstRow}{'->'}{lastRow + 1} of {rowCount} rows
|
|
{:else}
|
|
{firstRow}{'->'}{lastRow + 1}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|