fix(frontend): sync columnDefs + improve columnDefs management (#3632)

* fix(frontend): sync columnDefs + improve columnDefs management

* fix(frontend): sync columnDefs + improve columnDefs management

* fix(frontend): improve code quality

* fix(frontend): improve code quality

* fix(frontend): remove useless warning while the config is loading

* fix(frontend): Disable actions for database studio, since columnDefs are managed by the component

* feat(frontend): Fix Database studio

* feat(frontend): revert changes from Database Studio

* feat(frontend): fix DB Studio refresh

* fix(frontend): improve code quality

* fix(frontend): improve code quality

* fix(frontend): fix build

* fix(frontend): fix wording

* fix(frontend): fix aggrid
This commit is contained in:
Faton Ramadani
2024-05-01 10:25:48 +02:00
committed by GitHub
parent aa6204ff99
commit ca209e9c48
13 changed files with 322 additions and 239 deletions
@@ -201,7 +201,7 @@
}
}
function handleSyncRegion() {
async function handleSyncRegion() {
const gridItem = findGridItem($app, id)
if (!map || !gridItem) {
return
@@ -225,6 +225,8 @@
gridItem.data.configuration.longitude.value = center[0]
//@ts-ignore
gridItem.data.configuration.latitude.value = center[1]
$app = $app
}
}
</script>
@@ -111,7 +111,7 @@
}, 1000)
}
const { app, worldStore, mode, selectedComponent, runnableComponents } =
const { app, worldStore, mode, selectedComponent } =
getContext<AppViewerContext>('AppViewerContext')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
@@ -205,24 +205,6 @@
})
}
let isCallbackAdded = false
function addOnRecomputeCallback() {
$runnableComponents[id].cb = [
...$runnableComponents[id].cb,
() =>
new CancelablePromise(async (resolve) => {
await dbExplorerCount?.computeCount(true)
aggrid?.clearRows()
resolve()
})
]
isCallbackAdded = true
}
$: $runnableComponents[id]?.cb && !isCallbackAdded && addOnRecomputeCallback()
async function listTables() {
let resource = resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.resource
@@ -626,11 +608,19 @@
noInitialize
bind:runnableComponent
componentInput={input}
autoRefresh={true}
autoRefresh={false}
bind:loading
{render}
{id}
{outputs}
overrideCallback={() =>
new CancelablePromise(async (resolve) => {
await dbExplorerCount?.computeCount(true)
aggrid?.clearRows()
resolve()
})}
overrideAutoRefresh={true}
>
<div class="h-full" bind:clientHeight={componentContainerHeight}>
{#if !(hideSearch === true && hideInsert === true)}
@@ -678,6 +668,7 @@
containerHeight={componentContainerHeight - (buttonContainerHeight ?? 0)}
on:update={onUpdate}
on:delete={onDelete}
allowColumnDefsActions={false}
{actions}
/>
{/key}
@@ -1,13 +1,12 @@
<script lang="ts">
import { GridApi, createGrid, type IDatasource } from 'ag-grid-community'
import { isObject, sendUserToast } from '$lib/utils'
import { sendUserToast } from '$lib/utils'
import { createEventDispatcher, getContext } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../../types'
import type { TableAction, components } from '$lib/components/apps/editor/component'
import Alert from '$lib/components/common/alert/Alert.svelte'
import { deepEqual } from 'fast-equals'
import SyncColumnDefs from './SyncColumnDefs.svelte'
import 'ag-grid-community/styles/ag-grid.css'
import 'ag-grid-community/styles/ag-theme-alpine.css'
import { twMerge } from 'tailwind-merge'
@@ -21,7 +20,6 @@
import type { ColumnDef } from '../dbtable/utils'
import AppAggridTableActions from './AppAggridTableActions.svelte'
import Popover from '$lib/components/Popover.svelte'
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
export let id: string
export let customCss: ComponentCustomCSS<'aggridcomponent'> | undefined = undefined
@@ -36,6 +34,8 @@
export let outputs: Record<string, Output<any>>
export let allowDelete: boolean
export let actions: TableAction[] = []
export let result: any[] | undefined = undefined
export let allowColumnDefsActions: boolean = true
let inputs = {}
const context = getContext<AppViewerContext>('AppViewerContext')
@@ -43,32 +43,6 @@
let css = initCss($app.css?.aggridcomponent, customCss)
// let result: any[] | undefined = undefined
// $: result && setValues()
// let value: any[] = Array.isArray(result)
// ? (result as any[]).map((x, i) => ({ ...x, __index: i.toString() }))
// : [{ error: 'input was not an array' }]
// let loaded = false
// async function setValues() {
// value = Array.isArray(result)
// ? (result as any[]).map((x, i) => ({ ...x, __index: i.toString() }))
// : [{ error: 'input was not an array' }]
// if (api && loaded) {
// let selected = api.getSelectedNodes()
// if (selected && selected.length > 0 && resolvedConfig?.selectFirstRowByDefault != false) {
// let data = { ...selected[0].data }
// delete data['__index']
// outputs?.selectedRow?.set(data)
// }
// }
// if (!loaded) {
// loaded = true
// }
// }
let selectedRowIndex = -1
function toggleRow(row: any) {
@@ -182,7 +156,11 @@
})
})
function transformColumnDefs(columnDefs: any[]) {
function transformColumnDefs(columnDefs: any[] | undefined) {
if (!columnDefs) {
return []
}
const { isValid, errors } = validateColumnDefs(columnDefs)
if (!isValid) {
@@ -248,10 +226,17 @@
let firstRow: number = 0
let lastRow: number = 0
function validateColumnDefs(columnDefs: ColumnDef[]): { isValid: boolean; errors: string[] } {
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) => {
// Check if 'field' property exists and is a non-empty string
@@ -405,7 +390,7 @@
/>
{/each}
{#if Array.isArray(resolvedConfig.columnDefs) && resolvedConfig.columnDefs.every(isObject)}
<SyncColumnDefs {id} columnDefs={resolvedConfig.columnDefs} {result} {allowColumnDefsActions}>
<div
class={twMerge(
'flex flex-col h-full component-wrapper divide-y',
@@ -451,16 +436,7 @@
</div>
{/if}
</div>
{:else if resolvedConfig.columnDefs != undefined}
<Alert title="Parsing issues" type="error" size="xs">
The columnDefs should be an array of objects, received:
<pre class="overflow-auto">
{JSON.stringify(resolvedConfig.columnDefs)}
</pre>
</Alert>
{:else}
<Alert title="Parsing issues" type="error" size="xs">The columnDefs are undefined</Alert>
{/if}
</SyncColumnDefs>
<style>
.ag-theme-alpine {
@@ -27,6 +27,7 @@
export let render: boolean
export let customCss: ComponentCustomCSS<'aggridinfinitecomponent'> | undefined = undefined
export let actions: TableAction[] | undefined = undefined
let runnableComponent: RunnableComponent | undefined = undefined
function clear() {
@@ -196,9 +197,10 @@
{resolvedConfig}
{customCss}
{outputs}
allowDelete={false}
{result}
{actions}
allowDelete={false}
bind:this={aggrid}
/></div
>
/>
</div>
</RunnableWrapper>
@@ -14,10 +14,10 @@
import { initConfig, initOutput } from '$lib/components/apps/editor/appUtils'
import { components, type TableAction } from '$lib/components/apps/editor/component'
import Alert from '$lib/components/common/alert/Alert.svelte'
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
import { deepEqual } from 'fast-equals'
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
import SyncColumnDefs from './SyncColumnDefs.svelte'
import 'ag-grid-community/styles/ag-grid.css'
import './theme/windmill-theme.css'
@@ -449,145 +449,126 @@
bind:loading
hideRefreshButton={true}
>
{#if Array.isArray(value) && value.every(isObject)}
{#if Array.isArray(resolvedConfig.columnDefs) && resolvedConfig.columnDefs.every(isObject)}
<div
class={twMerge(
'flex flex-col h-full component-wrapper divide-y',
css?.container?.class,
'wm-aggrid-container'
)}
style={css?.container?.style}
bind:clientHeight
bind:clientWidth
>
{#if componentInput?.type === 'runnable' && componentInput.autoRefresh}
<div class="absolute top-2 right-2 z-50">
<RefreshButton {id} {loading} />
</div>
{/if}
<div
on:pointerdown|stopPropagation={() => {
$selectedComponent = [id]
}}
style:height="{clientHeight}px"
style:width="{clientWidth}px"
class="ag-theme-alpine relative"
class:ag-theme-alpine-dark={$darkMode}
>
{#key resolvedConfig?.pagination}
{#if loaded}
<div bind:this={eGui} style:height="100%" />
{:else}
<Loader2 class="animate-spin" />
{/if}
{/key}
<SyncColumnDefs {id} columnDefs={resolvedConfig.columnDefs} {result}>
<div
class={twMerge(
'flex flex-col h-full component-wrapper divide-y',
css?.container?.class,
'wm-aggrid-container'
)}
style={css?.container?.style}
bind:clientHeight
bind:clientWidth
>
{#if componentInput?.type === 'runnable' && componentInput.autoRefresh}
<div class="absolute top-2 right-2 z-50">
<RefreshButton {id} {loading} />
</div>
{#if resolvedConfig.footer}
<div class="flex gap-1 w-full justify-between items-center text-sm text-secondary/80 p-2">
<div>
<Popover>
<svelte:fragment slot="text">Download</svelte:fragment>
{/if}
<div
on:pointerdown|stopPropagation={() => {
$selectedComponent = [id]
}}
style:height="{clientHeight}px"
style:width="{clientWidth}px"
class="ag-theme-alpine relative"
class:ag-theme-alpine-dark={$darkMode}
>
{#key resolvedConfig?.pagination}
{#if loaded}
<div bind:this={eGui} style:height="100%" />
{:else}
<Loader2 class="animate-spin" />
{/if}
{/key}
</div>
{#if resolvedConfig.footer}
<div class="flex gap-1 w-full justify-between items-center text-sm text-secondary/80 p-2">
<div>
<Popover>
<svelte:fragment slot="text">Download</svelte:fragment>
<Button
startIcon={{ icon: Download }}
color="light"
size="xs2"
on:click={() => {
api?.exportDataAsCsv()
}}
iconOnly
/>
</Popover>
</div>
<div class="flex flex-row gap-1 items-center">
{#if resolvedConfig?.pagination}
{#key refreshCount}
<div class="text-xs mx-2 text-primary">
{(api?.paginationGetPageSize() ?? 0) * (api?.paginationGetCurrentPage() ?? 0) + 1}
to {Math.min(
api?.paginationGetRowCount() ?? 0,
((api?.paginationGetCurrentPage() ?? 0) + 1) *
(api?.paginationGetPageSize() ?? 0)
)}
of {api?.paginationGetRowCount()}
</div>
<Button
startIcon={{ icon: Download }}
iconOnly
startIcon={{ icon: SkipBack }}
color="light"
size="xs2"
disabled={api?.paginationGetCurrentPage() == 0}
on:click={() => {
api?.exportDataAsCsv()
api?.paginationGoToFirstPage()
refreshCount++
}}
iconOnly
/>
</Popover>
</div>
<div class="flex flex-row gap-1 items-center">
{#if resolvedConfig?.pagination}
{#key refreshCount}
<div class="text-xs mx-2 text-primary">
{(api?.paginationGetPageSize() ?? 0) * (api?.paginationGetCurrentPage() ?? 0) +
1}
to {Math.min(
api?.paginationGetRowCount() ?? 0,
((api?.paginationGetCurrentPage() ?? 0) + 1) *
(api?.paginationGetPageSize() ?? 0)
)}
of {api?.paginationGetRowCount()}
</div>
<Button
iconOnly
startIcon={{ icon: SkipBack }}
color="light"
size="xs2"
disabled={api?.paginationGetCurrentPage() == 0}
on:click={() => {
api?.paginationGoToFirstPage()
refreshCount++
}}
/>
<Button
iconOnly
startIcon={{ icon: ChevronLeft }}
color="light"
size="xs2"
disabled={api?.paginationGetCurrentPage() == 0}
on:click={() => {
api?.paginationGoToPreviousPage()
refreshCount++
}}
/>
<div class="text-xs mx-2 text-primary">
Page {(api?.paginationGetCurrentPage() ?? 0) + 1} of {api?.paginationGetTotalPages() ??
0}
</div>
<Button
iconOnly
startIcon={{ icon: ChevronRight }}
color="light"
size="xs2"
disabled={(api?.paginationGetCurrentPage() ?? 0) + 1 ==
api?.paginationGetTotalPages()}
on:click={() => {
api?.paginationGoToNextPage()
refreshCount++
}}
/>
<Button
iconOnly
startIcon={{ icon: SkipForward }}
color="light"
size="xs2"
disabled={(api?.paginationGetCurrentPage() ?? 0) + 1 ==
api?.paginationGetTotalPages()}
on:click={() => {
api?.paginationGoToLastPage()
refreshCount++
}}
/>
{/key}
{/if}
</div>
<Button
iconOnly
startIcon={{ icon: ChevronLeft }}
color="light"
size="xs2"
disabled={api?.paginationGetCurrentPage() == 0}
on:click={() => {
api?.paginationGoToPreviousPage()
refreshCount++
}}
/>
<div class="text-xs mx-2 text-primary">
Page {(api?.paginationGetCurrentPage() ?? 0) + 1} of {api?.paginationGetTotalPages() ??
0}
</div>
<Button
iconOnly
startIcon={{ icon: ChevronRight }}
color="light"
size="xs2"
disabled={(api?.paginationGetCurrentPage() ?? 0) + 1 ==
api?.paginationGetTotalPages()}
on:click={() => {
api?.paginationGoToNextPage()
refreshCount++
}}
/>
<Button
iconOnly
startIcon={{ icon: SkipForward }}
color="light"
size="xs2"
disabled={(api?.paginationGetCurrentPage() ?? 0) + 1 ==
api?.paginationGetTotalPages()}
on:click={() => {
api?.paginationGoToLastPage()
refreshCount++
}}
/>
{/key}
{/if}
</div>
{/if}
</div>
{:else if resolvedConfig.columnDefs != undefined}
<Alert title="Parsing issues" type="error" size="xs">
The columnDefs should be an array of objects, received:
<pre class="overflow-auto">
{JSON.stringify(resolvedConfig.columnDefs)}
</pre>
</Alert>
{:else}
<Alert title="Parsing issues" type="error" size="xs">The columnDefs are undefined</Alert>
{/if}
{:else if result != undefined}
<Alert title="Parsing issues" type="error" size="xs">
The result should be an array of objects, received:
<pre class="overflow-auto mt-2">
{JSON.stringify(result)}
</pre>
</Alert>
{/if}
</div>
{/if}
</div>
</SyncColumnDefs>
</RunnableWrapper>
<style>
@@ -0,0 +1,129 @@
<script lang="ts">
import { getContext, tick } from 'svelte'
import type { AppViewerContext } from '../../../types'
import { findGridItem } from '$lib/components/apps/editor/appUtils'
import Button from '$lib/components/common/button/Button.svelte'
import { RefreshCw } from 'lucide-svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Alert from '$lib/components/common/alert/Alert.svelte'
import { isObject } from '$lib/utils'
export let id: string
export let columnDefs: Array<any> = []
export let result: Array<any> | undefined = []
export let allowColumnDefsActions: boolean = true
const { app, mode, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
async function syncColumns() {
let gridItem = findGridItem($app, id)
if (gridItem && result) {
const keys = Object.keys(result[0] ?? {}) ?? []
if (gridItem.data.configuration.columnDefs.type === 'static') {
gridItem.data.configuration.columnDefs.value = keys.map((key) => ({
field: key,
headerName: key,
flex: 1
}))
} else if (gridItem.data.configuration.columnDefs.type === 'evalv2') {
gridItem.data.configuration.columnDefs.expr = JSON.stringify(
keys.map((key, index) => ({
field: key,
headerName: key,
flex: 1
}))
)
}
await updateConfiguration()
}
}
async function setEmptyColumns() {
let gridItem = findGridItem($app, id)
if (gridItem && gridItem.data.configuration.columnDefs.type === 'static') {
gridItem.data.configuration.columnDefs.value = []
await updateConfiguration()
} else if (gridItem && gridItem.data.configuration.columnDefs.type === 'evalv2') {
gridItem.data.configuration.columnDefs.expr = '[]'
await updateConfiguration()
}
}
async function updateConfiguration() {
$selectedComponent = undefined
await tick()
$selectedComponent = [id]
}
</script>
{#if Array.isArray(result) && result.every(isObject)}
{#if Array.isArray(columnDefs) && columnDefs.every(isObject)}
{#if $mode === 'dnd' && columnDefs?.length === 0 && result?.length > 0 && allowColumnDefsActions}
<div class="m-16">
<Alert title="Missing column definitions">
<div class="flex flex-col items-start gap-2">
<div class="text-xs"> No columns definition found. Columns found in data: </div>
<div class="text-sm flex flex-row gap-2">
{#each Object.keys(result[0] ?? []) as key}
<Badge small color="dark-gray">{key}</Badge>
{/each}
</div>
<div class="w-full flex fles-row justify-end">
<Button startIcon={{ icon: RefreshCw }} size="xs" color="dark" on:click={syncColumns}>
Sync columns definition
</Button>
</div>
</div>
</Alert>
</div>
{:else}
<slot />
{/if}
{:else if columnDefs !== undefined}
<div class="m-16">
<Alert title="Parsing issues" type="error" size="xs">
<div class="flex flex-col items-start gap-2">
The columnDefs should be an array of objects, received:
<pre class="overflow-auto">
{JSON.stringify(columnDefs)}
</pre>
{#if allowColumnDefsActions}
<div class="w-full flex fles-row justify-end">
<Button
startIcon={{ icon: RefreshCw }}
size="xs"
color="red"
on:click={setEmptyColumns}
>
Fix columns definitions
</Button>
</div>
{/if}
</div>
</Alert>
</div>
{:else}
<div class="m-16">
<Alert title="Parsing issues" type="error" size="xs">
<div class="flex flex-col items-start gap-2">
The columnDefs are undefined.
{#if allowColumnDefsActions}
<div class="w-full flex fles-row justify-end">
<Button
startIcon={{ icon: RefreshCw }}
size="xs"
color="red"
on:click={setEmptyColumns}
>
Fix columns definitions
</Button>
</div>
{/if}
</div>
</Alert>
</div>
{/if}
{/if}
@@ -54,6 +54,8 @@
export let hasChildrens: boolean
export let allowConcurentRequests = false
export let noInitialize = false
export let overrideCallback: (() => CancelablePromise<void>) | undefined = undefined
export let overrideAutoRefresh: boolean = false
const {
worldStore,
@@ -538,24 +540,28 @@
undefined
onMount(() => {
cancellableRun = (inlineScript?: InlineScript, setRunnableJobEditorPanel?: boolean) => {
let rejectCb: (err: Error) => void
let p: Partial<CancelablePromise<any>> = new Promise<any>((resolve, reject) => {
rejectCb = reject
donePromise = resolve
executeComponent(true, inlineScript, setRunnableJobEditorPanel).catch(reject)
})
p.cancel = () => {
resultJobLoader?.cancelJob()
loading = false
rejectCb(new Error('Canceled'))
}
if (overrideCallback) {
cancellableRun = overrideCallback
} else {
cancellableRun = (inlineScript?: InlineScript, setRunnableJobEditorPanel?: boolean) => {
let rejectCb: (err: Error) => void
let p: Partial<CancelablePromise<any>> = new Promise<any>((resolve, reject) => {
rejectCb = reject
donePromise = resolve
executeComponent(true, inlineScript, setRunnableJobEditorPanel).catch(reject)
})
p.cancel = () => {
resultJobLoader?.cancelJob()
loading = false
rejectCb(new Error('Canceled'))
}
return p as CancelablePromise<void>
return p as CancelablePromise<void>
}
}
$runnableComponents[id] = {
autoRefresh: autoRefresh && recomputableByRefreshButton,
autoRefresh: (autoRefresh && recomputableByRefreshButton) || overrideAutoRefresh,
refreshOnStart: refreshOnStart,
cb: [...($runnableComponents[id]?.cb ?? []), cancellableRun]
}
@@ -8,10 +8,13 @@
import RunnableComponent from './RunnableComponent.svelte'
import { sendUserToast } from '$lib/toast'
import InitializeComponent from './InitializeComponent.svelte'
import type { CancelablePromise } from '$lib/gen'
export let componentInput: AppInput | undefined
export let noInitialize = false
export let hideRefreshButton: boolean | undefined = undefined
export let overrideCallback: (() => CancelablePromise<void>) | undefined = undefined
export let overrideAutoRefresh: boolean = false
type SideEffectAction =
| {
@@ -255,6 +258,8 @@
hideRefreshButton={componentInput.hideRefreshButton ?? hideRefreshButton}
transformer={componentInput.transformer}
{autoRefresh}
{overrideCallback}
{overrideAutoRefresh}
recomputableByRefreshButton={componentInput.autoRefresh ?? true}
bind:recomputeOnInputChanged={componentInput.recomputeOnInputChanged}
{id}
@@ -266,8 +266,6 @@
)
)) as ([string, Record<string, any>] | undefined)[]
console.log('allTriggers', allTriggers)
delete policy.triggerables
policy.triggerables_v2 = Object.fromEntries(
allTriggers.filter(Boolean) as [string, Record<string, any>][]
@@ -45,7 +45,7 @@
{
label: 'Show style panel',
onClick: () => {
secondaryMenuLeft?.toggle(StylePanel, {})
secondaryMenuLeft?.toggle(StylePanel, { type: 'style' })
},
icon: Paintbrush2,
disabled: $secondaryMenuLeft.isOpen
@@ -744,11 +744,7 @@ const aggridinfinitecomponentconst = {
type: 'static',
fieldType: 'array',
subFieldType: 'ag-grid',
value: [
{ field: 'id', flex: 1 },
{ field: 'name', editable: true, flex: 1 },
{ field: 'age', flex: 1 }
]
value: []
} as StaticAppInput,
flex: {
type: 'static',
@@ -818,22 +814,11 @@ const aggridinfinitecomponentconst = {
}
},
componentInput: {
type: 'static',
fieldType: 'array',
subFieldType: 'object',
value: [
{
id: 1,
name: 'A cell with a long name',
age: 42
},
{
id: 2,
name: 'A briefer cell',
age: 84
}
]
} as StaticAppInput
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined
}
}
} as const
@@ -3670,6 +3655,12 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
value: false,
tooltip:
'When true, actions will wrap to the next line. Otherwise, the column will grow to fit the actions.'
},
footer: {
type: 'static',
fieldType: 'boolean',
value: true,
tooltip: 'Allow visible footer for pagination and download'
}
},
componentInput: undefined
@@ -34,7 +34,9 @@
class="flex justify-between {right ? '' : 'flex-row-reverse'} items-center gap-1 px-3 py-2"
>
<CloseButton on:close={() => secondaryMenu?.close()} />
<div class="text-xs font-bold"> Style Panel</div>
{#if $secondaryMenu?.props?.type === 'style'}
<div class="text-xs font-bold"> Style Panel</div>
{/if}
</div>
<div class="relative h-full overflow-y-auto">
{#if typeof $secondaryMenu.component === 'string'}
@@ -102,7 +102,7 @@
{#if value}
<div class="flex flex-col w-96 p-2 gap-4">
<span class="text-sm mb-2 leading-6 font-semibold">
Column definition
Column definitions
<Tooltip
documentationLink="https://www.ag-grid.com/javascript-data-grid/column-definitions/"
>