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}
+
{
+ richRender = !richRender
+ }}
+ >
+
+ {/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}
-
-
+
+
{
@@ -580,7 +579,6 @@
{$scale}%
= 100}
on:click={() => {
diff --git a/frontend/src/lib/components/table/AutoDataTable.svelte b/frontend/src/lib/components/table/AutoDataTable.svelte
new file mode 100644
index 0000000000..b314e0bfae
--- /dev/null
+++ b/frontend/src/lib/components/table/AutoDataTable.svelte
@@ -0,0 +1,406 @@
+
+
+
+
+
+
+
+
+
+ {#if selection.length > 0}
+
+ {pluralize(selection?.length ?? 1, 'item') + ' selected'}
+
+ {/if}
+ {#if hiddenColumns.length > 0}
+
+
+ {
+ hiddenColumns = []
+ }}
+ startIcon={{
+ icon: Columns
+ }}
+ >
+ Display hidden columns ({pluralize(hiddenColumns?.length ?? 1, 'column')})
+
+
+ {/if}
+
+
+ {
+ const csv = structuredObjects
+ .filter(({ _id }) => {
+ if (selection.length > 0) {
+ return selection.includes(_id)
+ } else {
+ return true
+ }
+ })
+ .map(({ rowData }) => Object.values(rowData).join(','))
+ .join('\n')
+
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.setAttribute('href', url)
+ link.setAttribute('download', 'data.csv')
+ link.style.visibility = 'hidden'
+ document.body.appendChild(link)
+ link.click()
+
+ document.body.removeChild(link)
+ }}
+ >
+ {#if selection.length > 0}
+ Download selected as CSV
+ {:else}
+ Download as CSV
+ {/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)}
+ {
+ hiddenColumns = hiddenColumns.filter((col) => col !== key)
+ }}
+ >
+
+
+ {:else}
+ {
+ hiddenColumns = [...hiddenColumns, key]
+ }}
+ >
+
+
+ {/if}
+ {#if isSortable(key)}
+ {#if activeSorting?.column === key}
+ {
+ activeSorting = {
+ column: key,
+ direction: activeSorting?.direction == 'asc' ? 'desc' : 'asc'
+ }
+ }}
+ disabled={hiddenColumns.includes(key)}
+ >
+ {#if activeSorting?.direction == 'asc'}
+
+ {:else}
+
+ {/if}
+
+ {:else}
+ {
+ activeSorting = {
+ column: key,
+ direction: activeSorting?.direction == 'asc' ? 'desc' : 'asc'
+ }
+ }}
+ disabled={hiddenColumns.includes(key)}
+ >
+
+
+ {/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}
-
- 25
- 50
- 100
-
- {/if}
- dispatch('previous')}
- disabled={currentPage === 1}
- startIcon={{ icon: ArrowLeftIcon }}
- >
- Previous
-
- {#if showNext}
+ {#if perPage !== undefined}
+
+ 5
+ 10
+ 25
+ 50
+ 100
+
+ {/if}
dispatch('next')}
- endIcon={{ icon: ArrowRightIcon }}
+ on:click={() => dispatch('previous')}
+ disabled={currentPage === 1}
+ startIcon={{ icon: ArrowLeftIcon }}
>
- Next
+ Previous
- {/if}
+ {#if showNext}
+ dispatch('next')}
+ endIcon={{ icon: ArrowRightIcon }}
+ >
+ Next
+
+ {/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}
-