mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 08:01:38 +00:00
feat: add universal search to object viewer
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user