From 78f80c8b0d2293cb2a38ed161eb62397b846e337 Mon Sep 17 00:00:00 2001 From: Faton Ramadani Date: Thu, 18 Jan 2024 17:26:46 +0100 Subject: [PATCH] feat(frontend): Rich table display (#3028) * feat(frontend): rich debug table * feat(frontend): rich debug table * feat(frontend): rich debug table * feat(frontend): wip * feat(frontend): table v0 * feat(frontend): fix layout audit page * feat(frontend): display rich result by default * feat(frontend): add selected rows * feat(frontend): add unique ids * feat(frontend): restore max-h * feat(frontend): md support + remove sorting on types that don't support comparaison * feat(frontend): fix actions * feat(frontend): fix md * feat(frontend): fix md --------- Co-authored-by: Ruben Fiszel --- frontend/package-lock.json | 8 +- frontend/package.json | 2 +- .../src/lib/components/DisplayResult.svelte | 95 ++-- .../components/apps/editor/AppEditor.svelte | 6 +- .../lib/components/table/AutoDataTable.svelte | 406 ++++++++++++++++++ .../src/lib/components/table/DataTable.svelte | 56 +-- frontend/src/lib/components/table/Row.svelte | 4 +- .../src/lib/components/table/tableUtils.ts | 7 + .../(root)/(logged)/run/[...run]/+page.svelte | 2 +- 9 files changed, 523 insertions(+), 63 deletions(-) create mode 100644 frontend/src/lib/components/table/AutoDataTable.svelte create mode 100644 frontend/src/lib/components/table/tableUtils.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 06bb7f1275..1eb7676586 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -102,7 +102,7 @@ "svelte-range-slider-pips": "^2.2.3", "svelte-splitpanes": "^0.8.0", "svelte2tsx": "^0.6.16", - "tailwindcss": "^3.3.2", + "tailwindcss": "^3.4.1", "tslib": "^2.6.1", "typescript": "^5.1.3", "vite": "^4.5.0", @@ -8835,9 +8835,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", - "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", diff --git a/frontend/package.json b/frontend/package.json index 925a7c929b..525824affe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,7 +62,7 @@ "svelte-range-slider-pips": "^2.2.3", "svelte-splitpanes": "^0.8.0", "svelte2tsx": "^0.6.16", - "tailwindcss": "^3.3.2", + "tailwindcss": "^3.4.1", "tslib": "^2.6.1", "typescript": "^5.1.3", "vite": "^4.5.0", diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index 1be2cad781..0507f3efa1 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -4,10 +4,12 @@ import TableCustom from './TableCustom.svelte' import { copyToClipboard, roughSizeOfObject, truncate } from '$lib/utils' import { Button, Drawer, DrawerContent } from './common' - import { ClipboardCopy, Download, Expand, PanelRightOpen } from 'lucide-svelte' + import { ClipboardCopy, Download, Expand, PanelRightOpen, Table2 } from 'lucide-svelte' import Portal from 'svelte-portal' import ObjectViewer from './propertyPicker/ObjectViewer.svelte' import S3FilePicker from './S3FilePicker.svelte' + import AutoDataTable from './table/AutoDataTable.svelte' + import Markdown from 'svelte-exmarkdown' export let result: any export let requireHtmlApproval = false @@ -32,6 +34,7 @@ | 's3object' | 's3object-list' | 'plain' + | 'markdown' | undefined $: resultKind = inferResultKind(result) @@ -93,9 +96,9 @@ return 'json' } - if (isRectangularArray(result)) { + if ((keys.length == 1 && keys[0] == 'table-row') || isRectangularArray(result)) { return 'table-row' - } else if (isObjectOfArray(result, keys)) { + } else if ((keys.length == 1 && keys[0] == 'table-col') || isObjectOfArray(result, keys)) { return 'table-col' } else if (keys.length == 1 && keys[0] == 'html') { return 'html' @@ -138,6 +141,8 @@ result.every((elt) => inferResultKind(elt) === 's3object') ) { return 's3object-list' + } else if (keys.length === 1 && (keys.includes('md') || keys.includes('markdown'))) { + return 'markdown' } } catch (err) {} } @@ -158,6 +163,42 @@ return obj.content } } + + function isArrayWithObjects(json) { + return ( + Array.isArray(json) && + json.length > 0 && + json.every((item) => typeof item === 'object' && Object.keys(item).length > 0) + ) + } + + $: isTableDisplay = isArrayWithObjects(result) + let richRender: boolean = true + + type InputObject = { [key: string]: number[] } + + function transform(input: InputObject): any[] { + const maxLength = Math.max(...Object.values(input).map((arr) => arr.length)) + const result: Array<{ + [key: string]: number | null + }> = [] + + for (let i = 0; i < maxLength; i++) { + const obj: { [key: string]: number | null } = {} + + for (const key of Object.keys(input)) { + if (i < input[key].length) { + obj[key] = input[key][i] + } else { + obj[key] = null + } + } + + result.push(obj) + } + + return result + }
@@ -178,36 +219,30 @@ > + {#if isTableDisplay} + + {/if}
{/if} {/if} - {#if !forceJson && resultKind == 'table-col'}
- {#each Object.keys(result) as col} -
-
- {col} -
- {#if Array.isArray(result[col])} - {#each result[col] as item} -
- {typeof item === 'string' ? item : JSON.stringify(item)} -
- {/each} - {/if} -
- {/each} -
- {:else if !forceJson && resultKind == 'table-row'}
+ {#if !forceJson && resultKind == 'table-col'} + {@const data = 'table-col' in result ? result['table-col'] : result} + + + {:else if !forceJson && resultKind == 'table-row'} + {@const data = 'table-row' in result ? result['table-row'] : result} +
- {#each asListOfList(result) as row} + {#each Array.isArray(asListOfList(data)) ? asListOfList(data) : [] as row} {#each row as v} {truncate(JSON.stringify(v), 200) ?? ''} @@ -337,6 +372,12 @@ {/each}
+ {:else if !forceJson && resultKind == 'markdown'} +
+ +
+ {:else if !forceJson && isTableDisplay && richRender} + {:else if largeObject} {#if typeof result == 'object' && 'filename' in result && 'file' in result}
-
-
+
+
+
+ {/if} +
+
+ + { + const actions = [ + { + displayName: 'Download JSON', + icon: Download, + action: () => { + const json = JSON.stringify(objects, null, 2) + + const blob = new Blob([json], { type: 'text/json;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.setAttribute('href', url) + link.setAttribute('download', 'data.json') + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + + document.body.removeChild(link) + } + } + ] + + if (hiddenColumns.length > 0) { + actions.push({ + displayName: 'Display hidden columns', + icon: EyeIcon, + action: () => { + hiddenColumns = [] + } + }) + } + + if (selection.length > 0) { + actions.push({ + displayName: 'Clear selection', + icon: Columns, + action: () => { + selection = [] + renderCount++ + } + }) + } + + return actions + }} + > + + + + +
+
+ {#key renderCount} + {#if data.length == 0} +
+
No data found
+
+ Try changing your search query +
+
+ {:else} + { + currentPage += 1 + }} + on:previous={() => { + currentPage -= 1 + }} + on:change={(event) => { + currentPage = event.detail + }} + showNext={currentPage * perPage < objects.length} + > + + + + + + {#each Object.keys(data[0].rowData ?? {}) ?? [] as key, index} + +
+ {key} + {#if hiddenColumns.includes(key)} + + {:else} + + {/if} + {#if isSortable(key)} + {#if activeSorting?.column === key} + + {:else} + + {/if} + {/if} +
+
+ {/each} + + + + {#each data as { _id, rowData }, index (index)} + + + handleCheckboxChange(_id)} + /> + + {#each Object.keys(rowData ?? {}) ?? [] as key, index} + {@const value = rowData[key]} + + {#if hiddenColumns.includes(key)} + ... + {:else if Array.isArray(value) && typeof value[0] === 'string'} +
+ {#each value as item, index} + + {item} + + {/each} +
+ {:else if Array.isArray(value)} +
+ {#each value as val} +
+ {JSON.stringify(val)} +
+ {/each} +
+ {:else if typeof value === 'string' && isEmail(value)} + + {value} + + {:else if typeof value === 'string' && isLink(value)} + + {value} + + {:else} + +
+ {value} +
+ {value} +
+ {/if} +
+ {/each} +
+ {/each} + +
+ {/if} + {/key} +
+
+ + diff --git a/frontend/src/lib/components/table/DataTable.svelte b/frontend/src/lib/components/table/DataTable.svelte index 1412980b41..42e8ee1e6e 100644 --- a/frontend/src/lib/components/table/DataTable.svelte +++ b/frontend/src/lib/components/table/DataTable.svelte @@ -28,41 +28,47 @@
- - -
+
+ + +
+
{#if paginated && !shouldHidePagination}
- Page: {currentPage} +
+ Page: {currentPage} - {#if perPage !== undefined} - - {/if} - - {#if showNext} + {#if perPage !== undefined} + + {/if} - {/if} + {#if showNext} + + {/if} +
{:else if shouldLoadMore}
diff --git a/frontend/src/lib/components/table/Row.svelte b/frontend/src/lib/components/table/Row.svelte index 9db10d1596..19f08bff40 100644 --- a/frontend/src/lib/components/table/Row.svelte +++ b/frontend/src/lib/components/table/Row.svelte @@ -4,6 +4,7 @@ export let hoverable: boolean = false export let selected: boolean = false + export let dividable: boolean = false const dispatch = createEventDispatcher() @@ -11,7 +12,8 @@ class={twMerge( hoverable ? 'hover:bg-surface-hover cursor-pointer' : '', selected ? 'bg-blue-50 dark:bg-blue-900/50' : '', - 'transition-all' + 'transition-all', + dividable ? 'divide-x' : '' )} on:click={() => { dispatch('click') diff --git a/frontend/src/lib/components/table/tableUtils.ts b/frontend/src/lib/components/table/tableUtils.ts new file mode 100644 index 0000000000..25d72ee014 --- /dev/null +++ b/frontend/src/lib/components/table/tableUtils.ts @@ -0,0 +1,7 @@ +export function isLink(value: string) { + return value?.startsWith('http://') || value?.startsWith('https://') +} + +export function isEmail(value: string) { + return value?.includes('@') +} diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index bc19d5595c..f8a68757f7 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -519,7 +519,7 @@ {#if job} -
+
{#if viewTab == 'logs'}