feat: add universal search to object viewer

This commit is contained in:
Ruben Fiszel
2025-04-27 16:29:55 +02:00
parent c599857e4a
commit 72547437fe
6 changed files with 119 additions and 38 deletions
@@ -48,10 +48,10 @@
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
let hasAlreadyFailed = false
async function loadInputHistory() {
@@ -379,7 +379,8 @@
class="w-full h-full items-center text-xs font-normal grid grid-cols-8 gap-4 min-w-0"
>
<div class="">
<div class="rounded-full w-2 h-2 {i.success ? 'bg-green-400' : 'bg-red-400'}"></div>
<div class="rounded-full w-2 h-2 {i.success ? 'bg-green-400' : 'bg-red-400'}"
></div>
</div>
<div class="col-span-2 truncate" title={i.created_by}>
{i.created_by}
@@ -39,7 +39,7 @@
{value}
{placeholder}
rows="1"
class="resize-y duration-200 {inputClass}"
class="resize-y {inputClass}"
{...$$restProps}
on:input={handleInput}
on:keydown|stopPropagation
@@ -51,7 +51,7 @@
{type}
{value}
{placeholder}
class="duration-200 {(value ? '!pr-[26px] ' : '') + inputClass}"
class=" {(value ? '!pr-[26px] ' : '') + inputClass}"
{...$$restProps}
on:input={handleInput}
on:keydown|stopPropagation
@@ -63,7 +63,7 @@
<button
transition:fade|local={{ duration: 80 }}
class="absolute z-10 top-[9.5px] right-2 rounded-full p-0.5 text-primary bg-surface-secondary
duration-200 hover:bg-surface-hovr focus:bg-surface-hover {buttonClass}"
hover:bg-surface-hover focus:bg-surface-hover {buttonClass}"
aria-label="Clear"
on:click|preventDefault|stopPropagation={clear}
>
@@ -116,7 +116,7 @@
<AnimatedButton
animate={$propPickerConfig?.insertionMode == 'connect'}
baseRadius="4px"
wrapperClasses="prop-picker-inputs h-full w-full pt-2 !bg-surface "
wrapperClasses="prop-picker-inputs h-full w-full pt-1 !bg-surface "
marginWidth="3px"
ringColor={$propPickerConfig?.insertionMode == 'insert' ||
$propPickerConfig?.insertionMode == 'append'
@@ -2,11 +2,11 @@
import { copyToClipboard, truncate } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { computeKey } from './utils'
import { computeKey, keepByKeyOrValue } from './utils'
import { NEVER_TESTED_THIS_FAR } from '../flows/models'
import Portal from '$lib/components/Portal.svelte'
import { Button } from '$lib/components/common'
import { Download, PanelRightOpen, TriangleAlertIcon } from 'lucide-svelte'
import { Download, PanelRightOpen, Search, TriangleAlertIcon, X } from 'lucide-svelte'
import S3FilePicker from '../S3FilePicker.svelte'
import { workspaceStore } from '$lib/stores'
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
@@ -25,12 +25,37 @@
export let expandedEvenOnLevel0: string | undefined = undefined
export let connecting = false
let jsonFiltered = json
$: jsonFiltered = onJsonChange(json)
let search = ''
let searchOpen = false
function onJsonChange(json: any) {
return searchOpen && search ? keepByKeyOrValue(json, search) : json
}
$: search != undefined && searchOpen && onSearch()
let searchTimeout: NodeJS.Timeout | undefined = undefined
function onSearch() {
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
jsonFiltered = search ? keepByKeyOrValue(json, search) : json
}, 100)
}
let s3FileViewer: S3FilePicker
let hoveredKey: string | null = null
const collapsedSymbol = '...'
$: keys = ['object', 's3object'].includes(getTypeAsString(json)) ? Object.keys(json) : []
$: isArray = Array.isArray(json)
$: keys = ['object', 's3object'].includes(getTypeAsString(jsonFiltered))
? Object.keys(jsonFiltered)
: []
$: isArray = Array.isArray(jsonFiltered)
$: openBracket = isArray ? '[' : '{'
$: closeBracket = isArray ? ']' : '}'
@@ -77,11 +102,51 @@
$: keyLimit = isArray ? 5 : 100
$: fullyCollapsed = keys.length > 1 && collapsed
function clearSearch() {
searchOpen = false
jsonFiltered = json
search = ''
}
</script>
<Portal name="object-viewer">
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
</Portal>
{#if level == 0}
<div class="float-right">
{#if searchOpen}
<div class="px-1 relative">
<input
on:keydown|stopPropagation={(event) => {
if (event?.key === 'Escape') {
clearSearch()
}
}}
type="text"
class="!h-6 !text-2xs mt-0.5"
bind:value={search}
placeholder="Search..."
/>
<button
class="absolute right-2 top-1 rounded-full hover:bg-surface-hover focus:bg-surface-hover text-secondary p-0.5"
on:click={() => {
clearSearch()
}}><X size={12} /></button
>
</div>
{:else}
<Button
color="light"
size="xs3"
iconOnly
btnClasses="text-tertiary hover:text-primary"
startIcon={{ icon: Search }}
on:click={() => (searchOpen = true)}
></Button>
{/if}
</div>
{/if}
{#if keys.length > 0}
{#if !fullyCollapsed}
<span>
@@ -132,10 +197,10 @@
</AnimatedButton>
<span class="text-2xs -ml-0.5 text-tertiary">:</span>
{#if getTypeAsString(json[key]) === 'object'}
{#if getTypeAsString(jsonFiltered[key]) === 'object'}
<svelte:self
{connecting}
json={json[key]}
json={jsonFiltered[key]}
level={level + 1}
currentPath={computeFullKey(key, rawKey)}
{pureViewer}
@@ -152,25 +217,25 @@
<span
class="val inline text-left {pureViewer
? 'cursor-auto'
: ''} rounded pl-0.5 {getTypeAsString(json[key])}"
: ''} rounded pl-0.5 {getTypeAsString(jsonFiltered[key])}"
on:click={() => {
selectProp(key, json[key], true)
selectProp(key, jsonFiltered[key], true)
}}
title={JSON.stringify(json[key])}
title={JSON.stringify(jsonFiltered[key])}
>
{#if json[key] === NEVER_TESTED_THIS_FAR}
{#if jsonFiltered[key] === NEVER_TESTED_THIS_FAR}
<span class="text-2xs text-tertiary font-normal">
Test the flow to see a value
</span>
{:else if json[key] == undefined}
{:else if jsonFiltered[key] == undefined}
<span class="text-2xs">undefined</span>
{:else if json[key] == null}
{:else if jsonFiltered[key] == null}
<span class="text-2xs">null</span>
{:else if typeof json[key] == 'string'}
<span class="text-2xs">"{truncate(json[key], 200)}"</span>
{:else if typeof json[key] == 'number' && Number.isInteger(json[key]) && !Number.isSafeInteger(json[key])}
{:else if typeof jsonFiltered[key] == 'string'}
<span class="text-2xs">"{truncate(jsonFiltered[key], 200)}"</span>
{:else if typeof jsonFiltered[key] == 'number' && Number.isInteger(jsonFiltered[key]) && !Number.isSafeInteger(jsonFiltered[key])}
<span class="inline-flex flex-row gap-1 items-center text-2xs">
{truncate(JSON.stringify(json[key]), 200)}
{truncate(JSON.stringify(jsonFiltered[key]), 200)}
<Popover>
<TriangleAlertIcon size={14} class="text-yellow-500 mb-0.5" />
<svelte:fragment slot="text">
@@ -181,7 +246,7 @@
</span>
{:else}
<span class="text-2xs">
{truncate(JSON.stringify(json[key]), 200)}
{truncate(JSON.stringify(jsonFiltered[key]), 200)}
</span>
{/if}
</span>
@@ -198,20 +263,20 @@
{#if level == 0 && topBrackets}
<div class="flex">
<span class="text-tertiary">{closeBracket}</span>
{#if getTypeAsString(json) === 's3object'}
{#if getTypeAsString(jsonFiltered) === 's3object'}
<a
class="text-secondary underline font-semibold text-2xs whitespace-nowrap ml-1 w-fit"
href={`/api/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(
json?.s3 ?? ''
)}${json?.storage ? `&storage=${json.storage}` : ''}`}
download={json?.s3.split('/').pop() ?? 'unnamed_download.file'}
jsonFiltered?.s3 ?? ''
)}${jsonFiltered?.storage ? `&storage=${jsonFiltered.storage}` : ''}`}
download={jsonFiltered?.s3.split('/').pop() ?? 'unnamed_download.file'}
>
<span class="flex items-center gap-1"><Download size={12} />download</span>
</a>
<button
class="text-secondary underline text-2xs whitespace-nowrap ml-1"
on:click={() => {
s3FileViewer?.open?.(json)
s3FileViewer?.open?.(jsonFiltered)
}}
><span class="flex items-center gap-1"><PanelRightOpen size={12} />open preview</span>
</button>
@@ -240,7 +305,7 @@
{/if}
{:else if topBrackets}
<span class="text-primary">{openBracket}{closeBracket}</span>
{:else if json == undefined}
{:else if jsonFiltered == undefined}
<span class="text-tertiary text-2xs ml-2">undefined</span>
{:else}
<span class="text-tertiary text-2xs ml-2">No items</span>
@@ -197,7 +197,7 @@
</script>
<div class="flex flex-col h-full rounded">
<div class="px-1 py-1">
<div class="px-1 pb-1">
<ClearableInput bind:value={search} placeholder="Search prop..." />
</div>
<!-- <div
@@ -1,10 +1,18 @@
function filterByKey(obj: Object, key: string): any {
function filterObject(obj: any, key: string, filterValue: boolean): any {
if (typeof obj !== 'object' || obj === null) {
return undefined
if (!filterValue || obj === undefined || obj === null) {
return undefined
} else {
return obj.toString().includes(key) ? obj : undefined
}
} else if (Array.isArray(obj)) {
let a = obj
.map((k, o) =>
typeof o == 'object' ? filterByKey(o, key) : String(k - 1).includes(key) ? o : undefined
typeof o == 'object'
? filterObject(o, key, filterValue)
: String(k - 1).includes(key)
? o
: undefined
)
.filter((v) => v !== undefined)
if (a.length === 0) {
@@ -15,7 +23,7 @@ function filterByKey(obj: Object, key: string): any {
} else {
let o = Object.fromEntries(
Object.entries(obj)
.map(([k, v]) => (k.includes(key) ? [k, v] : [k, filterByKey(v, key)]))
.map(([k, v]) => (k.includes(key) ? [k, v] : [k, filterObject(v, key, filterValue)]))
.filter(([k, v]) => v !== undefined)
)
if (Object.keys(o).length === 0) {
@@ -29,7 +37,14 @@ export function keepByKey(json: Object | undefined, key: string): Object {
if (!json) {
return {}
}
return filterByKey(json, key) ?? {}
return filterObject(json, key, false) ?? {}
}
export function keepByKeyOrValue(json: Object | undefined, key: string): Object {
if (!json) {
return {}
}
return filterObject(json, key, true) ?? {}
}
// https://stackoverflow.com/questions/23377217/way-to-test-if-a-string-is-valid-identifier-name-in-javascript