mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 08:00:45 +00:00
a45c9ca525
* upgrade duckdb * basic ducklake works * ducklake works with custom db catalogs * fix: pwsh skip already installed modules outside of cache (#6037) * improve query performance of user stats * separate ducklake_catalog db * ducklake settings * DucklakeSettings frontend * Ducklake ws settings saved in backend * fetch ducklake catalog resource * Ducklake works with configured s3 storage * Ducklake as asset * ducklake asset icon * Fix duckdb array and object args not working properly (#6254) * Fix bug with comments in duckdb * Avoid multiple queries when doing ATTACH ducklake * trunc sig no longer needed now that comments are trimmed * cache DuckdbConnectionSettingsResponse * duplicated code * transform_attach_ducklake contributes to duckdb_connection_settings_cache * eliminate the need for used_storages * nit * cleaner management of the bigquery credentials file * DBManagerDrawer refactor to prepare for Ducklake * get ducklake schema * implement delete for ducklake * load column metadata for ducklake * Select query works for ducklake, basic db explorer works ! * duckdb count query * Support all db ops for ducklake * clean migrations * SQL repl for Ducklake * fix broken database studio * nit * assert function * Ducklake in Editor Bar * default ducklake syntax + allow extra args * DucklakeCatalogWizard UI * nit + remove extra $ * modal when databases do not exist * cannot be windmill * Ducklake works safely with instance database * Avoid sending instance db credentials on network * resource leak security * remove fetch_attach_db_conn_str * prevent instance pg password leak * hide asset usage count when not available * case unsensitivity duckdb * warnings * disable instance catalog * use shorthand syntax when inserting with EditorBar * Instance ducklake catalog is now safe to use * use safer argon2 pwd * update package json parsers * update package json * better msgs * tooltips * disable explore button until saved * nit * fix warnings * better ducklake_user password management * nit * Sanitize passwords from errors in ducklake * DisplayResult broken in job result * remove superadmin requirement to check databases_exist * duckdb_connection_settings_v2_inner * Ducklake works on agent worker (finally) * ci * #[allow(dead_code)] * fix openapi missing response * Separate +Database button for DuckDB in EditorBar * Fix dropdown in ducklake settings * Attempt to fix migration race condition in CI * update sqlx failing for some offline queries * avoid temp password for ducklake_user * nits * ducklake settings nits * update duckdb default script * fix sql repl resetting text on refresh * avoid pgcrypto extension --------- Co-authored-by: HugoCasa <hugo@casademont.ch> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
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)
|
|
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(() => {
|
|
sendUserToast('Error deleting row', 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>
|