feat(frontend): Aggrid infinite (#3592)

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): done

* feat(frontend): done

* feat(frontend): done

* feat(frontend): fix initializing

* feat(frontend): update all refreshButtons

* feat(frontend): fix build
This commit is contained in:
Faton Ramadani
2024-04-24 09:32:28 +02:00
committed by GitHub
parent b3f3df0d01
commit 7a8ffbea46
19 changed files with 515 additions and 83 deletions
@@ -1,39 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { LoaderIcon, RefreshCw } from 'lucide-svelte'
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppViewerContext } from '../types'
import Popover from '$lib/components/Popover.svelte'
export let id: string
export let loading: boolean
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
</script>
<Popover>
<Button
startIcon={{
icon: loading ? LoaderIcon : RefreshCw,
classes: twMerge(
loading ? 'animate-spin text-blue-800' : '',
'transition-all text-gray-500 dark:text-white'
)
}}
color="light"
size="xs2"
btnClasses={twMerge(loading ? ' bg-blue-100 dark:bg-blue-400' : '', 'transition-all')}
on:click={() => {
$runnableComponents[id]?.cb?.map((cb) => cb())
}}
iconOnly
/>
<svelte:fragment slot="text">
{#if loading}
Refreshing...
{:else}
Refresh
{/if}
</svelte:fragment>
</Popover>
@@ -39,7 +39,7 @@
import { getSelectInput } from './queries/select'
import DebouncedInput from '../../helpers/DebouncedInput.svelte'
import { CancelablePromise } from '$lib/gen'
import RefreshButton from '$lib/components/apps/components/RefreshButton.svelte'
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -27,7 +27,9 @@
export let customCss: ComponentCustomCSS<'aggridcomponent'> | undefined = undefined
export let containerHeight: number | undefined = undefined
export let resolvedConfig: InitConfig<
(typeof components)['dbexplorercomponent']['initialData']['configuration']
| (typeof components)['dbexplorercomponent']['initialData']['configuration']
| (typeof components)['aggridinfinitecomponent']['initialData']['configuration']
| (typeof components)['aggridinfinitecomponentee']['initialData']['configuration']
>
export let datasource: IDatasource
export let state: any = undefined
@@ -236,8 +238,8 @@
return r
}
let firstRow = 0
let lastRow = 0
let firstRow: number = 0
let lastRow: number = 0
function validateColumnDefs(columnDefs: ColumnDef[]): { isValid: boolean; errors: string[] } {
let isValid = true
@@ -431,7 +433,11 @@
/>
</Popover>
{firstRow}{'->'}{lastRow + 1} of {datasource?.rowCount} rows
{#if datasource?.rowCount}
{firstRow}{'->'}{lastRow + 1} of {datasource?.rowCount} rows
{:else}
{firstRow}{'->'}{lastRow + 1}
{/if}
</div>
</div>
{:else if resolvedConfig.columnDefs != undefined}
@@ -0,0 +1,177 @@
<script lang="ts">
import type { IDatasource } from 'ag-grid-community'
import { getContext } from 'svelte'
import type { AppInput } from '../../../inputType'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../../types'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import { initConfig, initOutput } from '$lib/components/apps/editor/appUtils'
import { components, type TableAction } from '$lib/components/apps/editor/component'
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
import 'ag-grid-community/styles/ag-grid.css'
import './theme/windmill-theme.css'
import { initCss } from '$lib/components/apps/utils'
import ResolveStyle from '../../helpers/ResolveStyle.svelte'
import AppAggridExplorerTable from './AppAggridExplorerTable.svelte'
import type { RunnableComponent } from '../..'
import { getPrimaryKeys } from '../dbtable/utils'
import InitializeComponent from '../../helpers/InitializeComponent.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let initializing: boolean | undefined = undefined
export let render: boolean
export let customCss: ComponentCustomCSS<'aggridcomponent'> | undefined = undefined
export let actions: TableAction[] | undefined = undefined
let runnableComponent: RunnableComponent | undefined = undefined
const context = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = context
let css = initCss($app.css?.aggridcomponent, customCss)
let result: any[] | undefined = undefined
let loading: boolean = false
let resolvedConfig = initConfig(
components['aggridinfinitecomponent'].initialData.configuration,
configuration
)
let outputs = initOutput($worldStore, id, {
selectedRowIndex: 0,
selectedRow: {},
selectedRows: [] as any[],
result: [] as any[],
inputs: {},
loading: false,
page: 0,
newChange: { row: 0, column: '', value: undefined },
ready: undefined as boolean | undefined,
params: {
offset: 0,
limit: 10,
orderBy: resolvedConfig.columnDefs?.[0]?.field,
isDesc: false
}
})
let aggrid: AppAggridExplorerTable | undefined = undefined
function clear() {
if (componentInput?.type !== 'runnable') {
aggrid?.clearRows()
}
}
$: result && clear()
const datasource: IDatasource = {
rowCount: undefined,
getRows: async function (params) {
if (!render) {
return
}
const currentParams = {
offset: params.startRow,
limit: params.endRow - params.startRow,
orderBy: params.sortModel?.[0]?.colId ?? resolvedConfig.columnDefs?.[0]?.field,
isDesc: params.sortModel?.[0]?.sort === 'desc'
}
outputs.params.set(currentParams)
if (!runnableComponent && result) {
params.successCallback(result, result.length)
}
runnableComponent?.runComponent(undefined, undefined, undefined, currentParams, {
done: (items) => {
let lastRow = -1
if (datasource?.rowCount && datasource.rowCount <= params.endRow) {
lastRow = datasource.rowCount
}
if (items && Array.isArray(items)) {
let processedData = items.map((item) => {
let primaryKeys = getPrimaryKeys(resolvedConfig.columnDefs)
let o = {}
primaryKeys.forEach((pk) => {
o[pk] = item[pk]
})
item['__index'] = JSON.stringify(o)
return item
})
if (items.length < params.endRow - params.startRow) {
lastRow = params.startRow + items.length
}
datasource.rowCount = undefined
params.successCallback(processedData, lastRow)
} else {
params.failCallback()
}
},
cancel: () => {
params.failCallback()
},
error: () => {
params.failCallback()
}
})
}
}
</script>
{#each Object.keys(components['aggridcomponent'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
{key}
bind:resolvedConfig={resolvedConfig[key]}
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.tablecomponent}
/>
{/each}
<InitializeComponent {id} />
<RunnableWrapper
{outputs}
{componentInput}
{id}
bind:initializing
bind:result
bind:loading
bind:runnableComponent
{render}
autoRefresh={false}
allowConcurentRequests
>
<AppAggridExplorerTable
{id}
{datasource}
{resolvedConfig}
{customCss}
{outputs}
allowDelete={false}
{actions}
bind:this={aggrid}
/>
</RunnableWrapper>
@@ -0,0 +1,47 @@
<script context="module">
</script>
<script lang="ts">
import type { AppInput } from '$lib/components/apps/inputType'
import type { ComponentCustomCSS, RichConfigurations } from '$lib/components/apps/types'
import 'ag-grid-community/styles/ag-grid.css'
import 'ag-grid-community/styles/ag-theme-alpine.css'
import { Loader2 } from 'lucide-svelte'
import type { TableAction } from '$lib/components/apps/editor/component'
import AppAggridInfiniteTable from './AppAggridInfiniteTable.svelte'
export let id: string
export let license: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let initializing: boolean | undefined = undefined
export let render: boolean
export let customCss: ComponentCustomCSS<'aggridinfinitecomponentee'> | undefined = undefined
export let actions: TableAction[] = []
let loaded = false
async function load() {
await import('ag-grid-enterprise')
const { LicenseManager } = await import('ag-grid-enterprise')
LicenseManager.setLicenseKey(license)
loaded = true
}
load()
</script>
{#if loaded}
<AppAggridInfiniteTable
{id}
{componentInput}
{configuration}
{initializing}
{render}
{customCss}
{actions}
/>
{:else}
<Loader2 class="animate-spin" />
{/if}
@@ -17,7 +17,7 @@
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/RefreshButton.svelte'
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
import 'ag-grid-community/styles/ag-grid.css'
import './theme/windmill-theme.css'
@@ -467,8 +467,8 @@
{: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 class="overflow-auto mt-2">
{JSON.stringify(result)}
</pre>
</Alert>
{/if}
@@ -44,7 +44,7 @@
import { EyeIcon, Plug2 } from 'lucide-svelte'
import AppCell from './AppCell.svelte'
import sum from 'hash-sum'
import RefreshButton from '$lib/components/apps/components/RefreshButton.svelte'
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -1,21 +1,39 @@
<script lang="ts">
import { RefreshCw } from 'lucide-svelte'
import { Button } from '$lib/components/common'
import { LoaderIcon, RefreshCw } from 'lucide-svelte'
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import Popover from '$lib/components/Popover.svelte'
import type { AppViewerContext } from '../../types'
export let componentId: string
export let id: string
export let loading: boolean
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
async function refresh() {
await $runnableComponents[componentId]?.cb?.map((cb) => cb())
}
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
</script>
<button
on:pointerdown|preventDefault|stopPropagation
on:click|preventDefault|stopPropagation={refresh}
class="center-center p-1 rounded border bg-surface/60 hover:bg-surface-hover z-10"
>
<RefreshCw class={loading ? 'animate-spin' : ''} size={16} />
</button>
<Popover>
<Button
startIcon={{
icon: loading ? LoaderIcon : RefreshCw,
classes: twMerge(
loading ? 'animate-spin text-blue-800' : '',
'transition-all text-gray-500 dark:text-white'
)
}}
color="light"
size="xs2"
btnClasses={twMerge(loading ? ' bg-blue-100 dark:bg-blue-400' : '', 'transition-all')}
on:click={() => {
$runnableComponents[id]?.cb?.map((cb) => cb())
}}
iconOnly
/>
<svelte:fragment slot="text">
{#if loading}
Refreshing...
{:else}
Refresh
{/if}
</svelte:fragment>
</Popover>
@@ -20,11 +20,11 @@
} from '../../types'
import { computeGlobalContext, eval_like } from './eval'
import InputValue from './InputValue.svelte'
import RefreshButton from './RefreshButton.svelte'
import { selectId } from '../../editor/appUtils'
import ResultJobLoader from '$lib/components/ResultJobLoader.svelte'
import { userStore } from '$lib/stores'
import { get } from 'svelte/store'
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
// Component props
export let id: string
@@ -722,7 +722,7 @@
{/if}
{#if render && !initializing && autoRefresh === true && !hideRefreshButton}
<div class="flex absolute top-1 right-1 z-50 app-component-refresh-btn">
<RefreshButton {loading} componentId={id} />
<RefreshButton {loading} {id} />
</div>
{/if}
</div>
@@ -73,6 +73,8 @@
import AppDateSliderInput from '../../components/inputs/AppDateSliderInput.svelte'
import AppTimeInput from '../../components/inputs/AppTimeInput.svelte'
import AppDateTimeInput from '../../components/inputs/AppDateTimeInput.svelte'
import AppAggridInfiniteTable from '../../components/display/table/AppAggridInfiniteTable.svelte'
import AppAggridInfiniteTableEe from '../../components/display/table/AppAggridInfiniteTableEe.svelte'
export let component: AppComponent
export let selected: boolean
@@ -366,6 +368,27 @@
actions={component.actions ?? []}
{render}
/>
{:else if component.type === 'aggridinfinitecomponent'}
<AppAggridInfiniteTable
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
{render}
/>
{:else if component.type === 'aggridinfinitecomponentee'}
<AppAggridInfiniteTableEe
license={component.license}
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
{render}
/>
{:else if component.type === 'textcomponent'}
<AppText
id={component.id}
@@ -151,6 +151,16 @@ export type AggridComponentEe = BaseComponent<'aggridcomponentee'> & {
license: string
actions: TableAction[]
}
export type AggridInfiniteComponent = BaseComponent<'aggridinfinitecomponent'> & {
actions: TableAction[]
}
export type AggridInfiniteComponentEe = BaseComponent<'aggridinfinitecomponentee'> & {
actions: TableAction[]
license: string
}
export type DisplayComponent = BaseComponent<'displaycomponent'>
export type LogComponent = BaseComponent<'logcomponent'>
export type JobIdLogComponent = BaseComponent<'jobidlogcomponent'>
@@ -305,6 +315,8 @@ export type TypedComponent =
| DateSliderComponent
| TimeInputComponent
| DateTimeInputComponent
| AggridInfiniteComponent
| AggridInfiniteComponentEe
export type AppComponent = BaseAppComponent & TypedComponent
@@ -712,6 +724,101 @@ const aggridcomponentconst = {
}
} as const
const aggridinfinitecomponentconst = {
name: 'AgGrid Infinite Table',
icon: Table2,
documentationLink: `${documentationBaseUrl}/aggrid_infinite_table`,
dims: '3:10-6:10' as AppComponentDimensions,
customCss: {
container: { class: '', style: '' }
},
initialData: {
configuration: {
columnDefs: {
type: 'static',
fieldType: 'array',
subFieldType: 'ag-grid',
value: [
{ field: 'id', flex: 1 },
{ field: 'name', editable: true, flex: 1 },
{ field: 'age', flex: 1 }
]
} as StaticAppInput,
flex: {
type: 'static',
fieldType: 'boolean',
value: true,
tooltip: 'default col flex is 1 (see ag-grid docs)'
},
allEditable: {
type: 'static',
fieldType: 'boolean',
value: false,
hide: true,
tooltip: 'Configure all columns as Editable by users'
},
multipleSelectable: {
type: 'static',
fieldType: 'boolean',
value: false,
tooltip: 'Make multiple rows selectable at once'
},
rowMultiselectWithClick: {
type: 'static',
fieldType: 'boolean',
value: true,
tooltip: 'If multiple selectable, allow multiselect with click'
},
selectFirstRowByDefault: {
type: 'static',
fieldType: 'boolean',
value: true as boolean,
tooltip: 'Select the first row by default on start'
},
extraConfig: {
type: 'static',
fieldType: 'object',
value: {},
tooltip: 'any configuration that can be passed to ag-grid top level'
},
compactness: {
type: 'static',
fieldType: 'select',
value: 'normal',
selectOptions: ['normal', 'compact', 'comfortable'],
tooltip: 'Change the row height'
},
wrapActions: {
type: 'static',
fieldType: 'boolean',
value: false,
tooltip:
'When true, actions will wrap to the next line. Otherwise, the column will grow to fit the actions.'
}
},
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
}
} as const
const agchartscomponentconst = {
name: 'AgCharts',
icon: BarChart4,
@@ -1734,6 +1841,8 @@ This is a paragraph.
},
aggridcomponent: aggridcomponentconst,
aggridcomponentee: { ...aggridcomponentconst, name: 'AgGrid Table EE' },
aggridinfinitecomponent: aggridinfinitecomponentconst,
aggridinfinitecomponentee: { ...aggridinfinitecomponentconst, name: 'AgGrid Infinite EE' },
checkboxcomponent: {
name: 'Toggle',
icon: ToggleLeft,
@@ -5,13 +5,14 @@ export function defaultCode(component: string, language: string): string | undef
if (language == 'bun') {
lang = 'deno'
}
return DEFAULT_CODES[component]?.[lang]
}
export const DEFAULT_CODES: Partial<
Record<
AppComponent['type'],
Partial<Record<'deno' | 'python3' | 'go' | 'bash' | 'pgsql' | 'mysql', string>>
Partial<Record<'deno' | 'python3' | 'go' | 'bash' | 'pgsql' | 'mysql' | 'postgresql', string>>
>
> = {
tablecomponent: {
@@ -41,15 +42,7 @@ export const DEFAULT_CODES: Partial<
"name": "A briefer cell",
"age": 84
}
]`,
pgsql: `import { pgSql } from "npm:windmill-client@${__pkg__.version}";
type Postgresql = object
export async function main(db: Postgresql) {
const query = await pgSql(db)\`SELECT * FROM demo;\`;
return query.rows;
}`
]`
},
aggridcomponent: {
deno: `export async function main() {
@@ -99,6 +92,72 @@ export async function main(db: Postgresql) {
# if page0Invalid:
# raise Exception("first step invalid")
# elif ...
`
},
aggridinfinitecomponent: {
deno: `export async function main(offset: number, limit:number, orderBy: string, isDesc: boolean) {
return [
{
"id": 1,
"name": "A cell with a long name",
"age": 42
},
{
"id": 2,
"name": "A briefer cell",
"age": 84
}
]
}`,
python3: `def main(offset: int, limit: int, orderBy: str, isDesc: bool):
return [
{
"id": 1,
"name": "A cell with a long name",
"age": 42
},
{
"id": 2,
"name": "A briefer cell",
"age": 84
}
]`,
postgresql: `-- $1 limit
-- $2 offset
SELECT * FROM demo LIMIT $1::INT OFFSET $2::INT;
`
},
aggridinfinitecomponentee: {
deno: `export async function main(offset: number, limit:number, orderBy: string, isDesc: boolean) {
return [
{
"id": 1,
"name": "A cell with a long name",
"age": 42
},
{
"id": 2,
"name": "A briefer cell",
"age": 84
}
]
}`,
python3: `def main(offset: int, limit: int, orderBy: str, isDesc: bool):
return [
{
"id": 1,
"name": "A cell with a long name",
"age": 42
},
{
"id": 2,
"name": "A briefer cell",
"age": 84
}
]`,
postgresql: `-- $1 limit
-- $2 offset
SELECT * FROM demo LIMIT $1::INT OFFSET $2::INT;
`
},
textcomponent: {
@@ -77,7 +77,14 @@ const display: ComponentSet = {
const tables: ComponentSet = {
title: 'Tables',
components: ['tablecomponent', 'aggridcomponent', 'aggridcomponentee', 'dbexplorercomponent']
components: [
'tablecomponent',
'aggridcomponent',
'aggridcomponentee',
'dbexplorercomponent',
'aggridinfinitecomponent',
'aggridinfinitecomponentee'
]
} as const
const charts: ComponentSet = {
@@ -98,9 +98,11 @@ export function getComponentControl(type: keyof typeof components): Array<Compon
case 'drawercomponent':
return [open, close]
case 'aggridcomponent':
return [getAgGrid, setSelectedIndex]
case 'aggridcomponentee':
case 'aggridinfinitecomponent':
case 'aggridinfinitecomponentee':
return [getAgGrid, setSelectedIndex]
case 's3fileinputcomponent':
return [clearFiles]
case 'displaycomponent':
@@ -610,6 +610,8 @@ export const quickStyleProperties: Record<
},
aggridcomponent: {},
aggridcomponentee: {},
aggridinfinitecomponent: {},
aggridinfinitecomponentee: {},
buttoncomponent: {
button: buttonDefaultProps,
container: containerDefaultProps
@@ -93,12 +93,8 @@
if (componentOutputs.currentStepIndex) {
newFields['stepIndex'] = {
type: 'connected',
connection: {
componentId: id,
path: 'currentStepIndex'
},
value: componentOutputs.currentStepIndex.peak(),
type: 'evalv2',
expr: `${id}.currentStepIndex`,
fieldType: 'number'
}
}
@@ -315,6 +315,13 @@
recomputeOnInputChanged={componentSettings.item.data.componentInput
.recomputeOnInputChanged}
showOnDemandOnlyToggle
acceptSelf={component.type === 'aggridinfinitecomponent' ||
component.type === 'aggridinfinitecomponentee' ||
component.type === 'steppercomponent'}
overridenByComponent={component.type === 'aggridinfinitecomponent' ||
component.type === 'aggridinfinitecomponentee'
? ['offset', 'limit', 'orderBy', 'isDesc']
: []}
/>
</div>
{/if}
@@ -17,6 +17,7 @@
export let acceptSelf: boolean = false
export let recomputeOnInputChanged = true
export let showOnDemandOnlyToggle = false
export let overridenByComponent: string[] = []
$: finalInputSpecsConfiguration = inputSpecsConfiguration ?? inputSpecs
@@ -26,7 +27,14 @@
{#if inputSpecs}
<div class="w-full flex flex-col gap-4">
{#each Object.keys(finalInputSpecsConfiguration) as k}
{#if finalInputSpecsConfiguration[k]?.type == 'oneOf'}
{#if overridenByComponent.includes(k)}
<div>
<span class="text-xs font-semibold truncate text-primary">
{k}
</span>
<div class="text-tertiary text-xs">Managed by the component</div>
</div>
{:else if finalInputSpecsConfiguration[k]?.type == 'oneOf'}
<OneOfInputSpecsEditor
{acceptSelf}
key={k}
@@ -71,6 +79,16 @@
</div>{/if}
{/if}
{/each}
{#if overridenByComponent.length > 0}
{#each overridenByComponent.filter((item) => Object.keys(finalInputSpecsConfiguration).indexOf(item) < 0) as k}
<div>
<span class="text-xs font-semibold truncate text-primary">
{k}
</span>
<div class="text-tertiary text-xs">Managed by the component</div>
</div>
{/each}
{/if}
</div>
{:else}
<div class="text-tertiary text-sm">No inputs</div>
@@ -9,7 +9,7 @@
export let jobs: Job[] | undefined
export let user: string | null
export let label: string | null
export let label: string | null = null
export let folder: string | null
export let path: string | null
export let success: 'success' | 'failure' | 'running' | undefined = undefined