feat: add search by args on input history directly
This commit is contained in:
@@ -10697,6 +10697,7 @@ paths:
|
||||
- $ref: "#/components/parameters/RunnableTypeQuery"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- name: include_preview
|
||||
in: query
|
||||
schema:
|
||||
|
||||
@@ -118,6 +118,7 @@ pub struct CompletedJobMini {
|
||||
#[derive(Deserialize)]
|
||||
struct GetInputHistory {
|
||||
include_preview: Option<bool>,
|
||||
args: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_input_history(
|
||||
@@ -132,11 +133,18 @@ async fn get_input_history(
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let args_query = if let Some(args) = &g.args {
|
||||
format!("and args @> '{}'", args)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let sql = &format!(
|
||||
"select id, v2_job.created_at, created_by, 'null'::jsonb as args, status = 'success' as success from v2_job JOIN v2_job_completed USING (id) \
|
||||
where {} = $1 and kind = any($2) and v2_job.workspace_id = $3 AND v2_job_completed.status != 'skipped' \
|
||||
where {} = $1 and kind = any($2) {args_query} and v2_job.workspace_id = $3 AND v2_job_completed.status != 'skipped' \
|
||||
order by v2_job.created_at desc limit $4 offset $5",
|
||||
r.runnable_type.column_name()
|
||||
r.runnable_type.column_name(),
|
||||
|
||||
);
|
||||
|
||||
let query = sqlx::query_as::<_, CompletedJobMini>(sql);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
export let showAuthor = false
|
||||
export let placement: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end' = 'top-end'
|
||||
export let limitPayloadSize = false
|
||||
export let searchArgs: Record<string, any> | undefined = undefined
|
||||
|
||||
let historicList: HistoricList | undefined = undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -93,7 +94,7 @@
|
||||
concurrencyKey={null}
|
||||
tag={null}
|
||||
success="running"
|
||||
argFilter={undefined}
|
||||
argFilter={searchArgs ? JSON.stringify(searchArgs) : undefined}
|
||||
bind:loading
|
||||
syncQueuedRunsCount={false}
|
||||
refreshRate={10000}
|
||||
@@ -146,6 +147,7 @@
|
||||
{showAuthor}
|
||||
{placement}
|
||||
{limitPayloadSize}
|
||||
{searchArgs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let showAuthor = false
|
||||
export let placement: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end' = 'bottom-start'
|
||||
export let limitPayloadSize = false
|
||||
export let searchArgs: Record<string, any> | undefined = undefined
|
||||
|
||||
let infiniteList: InfiniteList | undefined = undefined
|
||||
let loadInputsPageFn: ((page: number, perPage: number) => Promise<any>) | undefined = undefined
|
||||
@@ -37,7 +38,8 @@
|
||||
runnableType,
|
||||
page,
|
||||
perPage,
|
||||
includePreview: true
|
||||
includePreview: true,
|
||||
args: searchArgs ? JSON.stringify(searchArgs) : undefined
|
||||
})
|
||||
|
||||
const inputsWithPayload = await Promise.all(
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import SavedInputsPicker from '$lib/components/SavedInputsPicker.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
|
||||
import { type RunnableType } from '$lib/gen/types.gen.js'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import SaveInputsButton from '$lib/components/SaveInputsButton.svelte'
|
||||
import { Button } from './common'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { ExternalLink, Search } from 'lucide-svelte'
|
||||
import { Popover } from './meltComponents'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import MultiSelect from './multiselect/MultiSelectWrapper.svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let scriptHash: string | null = null
|
||||
@@ -15,7 +18,7 @@
|
||||
export let flowPath: string | null = null
|
||||
export let inputSelected: 'saved' | 'history' | undefined = undefined
|
||||
export let jsonView: boolean = false
|
||||
|
||||
export let schema: any
|
||||
// Are the current Inputs valid and able to be saved?
|
||||
export let isValid: boolean
|
||||
export let args: object
|
||||
@@ -51,6 +54,19 @@
|
||||
dispatch('selected_args', savedArgs)
|
||||
}
|
||||
}
|
||||
|
||||
let searchArgs = {}
|
||||
let appliedSearchArgs = {}
|
||||
|
||||
let searchArgsFields = [] as string[]
|
||||
|
||||
$: emptySearchArgs = Object.keys(appliedSearchArgs).length === 0
|
||||
|
||||
$: filteredSchema = {
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(schema.properties).filter(([key]) => searchArgsFields.includes(key))
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-w-[300px] h-full flex flex-col">
|
||||
@@ -92,17 +108,92 @@
|
||||
<Pane class="px-4 py-4 h-full">
|
||||
<Section label="History" wrapperClass="h-full" small={true}>
|
||||
<svelte:fragment slot="action">
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
btnClasses="!text-tertiary"
|
||||
endIcon={{ icon: ExternalLink }}
|
||||
on:click={() => {
|
||||
window.open(`/runs/${runnableId}`, '_blank')
|
||||
}}
|
||||
>
|
||||
All runs
|
||||
</Button>
|
||||
<div class="flex space-x-2">
|
||||
<Popover
|
||||
class="w-fit"
|
||||
usePointerDownOutside
|
||||
closeOnOtherPopoverOpen
|
||||
on:click={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
floatingConfig={{ placement: 'top-end' }}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
variant="contained"
|
||||
size="xs2"
|
||||
color={emptySearchArgs ? 'light' : 'dark'}
|
||||
nonCaptureEvent
|
||||
title="Search history"
|
||||
iconOnly={emptySearchArgs}
|
||||
endIcon={{ icon: Search }}
|
||||
>
|
||||
{emptySearchArgs ? '' : 'Active filters'}
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<div id="multi-select-search" />
|
||||
<div class="p-2 overflow-auto max-h-[400px] min-h-[300px] w-[400px]">
|
||||
<div class="flex items-center flex-wrap gap-x-2 justify-between">
|
||||
<div class="text-sm text-secondary">Search by args</div>
|
||||
<div class="flex flex-wrap gap-x-2">
|
||||
<Button
|
||||
on:click={async () => {
|
||||
appliedSearchArgs = structuredClone(searchArgs)
|
||||
await tick()
|
||||
historicInputs?.refresh()
|
||||
}}
|
||||
endIcon={{ icon: Search }}
|
||||
variant="contained"
|
||||
size="xs2"
|
||||
color="dark">Search</Button
|
||||
>
|
||||
{#if !emptySearchArgs}
|
||||
<Button
|
||||
on:click={async () => {
|
||||
searchArgs = {}
|
||||
appliedSearchArgs = {}
|
||||
await tick()
|
||||
historicInputs?.refresh()
|
||||
}}
|
||||
variant="contained"
|
||||
size="xs2"
|
||||
color="light">Reset filters</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<MultiSelect
|
||||
topPlacement
|
||||
target="#multi-select-search"
|
||||
placeholder="arg fields to filter on"
|
||||
items={Object.keys(schema.properties)}
|
||||
bind:value={searchArgsFields}
|
||||
/>
|
||||
</div>
|
||||
{#key filteredSchema}
|
||||
<SchemaForm shouldHideNoInputs schema={filteredSchema} bind:args={searchArgs} />
|
||||
{/key}
|
||||
{#if searchArgsFields.length === 0}
|
||||
<div class="text-secondary text-sm my-8 mx-auto">No filters</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
btnClasses="!text-tertiary"
|
||||
endIcon={{ icon: ExternalLink }}
|
||||
on:click={() => {
|
||||
window.open(`/runs/${runnableId}`, '_blank')
|
||||
}}
|
||||
>
|
||||
All runs
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<HistoricInputs
|
||||
bind:this={historicInputs}
|
||||
@@ -112,6 +203,7 @@
|
||||
if (e.detail) savedInputsPicker?.resetSelected()
|
||||
selectArgs(e.detail?.args, e.detail ? 'history' : undefined)
|
||||
}}
|
||||
searchArgs={emptySearchArgs ? undefined : appliedSearchArgs}
|
||||
showAuthor
|
||||
placement="top-end"
|
||||
/>
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelect.svelte'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
|
||||
export let items: any[]
|
||||
export let value: string[] | undefined = [] as string[]
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let target: string | HTMLElement | undefined = undefined
|
||||
export let topPlacement = false
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
strategy: 'absolute',
|
||||
placement: topPlacement ? 'top-start' : 'bottom-start',
|
||||
middleware: [offset(5), flip(), shift()]
|
||||
})
|
||||
|
||||
export let items: any[]
|
||||
export let value: string[] | undefined = [] as string[]
|
||||
let outerDiv: HTMLDivElement | undefined = undefined
|
||||
let portalRef: HTMLDivElement | undefined = undefined
|
||||
|
||||
@@ -56,6 +60,7 @@
|
||||
--sms-selected-bg={darkMode ? '#c7d2fe' : '#e0e7ff'}
|
||||
--sms-selected-text-color={darkMode ? '#312e81' : '#3730a3'}
|
||||
bind:selected={value}
|
||||
{placeholder}
|
||||
options={items}
|
||||
on:close={() => {
|
||||
open = false
|
||||
@@ -80,7 +85,7 @@
|
||||
</div>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
<Portal name="multi-select">
|
||||
<Portal {target} name="multi-select">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
@@ -566,6 +566,7 @@
|
||||
<svelte:fragment slot="save_inputs">
|
||||
<SavedInputsV2
|
||||
bind:this={savedInputsV2}
|
||||
schema={flow?.schema}
|
||||
{jsonView}
|
||||
flowPath={flow?.path}
|
||||
{isValid}
|
||||
|
||||
@@ -724,6 +724,7 @@
|
||||
<svelte:fragment slot="save_inputs">
|
||||
{#if args}
|
||||
<SavedInputsV2
|
||||
schema={script?.schema}
|
||||
bind:this={savedInputsV2}
|
||||
scriptPath={script?.path}
|
||||
scriptHash={topHash}
|
||||
|
||||
Reference in New Issue
Block a user