mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
feat(frontend): Table wizard (#2416)
* feat(frontend): add helper to configure AG Grid columns def * feat(frontend): add all options * feat(frontend): column definition helper * feat(frontend): add header name * feat(frontend): add presets * feat(frontend): wip * feat(frontend): rework preset * feat(frontend): rework preset * feat(frontend): table wizard * feat(frontend): wip * feat(frontend): wip * feat(frontend): ordering * feat(frontend): add sizing options * feat(frontend): add sync button * feat(frontend): add quick add column buttonsa
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
|
||||
export let type: 'text' | 'badge' | 'link' = 'text'
|
||||
export let value: any
|
||||
</script>
|
||||
|
||||
{#if type === 'badge'}
|
||||
<Badge>
|
||||
{value}
|
||||
</Badge>
|
||||
{:else if type === 'link'}
|
||||
<a href={value} class="underline" target="_blank">{value}</a>
|
||||
{:else}
|
||||
{value}
|
||||
{/if}
|
||||
@@ -37,6 +37,7 @@
|
||||
import { Popup } from '$lib/components/common'
|
||||
import ComponentOutputViewer from '$lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte'
|
||||
import { Plug2 } from 'lucide-svelte'
|
||||
import AppCell from './AppCell.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -147,10 +148,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function cellIsObject(x: (any) => any, props: any): boolean {
|
||||
return typeof x != 'string' && typeof x(props) == 'object'
|
||||
}
|
||||
|
||||
let filteredResult: Array<Record<string, any>> = []
|
||||
|
||||
function setFilteredResult() {
|
||||
@@ -250,6 +247,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayNameById(id: string) {
|
||||
const component = resolvedConfig.columnDefs.find((columnDef) => columnDef.field === id)
|
||||
return component?.headerName
|
||||
}
|
||||
|
||||
function safeVisibleCell<T>(row: Row<T>) {
|
||||
try {
|
||||
return row.getVisibleCells()
|
||||
@@ -259,6 +261,25 @@
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function updateTable(resolvedConfig) {
|
||||
$table.getAllLeafColumns().map((column) => {
|
||||
const columnConfig = resolvedConfig.columnDefs.find(
|
||||
// @ts-ignore
|
||||
(columnDef) => columnDef.field === column.columnDef.accessorKey
|
||||
)
|
||||
|
||||
if (columnConfig?.hideColumn === column.getIsVisible()) {
|
||||
column.toggleVisibility()
|
||||
}
|
||||
})
|
||||
|
||||
$table.setColumnOrder(() =>
|
||||
resolvedConfig.columnDefs.map((columnDef: { field: any }) => columnDef.field)
|
||||
)
|
||||
}
|
||||
|
||||
$: updateTable(resolvedConfig)
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['tablecomponent'].initialData.configuration) as key (key)}
|
||||
@@ -327,9 +348,12 @@
|
||||
{@const context = header?.getContext()}
|
||||
{#if context}
|
||||
{@const component = renderCell(header.column.columnDef.header, context)}
|
||||
{@const displayName = getDisplayNameById(header.id)}
|
||||
<th class="!p-0">
|
||||
<span class="block px-4 py-4 text-sm font-semibold border-b">
|
||||
{#if !header.isPlaceholder && component}
|
||||
{#if displayName}
|
||||
{displayName}
|
||||
{:else if !header.isPlaceholder && component}
|
||||
<svelte:component this={component} />
|
||||
{/if}
|
||||
</span>
|
||||
@@ -363,18 +387,20 @@
|
||||
{#if cell?.column?.columnDef?.cell}
|
||||
{@const context = cell?.getContext()}
|
||||
{#if context}
|
||||
{@const component = renderCell(cell.column.columnDef.cell, context)}
|
||||
<td
|
||||
on:keydown={() => toggleRow(row)}
|
||||
on:click={() => toggleRow(row)}
|
||||
class="p-4 whitespace-pre-wrap truncate text-xs text-primary"
|
||||
style={'width: ' + cell.column.getSize() + 'px'}
|
||||
>
|
||||
{#if typeof cell.column.columnDef.cell != 'string' && cellIsObject(cell.column.columnDef.cell, context)}
|
||||
{JSON.stringify(cell.column.columnDef.cell(context), null, 4)}
|
||||
{:else if component != undefined}
|
||||
<svelte:component this={component} />
|
||||
{/if}
|
||||
<AppCell
|
||||
type={resolvedConfig.columnDefs.find(
|
||||
// TS types are wrong here
|
||||
// @ts-ignore
|
||||
(c) => c.field === cell.column.columnDef.accessorKey
|
||||
)?.type ?? 'text'}
|
||||
value={cell.getValue()}
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -90,6 +90,7 @@ export type PieChartComponent = BaseComponent<'piechartcomponent'>
|
||||
export type ChartJsComponent = BaseComponent<'chartjscomponent'>
|
||||
|
||||
export type ScatterChartComponent = BaseComponent<'scatterchartcomponent'>
|
||||
|
||||
export type TableComponent = BaseComponent<'tablecomponent'> & {
|
||||
actionButtons: (BaseAppComponent & ButtonComponent & GridItem)[]
|
||||
}
|
||||
@@ -1387,6 +1388,12 @@ This is a paragraph.
|
||||
},
|
||||
initialData: {
|
||||
configuration: {
|
||||
columnDefs: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'table-column',
|
||||
value: [{ field: 'id' }, { field: 'name' }, { field: 'age' }]
|
||||
} as StaticAppInput,
|
||||
search: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
|
||||
+25
-1
@@ -9,6 +9,7 @@
|
||||
import { dndzone, SOURCES, TRIGGERS } from 'svelte-dnd-action'
|
||||
import { generateRandomString, pluralize } from '$lib/utils'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import QuickAddColumn from './QuickAddColumn.svelte'
|
||||
|
||||
const flipDurationMs = 200
|
||||
|
||||
@@ -47,6 +48,8 @@
|
||||
value.push(selectOptions[0])
|
||||
} else if (subFieldType === 'ag-grid') {
|
||||
value.push({ field: 'newField', editable: true, flex: 1 })
|
||||
} else if (subFieldType === 'table-column') {
|
||||
value.push({ field: 'newColumn', headerName: 'New column', type: 'text' })
|
||||
}
|
||||
} else {
|
||||
value.push('')
|
||||
@@ -132,7 +135,7 @@
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="text-xs text-tertiary font-semibold">{pluralize(items.length, 'item')}</div>
|
||||
|
||||
{#if subFieldType === 'ag-grid'}
|
||||
{#if subFieldType === 'ag-grid' || subFieldType === 'table-column'}
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'Raw'
|
||||
@@ -164,7 +167,9 @@
|
||||
bind:value={item.value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between flex-col items-center">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
tabindex={dragDisabled ? 0 : -1}
|
||||
class="w-4 h-4 cursor-move"
|
||||
@@ -190,4 +195,23 @@
|
||||
<Button size="xs" color="light" startIcon={{ icon: faPlus }} on:click={() => addElementByType()}>
|
||||
Add
|
||||
</Button>
|
||||
|
||||
{#if subFieldType === 'table-column'}
|
||||
<QuickAddColumn
|
||||
columns={componentInput.value?.map((item) => item.field)}
|
||||
on:add={({ detail }) => {
|
||||
if (!componentInput.value) componentInput.value = []
|
||||
|
||||
componentInput.value.push({ field: detail, headerName: detail, type: 'text' })
|
||||
componentInput = componentInput
|
||||
|
||||
if (componentInput.value) {
|
||||
items.push({
|
||||
value: componentInput.value[componentInput.value.length - 1],
|
||||
id: generateRandomString()
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
|
||||
export let columns: string[] = []
|
||||
|
||||
let remainingColumns: string[] = []
|
||||
|
||||
const { worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let result = []
|
||||
|
||||
function subscribeToAllOutputs(observableOutputs: Record<string, Output<any>> | undefined) {
|
||||
if (observableOutputs) {
|
||||
Object.entries(observableOutputs).forEach(([k, output]) => {
|
||||
output?.subscribe(
|
||||
{
|
||||
id: 'alloutputs' + $selectedComponent?.[0] + '-' + k,
|
||||
next: (value) => {
|
||||
if (k === 'result') {
|
||||
result = value
|
||||
}
|
||||
}
|
||||
},
|
||||
result
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateRemainingColumns(result: any[], columns: string[]) {
|
||||
if (result.length > 0) {
|
||||
const allKeysSet: Set<string> = result.reduce((acc, obj) => {
|
||||
Object.keys(obj).forEach((key) => acc.add(key))
|
||||
return acc
|
||||
}, new Set<string>())
|
||||
|
||||
remainingColumns = Array.from(allKeysSet).filter((x: string) => !columns.includes(x))
|
||||
}
|
||||
}
|
||||
|
||||
$: $selectedComponent?.[0] &&
|
||||
subscribeToAllOutputs($worldStore?.outputsById?.[$selectedComponent?.[0]])
|
||||
$: updateRemainingColumns(result, columns)
|
||||
</script>
|
||||
|
||||
{#if remainingColumns.length > 0}
|
||||
<div class="text-xs font-semibold">Quick add </div>
|
||||
|
||||
<div class="flex flex-row gap-2 items-center flex-wrap">
|
||||
{#each remainingColumns as column}
|
||||
<Button on:click={() => dispatch('add', column)} size="xs2" color="light" variant="border">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Plus size="12" />
|
||||
{column}
|
||||
</div>
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
+22
@@ -14,6 +14,7 @@
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Settings } from 'lucide-svelte'
|
||||
import AgGridWizard from '$lib/components/wizards/AgGridWizard.svelte'
|
||||
import TableColumnWizard from '$lib/components/wizards/TableColumnWizard.svelte'
|
||||
|
||||
export let componentInput: StaticInput<any> | undefined
|
||||
export let fieldType: InputType | undefined = undefined
|
||||
@@ -139,6 +140,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if fieldType === 'table-column'}
|
||||
<div class="flex flex-row rounded-md bg-surface items-center h-full">
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
|
||||
bind:value={componentInput.value.field}
|
||||
placeholder="Field"
|
||||
/>
|
||||
<div class="absolute top-1 right-1">
|
||||
<TableColumnWizard bind:column={componentInput.value}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button color="light" size="xs2" nonCaptureEvent={true}>
|
||||
<div class="flex flex-row items-center gap-2 text-xs font-normal">
|
||||
<Settings size={16} />
|
||||
</div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</TableColumnWizard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<textarea
|
||||
|
||||
@@ -23,6 +23,7 @@ export type InputType =
|
||||
| 'tab-select'
|
||||
| 'schema'
|
||||
| 'ag-grid'
|
||||
| 'table-column'
|
||||
|
||||
// Connection to an output of another component
|
||||
// defined by the id of the component and the path of the output
|
||||
@@ -187,6 +188,7 @@ export type AppInput =
|
||||
| AppInputSpec<'array', object[], 'tab-select'>
|
||||
| AppInputSpec<'schema', object>
|
||||
| AppInputSpec<'array', object[], 'ag-grid'>
|
||||
| AppInputSpec<'array', object[], 'table-column'>
|
||||
|
||||
export type RowAppInput = Extract<AppInput, { type: 'row' }>
|
||||
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { Popup } from '../common'
|
||||
import Label from '../Label.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
export let column: {
|
||||
headerName: string
|
||||
hideColumn: boolean
|
||||
type: 'text' | 'badge' | 'link'
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{
|
||||
strategy: 'fixed',
|
||||
placement: 'left-end',
|
||||
middleware: [offset(8), flip(), shift()]
|
||||
}}
|
||||
containerClasses="border rounded-lg shadow-lg bg-surface p-4"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<slot name="trigger" />
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-96 p-2 gap-4">
|
||||
<span class="text-sm mb-2 leading-6 font-semibold">
|
||||
Table Column
|
||||
<Tooltip documentationLink="https://www.ag-grid.com/javascript-data-grid/column-definitions/">
|
||||
Column definitions are used to define columns in ag-Grid.
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
<Label label="Header name">
|
||||
<input placeholder="header name" bind:value={column.headerName} />
|
||||
</Label>
|
||||
|
||||
<Label label="Show column">
|
||||
<Toggle
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
options={{ right: 'Hide column' }}
|
||||
bind:checked={column.hideColumn}
|
||||
size="xs"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Type">
|
||||
<select bind:value={column.type}>
|
||||
<option value="text">Text</option>
|
||||
<option value="badge">Badge</option>
|
||||
<option value="link">Link</option>
|
||||
</select>
|
||||
</Label>
|
||||
</div>
|
||||
</Popup>
|
||||
Reference in New Issue
Block a user