mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-08 00:02:27 +00:00
table small improvements
This commit is contained in:
@@ -27,6 +27,8 @@
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import MapResult from './MapResult.svelte'
|
||||
import Popover from './Popover.svelte'
|
||||
import DownloadCsv from './table/DownloadCsv.svelte'
|
||||
import { convertJsonToCsv } from './table/tableUtils'
|
||||
|
||||
export let result: any
|
||||
export let requireHtmlApproval = false
|
||||
@@ -110,6 +112,7 @@
|
||||
}
|
||||
|
||||
let is_render_all = false
|
||||
let download_as_csv = false
|
||||
function inferResultKind(result: any) {
|
||||
if (result == 'WINDMILL_TOO_BIG') {
|
||||
largeObject = true
|
||||
@@ -117,6 +120,7 @@
|
||||
}
|
||||
|
||||
if (result !== undefined) {
|
||||
download_as_csv = false
|
||||
if (typeof result === 'string') {
|
||||
length = 0
|
||||
largeObject = false
|
||||
@@ -136,12 +140,17 @@
|
||||
return keys[0] as 'png' | 'svg' | 'jpeg' | 'html' | 'gif'
|
||||
}
|
||||
|
||||
const tableLargeObject = roughSizeOfObject(result) > TABLE_MAX_SIZE
|
||||
let size = roughSizeOfObject(result)
|
||||
// Otherwise, check if the result is too large (10kb) for json
|
||||
largeObject = tableLargeObject || roughSizeOfObject(result) > DISPLAY_MAX_SIZE
|
||||
|
||||
if (tableLargeObject) {
|
||||
if (size > TABLE_MAX_SIZE) {
|
||||
largeObject = true
|
||||
if (Array.isArray(result) && isTableRowObject(result)) {
|
||||
download_as_csv = true
|
||||
}
|
||||
return 'json'
|
||||
} else {
|
||||
largeObject = size > DISPLAY_MAX_SIZE
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
@@ -629,6 +638,12 @@
|
||||
>
|
||||
Download {filename ? '' : 'as JSON'}
|
||||
</a>
|
||||
{#if download_as_csv}
|
||||
<DownloadCsv
|
||||
getContent={() => convertJsonToCsv(result)}
|
||||
customText="Download as CSV"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 mb-2">
|
||||
|
||||
@@ -5,51 +5,34 @@
|
||||
import DataTable from './DataTable.svelte'
|
||||
import Head from './Head.svelte'
|
||||
import Row from './Row.svelte'
|
||||
import { pluralize, sendUserToast } from '$lib/utils'
|
||||
import { pluralize } from '$lib/utils'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { isEmail, isLink } from './tableUtils'
|
||||
import {
|
||||
computeStructuredObjectsAndHeaders,
|
||||
convertJsonToCsv,
|
||||
isEmail,
|
||||
isLink
|
||||
} from './tableUtils'
|
||||
import type { BadgeColor } from '../common'
|
||||
import Popover from '../Popover.svelte'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { Parser } from '@json2csv/plainjs'
|
||||
import DownloadCsv from './DownloadCsv.svelte'
|
||||
export let objects: Array<Record<string, any>> = []
|
||||
|
||||
let currentPage = 1
|
||||
let perPage = 25
|
||||
let search: string = ''
|
||||
|
||||
$: structuredObjects = computeStructuredObjects(objects)
|
||||
|
||||
let structuredObjects: {
|
||||
_id: number
|
||||
rowData: Record<string, any>
|
||||
}[] = []
|
||||
let headers: string[] = []
|
||||
|
||||
function computeStructuredObjects(objects: Array<Record<string, any>>) {
|
||||
if (Array.isArray(objects)) {
|
||||
let nextId = 1
|
||||
$: recomputeObjectsAndHeaders(objects)
|
||||
|
||||
let hds: string[] = []
|
||||
let objs = objects.map((obj) => {
|
||||
let rowData = obj && typeof obj == 'object' ? obj : {}
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData = Object.fromEntries(rowData.map((x, i) => ['col' + i, x]))
|
||||
}
|
||||
let ks = Object.keys(rowData)
|
||||
ks.forEach((x) => {
|
||||
if (!hds.includes(x)) {
|
||||
hds.push(x)
|
||||
}
|
||||
})
|
||||
return {
|
||||
_id: nextId++,
|
||||
rowData
|
||||
}
|
||||
})
|
||||
headers = hds
|
||||
return objs
|
||||
} else {
|
||||
headers = []
|
||||
return []
|
||||
}
|
||||
function recomputeObjectsAndHeaders(objects: Array<Record<string, any>>) {
|
||||
;[headers, structuredObjects] = computeStructuredObjectsAndHeaders(objects)
|
||||
}
|
||||
|
||||
function adjustCurrentPage() {
|
||||
@@ -62,42 +45,52 @@
|
||||
|
||||
$: perPage && adjustCurrentPage()
|
||||
|
||||
$: data = structuredObjects
|
||||
.filter(
|
||||
({ rowData }) =>
|
||||
search == undefined ||
|
||||
search == '' ||
|
||||
Object.values(rowData).some((value) =>
|
||||
$: data = computeData(structuredObjects, activeSorting, search)
|
||||
|
||||
type ActiveSorting = {
|
||||
column: string
|
||||
direction: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
let activeSorting: ActiveSorting | undefined = undefined
|
||||
|
||||
function computeData(
|
||||
structuredObjects: Array<Record<string, any>>,
|
||||
activeSorting: ActiveSorting | undefined,
|
||||
search: string
|
||||
): Array<Record<string, any>> {
|
||||
let objects = structuredObjects
|
||||
if (search != undefined && search != '') {
|
||||
objects = objects.filter((obj) =>
|
||||
Object.values(obj.rowData).some((value) =>
|
||||
JSON.stringify(value).toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!activeSorting) return 0
|
||||
const valA = a.rowData[activeSorting.column]
|
||||
const valB = b.rowData[activeSorting.column]
|
||||
const isAsc = activeSorting.direction === 'asc'
|
||||
if (valA == undefined || valA == null) {
|
||||
return isAsc ? -1 : 1
|
||||
}
|
||||
if (valB == undefined || valB == null) {
|
||||
return isAsc ? 1 : -1
|
||||
}
|
||||
if (isAsc) {
|
||||
return valA > valB ? 1 : -1
|
||||
} else {
|
||||
return valA > valB ? -1 : 1
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
if (activeSorting) {
|
||||
objects = objects.sort((a, b) => {
|
||||
if (!activeSorting) return 0
|
||||
const valA = a.rowData[activeSorting.column]
|
||||
const valB = b.rowData[activeSorting.column]
|
||||
const isAsc = activeSorting.direction === 'asc'
|
||||
if (valA == undefined || valA == null) {
|
||||
return isAsc ? -1 : 1
|
||||
}
|
||||
if (valB == undefined || valB == null) {
|
||||
return isAsc ? 1 : -1
|
||||
}
|
||||
if (isAsc) {
|
||||
return valA > valB ? 1 : -1
|
||||
} else {
|
||||
return valA > valB ? -1 : 1
|
||||
}
|
||||
})
|
||||
}
|
||||
return objects
|
||||
}
|
||||
|
||||
$: slicedData = data.slice((currentPage - 1) * perPage, currentPage * perPage)
|
||||
|
||||
let activeSorting:
|
||||
| {
|
||||
column: string
|
||||
direction: 'asc' | 'desc'
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
let selection = [] as Array<number>
|
||||
|
||||
// Function to handle individual row checkbox change
|
||||
@@ -141,16 +134,6 @@
|
||||
// let typof = typeof value
|
||||
// return (value != undefined && typof === 'string') || typof === 'number' || typof === 'boolean'
|
||||
// }
|
||||
|
||||
function convertJsonToCsv(arr: Array<Record<string, any>>): string {
|
||||
try {
|
||||
const parser = new Parser({})
|
||||
const csv = parser.parse(arr)
|
||||
return csv
|
||||
} catch (err) {
|
||||
throw new Error('An error occured when generating CSV:' + err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
@@ -167,45 +150,18 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
startIcon={{ icon: Download }}
|
||||
on:click={() => {
|
||||
try {
|
||||
const csvContent = convertJsonToCsv(
|
||||
structuredObjects
|
||||
.filter(({ _id }) => {
|
||||
if (selection.length > 0) {
|
||||
return selection.includes(_id)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
.map((obj) => obj.rowData)
|
||||
)
|
||||
|
||||
const blob = new Blob([csvContent], { 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)
|
||||
} catch (err) {
|
||||
sendUserToast(err, true)
|
||||
}
|
||||
<DownloadCsv
|
||||
getContent={() => {
|
||||
return convertJsonToCsv(
|
||||
selection.length > 0
|
||||
? structuredObjects
|
||||
.filter(({ _id }) => selection.includes(_id))
|
||||
.map((obj) => obj.rowData)
|
||||
: objects
|
||||
)
|
||||
}}
|
||||
>
|
||||
{#if selection.length > 0}
|
||||
Download selected as CSV
|
||||
{:else}
|
||||
Download as CSV
|
||||
{/if}
|
||||
</Button>
|
||||
customText={selection.length > 0 ? 'Download selected as CSV' : undefined}
|
||||
/>
|
||||
<Dropdown
|
||||
items={() => {
|
||||
const actions = [
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Download } from 'lucide-svelte'
|
||||
import { Button } from '../common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
export let getContent: () => string
|
||||
export let customText: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
startIcon={{ icon: Download }}
|
||||
on:click={() => {
|
||||
try {
|
||||
const blob = new Blob([getContent()], { 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)
|
||||
} catch (err) {
|
||||
sendUserToast(err, true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{customText || 'Download as CSV'}
|
||||
</Button>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Parser } from '@json2csv/plainjs'
|
||||
|
||||
export function isLink(value: string) {
|
||||
return value?.startsWith('http://') || value?.startsWith('https://')
|
||||
}
|
||||
@@ -5,3 +7,46 @@ export function isLink(value: string) {
|
||||
export function isEmail(value: string) {
|
||||
return value?.includes('@')
|
||||
}
|
||||
|
||||
export function computeStructuredObjectsAndHeaders(objects: Array<Record<string, any>>): [
|
||||
string[],
|
||||
{
|
||||
_id: number
|
||||
rowData: Record<string, any>
|
||||
}[]
|
||||
] {
|
||||
if (Array.isArray(objects)) {
|
||||
let nextId = 1
|
||||
|
||||
let hds: string[] = []
|
||||
let objs = objects.map((obj) => {
|
||||
let rowData = obj && typeof obj == 'object' ? obj : {}
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData = Object.fromEntries(rowData.map((x, i) => ['col' + i, x]))
|
||||
}
|
||||
let ks = Object.keys(rowData)
|
||||
ks.forEach((x) => {
|
||||
if (!hds.includes(x)) {
|
||||
hds.push(x)
|
||||
}
|
||||
})
|
||||
return {
|
||||
_id: nextId++,
|
||||
rowData
|
||||
}
|
||||
})
|
||||
return [hds, objs]
|
||||
} else {
|
||||
return [[], []]
|
||||
}
|
||||
}
|
||||
|
||||
export function convertJsonToCsv(arr: Array<Record<string, any>>): string {
|
||||
try {
|
||||
const parser = new Parser({})
|
||||
const csv = parser.parse(arr)
|
||||
return csv
|
||||
} catch (err) {
|
||||
throw new Error('An error occured when generating CSV:' + err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user