Skip to content

Commit

Permalink
feat(frontend): allows to specify column order for rich display (#3709)
Browse files Browse the repository at this point in the history
* feat(frontend): allows to specify column order for rich display

* patch: rename table-cell to table-row-object

* fix: nested table row object
  • Loading branch information
HugoCasa committed May 13, 2024
1 parent 7937053 commit c883db3
Showing 1 changed file with 116 additions and 71 deletions.
187 changes: 116 additions & 71 deletions frontend/src/lib/components/DisplayResult.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script lang="ts">
import { Highlight } from 'svelte-highlight'
import { json } from 'svelte-highlight/languages'
import TableCustom from './TableCustom.svelte'
import { copyToClipboard, roughSizeOfObject, truncate } from '$lib/utils'
import { copyToClipboard, roughSizeOfObject } from '$lib/utils'
import { Button, Drawer, DrawerContent } from './common'
import {
ClipboardCopy,
Expand Down Expand Up @@ -43,6 +42,7 @@
| 'json'
| 'table-col'
| 'table-row'
| 'table-row-object'
| 'html'
| 'png'
| 'file'
Expand All @@ -65,30 +65,14 @@
let enableHtml = false
let s3FileDisplayRawMode = false
function isRectangularArray(obj: any) {
if (!Array.isArray(obj) || obj.length == 0) {
return false
}
if (
!Object.values(obj)
.map(Array.isArray)
.reduce((a, b) => a && b)
) {
return false
}
let innerSize = obj[0].length
return Object.values(obj)
.map((x: any) => x.length == innerSize)
.reduce((a, b) => a && b)
function isArrayOfArrays(result: any): boolean {
return Array.isArray(result) && result.every((x) => Array.isArray(x))
}
function asListOfList(obj: any): ArrayLike<ArrayLike<any>> {
return obj as ArrayLike<ArrayLike<any>>
}
function isObjectOfArray(result: any[], keys: string[]): boolean {
return keys.map((k) => Array.isArray(result[k])).reduce((a, b) => a && b)
function isObjectOfArrays(result: any[], keys: string[]): boolean {
return (
!Array.isArray(result) && keys.map((k) => Array.isArray(result[k])).reduce((a, b) => a && b)
)
}
let largeObject: boolean | undefined = undefined
Expand Down Expand Up @@ -131,15 +115,12 @@
if (keys.length != 0) {
if (Array.isArray(result) && result.every((elt) => inferResultKind(elt) === 's3object')) {
return 's3object-list'
} else if (isRectangularArray(result)) {
return 'table-col'
} else if (keys.length == 1 && keys[0] == 'table-row') {
return 'table-row'
} else if (
(keys.length == 1 && keys[0] == 'table-col') ||
isObjectOfArray(result, keys)
) {
} else if (keys.length == 1 && keys[0] == 'table-col') {
return 'table-col'
} else if (keys.length == 1 && keys[0] == 'table-row-object') {
return 'table-row-object'
} else if (keys.length == 1 && keys[0] == 'html') {
return 'html'
} else if (keys.length == 1 && keys[0] == 'map') {
Expand Down Expand Up @@ -175,10 +156,16 @@
keys.includes('approvalPage')
) {
return 'approval'
} else if (keys.length === 1 && keys.includes('s3')) {
} else if (keys.length === 1 && keys.includes('s3') && typeof result.s3 === 'string') {
return 's3object'
} else if (keys.length === 1 && (keys.includes('md') || keys.includes('markdown'))) {
return 'markdown'
} else if (isArrayOfArrays(result)) {
return 'table-row'
} else if (isObjectOfArrays(result, keys)) {
return 'table-col'
} else if (isArrayOfObjects(result)) {
return 'table-row-object'
}
}
} catch (err) {}
Expand All @@ -204,23 +191,65 @@
}
}
function isArrayWithObjects(json) {
function isArrayOfObjects(json) {
// check array of objects (with possible a first row of headers)
return (
Array.isArray(json) &&
json.length > 0 &&
json.every(
(json.every(
(item) =>
item && typeof item === 'object' && Object.keys(item).length > 0 && !Array.isArray(item)
)
) ||
(Array.isArray(json[0]) &&
json[0].every((item) => typeof item === 'string') &&
json
.slice(1)
.every(
(item) =>
item &&
typeof item === 'object' &&
Object.keys(item).length > 0 &&
!Array.isArray(item)
)))
)
}
$: isTableDisplay = isArrayWithObjects(result)
let richRender: boolean = !forceJson
function handleArrayOfObjectsHeaders(json: any) {
// handle possible a first row of headers
if (
Array.isArray(json) &&
json.length > 0 &&
Array.isArray(json[0]) &&
json[0].every((item) => typeof item === 'string') &&
json
.slice(1)
.every(
(item) =>
item && typeof item === 'object' && Object.keys(item).length > 0 && !Array.isArray(item)
)
) {
const headers = json[0]
const rows = json.slice(1)
const result = rows.map((row) => {
const obj: { [key: string]: string } = {}
for (const header of headers) {
obj[header] = row[header]
}
return obj
})
return result
}
return json
}
type InputObject = { [key: string]: number[] }
function transform(input: InputObject): any[] {
function objectOfArraysToObjects(input: InputObject): any[] {
const maxLength = Math.max(...Object.values(input).map((arr) => arr.length))
const result: Array<{
[key: string]: number | null
Expand All @@ -243,14 +272,51 @@
return result
}
function arrayOfRowsToObjects(input: any) {
if (Array.isArray(input) && input.length > 0) {
// handle possible first row of headers
if (
input.length > 1 &&
Array.isArray(input[0]) &&
input[0].every((item) => typeof item === 'string')
) {
const headers = input[0]
const rows = input.slice(1)
return rows.map((row) => {
const obj: { [key: string]: string } = {}
for (let i = 0; i < headers.length; i++) {
obj[headers[i]] = row[i]
}
return obj
})
} else {
return input
}
}
return []
}
let globalForceJson: boolean = false
</script>

{#if is_render_all}
<div class="flex flex-col w-full gap-6">
{#if !noControls}
<div class="mb-2 text-tertiary text-sm">
as JSON&nbsp;<input class="windmillapp" type="checkbox" bind:checked={globalForceJson} />
<div class="text-tertiary text-sm">
<ToggleButtonGroup
class="h-6"
selected={globalForceJson ? 'json' : 'pretty'}
on:selected={(ev) => {
globalForceJson = ev.detail === 'json'
}}
>
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} />

<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} />
</ToggleButtonGroup>
</div>
{/if}
{#each result['render_all'] as res}
Expand All @@ -275,29 +341,19 @@
>
{#if result != undefined && length != undefined && largeObject != undefined}
<div class="flex justify-between items-center w-full">
<div class="text-tertiary text-sm flex items-center">
{#if (resultKind && typeof result == 'object' && !['json', 's3object', 's3object-list', 'table-col', 'table-row'].includes(resultKind) && !hideAsJson) || isTableDisplay}
<div class="text-tertiary text-sm">
{#if !hideAsJson && !['json', 's3object'].includes(resultKind ?? '') && typeof result === 'object'}
<ToggleButtonGroup
class="h-6"
selected={isTableDisplay
? richRender
? 'table'
: 'json'
: forceJson
? 'json'
: 'pretty'}
selected={forceJson ? 'json' : resultKind?.startsWith('table-') ? 'table' : 'pretty'}
on:selected={(ev) => {
if (isTableDisplay) {
richRender = ev.detail === 'table'
}
forceJson = ev.detail === 'json'
}}
>
{#if resultKind && !['json', 's3object', 's3object-list', 'table-col', 'table-row'].includes(resultKind) && !hideAsJson}
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} />
{/if}
{#if isTableDisplay}
{#if ['table-col', 'table-row', 'table-row-object'].includes(resultKind ?? '')}
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} />
{:else}
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} />
{/if}
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} />
</ToggleButtonGroup>
Expand Down Expand Up @@ -327,22 +383,13 @@
</div>
</div>{#if !forceJson && resultKind == 'table-col'}
{@const data = 'table-col' in result ? result['table-col'] : result}
<AutoDataTable objects={transform(data)} />
<AutoDataTable objects={objectOfArraysToObjects(data)} />
{:else if !forceJson && resultKind == 'table-row'}
{@const data = 'table-row' in result ? result['table-row'] : result}
<div class="grid grid-flow-col-dense border border-gray-200 dark:border-gray-600">
<TableCustom>
<tbody slot="body">
{#each Array.isArray(asListOfList(data)) ? asListOfList(data) : [] as row}
<tr>
{#each row as v}
<td class="!text-xs">{truncate(JSON.stringify(v), 200) ?? ''}</td>
{/each}
</tr>
{/each}
</tbody>
</TableCustom>
</div>
<AutoDataTable objects={arrayOfRowsToObjects(data)} />
{:else if !forceJson && resultKind == 'table-row-object'}
{@const data = 'table-row-object' in result ? result['table-row-object'] : result}
<AutoDataTable objects={handleArrayOfObjectsHeaders(data)} />
{:else if !forceJson && resultKind == 'html'}
<div class="h-full">
{#if !requireHtmlApproval || enableHtml}
Expand Down Expand Up @@ -537,8 +584,6 @@
<div class="prose-xs dark:prose-invert !list-disc !list-outside">
<Markdown md={result?.md ?? result?.markdown} />
</div>
{:else if !forceJson && isTableDisplay && richRender}
<AutoDataTable objects={result} />
{:else if largeObject}
{#if result && typeof result == 'object' && 'file' in result}
<div
Expand Down

0 comments on commit c883db3

Please sign in to comment.