diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6f44eed791..8aed409689 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -3174,6 +3174,8 @@ paths: - $ref: "#/components/parameters/JobKinds" - $ref: "#/components/parameters/Suspended" - $ref: "#/components/parameters/Running" + - $ref: "#/components/parameters/ArgsFilter" + - $ref: "#/components/parameters/ResultFilter" responses: "200": description: All available queued jobs @@ -3202,6 +3204,8 @@ paths: - $ref: "#/components/parameters/CreatedAfter" - $ref: "#/components/parameters/Success" - $ref: "#/components/parameters/JobKinds" + - $ref: "#/components/parameters/ArgsFilter" + - $ref: "#/components/parameters/ResultFilter" - name: is_skipped description: is the job skipped in: query @@ -3238,6 +3242,8 @@ paths: - $ref: "#/components/parameters/CreatedBefore" - $ref: "#/components/parameters/CreatedAfter" - $ref: "#/components/parameters/JobKinds" + - $ref: "#/components/parameters/ArgsFilter" + - $ref: "#/components/parameters/ResultFilter" - name: is_skipped description: is the job skipped in: query @@ -4646,6 +4652,19 @@ components: in: query schema: type: boolean + ArgsFilter: + name: args + description: filter on jobs containing those args as a json subset (@> in postgres) + in: query + schema: + type: string + + ResultFilter: + name: result + description: filter on jobs containing those result as a json subset (@> in postgres) + in: query + schema: + type: string After: name: after description: filter on created after (exclusive) timestamp diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index a34297fcf1..ad396b7f5c 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -334,6 +334,8 @@ pub struct ListQueueQuery { pub order_desc: Option, pub job_kinds: Option, pub suspended: Option, + // filter by matching a subset of the args using base64 encoded json subset + pub args: Option, } fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder { @@ -376,6 +378,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq sqlb.and_where_eq("suspend", 0); } } + if let Some(jk) = &lq.job_kinds { sqlb.and_where_in( "job_kind", @@ -383,6 +386,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq ); } + if let Some(args) = &lq.args { + sqlb.and_where("args @> ?".bind(&args.replace("'", "''"))); + } + sqlb } @@ -462,6 +469,7 @@ async fn list_jobs( order_desc: Some(true), job_kinds: lq.job_kinds, suspended: lq.suspended, + args: lq.args, }, &[ "'QueuedJob' as typ", @@ -1136,15 +1144,15 @@ where struct InPayload { payload: Option, } - - fn decode_payload>(t: T) -> anyhow::Result { - let vec = base64::engine::general_purpose::URL_SAFE - .decode(t) - .context("invalid base64")?; - serde_json::from_slice(vec.as_slice()).context("invalid json") - } } } + +fn decode_payload>(t: T) -> anyhow::Result { + let vec = base64::engine::general_purpose::URL_SAFE + .decode(t) + .context("invalid base64")?; + serde_json::from_slice(vec.as_slice()).context("invalid json") +} pub async fn run_flow_by_path( authed: Authed, Extension(user_db): Extension, @@ -1706,6 +1714,15 @@ fn list_completed_jobs_query( ); } + if let Some(args) = &lq.args { + sqlb.and_where("args @> ?".bind(&args.replace("'", "''"))); + } + + if let Some(result) = &lq.result { + sqlb.and_where("result @> ?".bind(&result.replace("'", "''"))); + } + + tracing::info!("{:?}", sqlb.sql()); sqlb } #[derive(Deserialize, Clone)] @@ -1723,6 +1740,10 @@ pub struct ListCompletedQuery { pub is_skipped: Option, pub is_flow_step: Option, pub suspended: Option, + // filter by matching a subset of the args using base64 encoded json subset + pub args: Option, + // filter by matching a subset of the result using base64 encoded json subset + pub result: Option, } async fn list_completed_jobs( diff --git a/frontend/src/lib/components/Slider.svelte b/frontend/src/lib/components/Slider.svelte index 04917e1484..d605d7658a 100644 --- a/frontend/src/lib/components/Slider.svelte +++ b/frontend/src/lib/components/Slider.svelte @@ -2,13 +2,18 @@ import { ChevronDown, ChevronUp } from 'lucide-svelte' import { slide } from 'svelte/transition' import Button from './common/button/Button.svelte' + import Tooltip from './Tooltip.svelte' export let text: string + export let tooltip: string | undefined = undefined export let view = false {#if view} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte index 74a24a86a0..081b5ff1cd 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte @@ -1,7 +1,8 @@ diff --git a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte index 505ab88a80..d436f8a5de 100644 --- a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte @@ -18,6 +18,8 @@ import Icon from 'svelte-awesome' import AutoComplete from 'simple-svelte-autocomplete' import NoItemFound from '$lib/components/home/NoItemFound.svelte' + import Slider from '$lib/components/Slider.svelte' + import JsonEditor from '$lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte' let jobs: Job[] | undefined let error: Error | undefined @@ -55,6 +57,16 @@ $: ($workspaceStore && loadJobs(createdBefore)) || (path && success && isSkipped && jobKinds) + let filterTimeout: NodeJS.Timeout | undefined = undefined + function debounceSyncer() { + filterTimeout && clearTimeout(filterTimeout) + filterTimeout = setTimeout(() => { + loadJobs(createdBefore) + }, 500) + } + + $: (true || argFilter || resultFilter) && debounceSyncer() + async function fetchJobs( createdBefore: string | undefined, createdAfter: string | undefined @@ -67,7 +79,13 @@ jobKinds, success, isSkipped, - isFlowStep: jobKindsCat != 'all' ? false : undefined + isFlowStep: jobKindsCat != 'all' ? false : undefined, + args: + argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined, + result: + resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == '' + ? resultFilter + : undefined }) } @@ -89,7 +107,7 @@ } async function syncer() { - if (jobs && createdBefore === undefined) { + if (sync && jobs && createdBefore === undefined) { const reversedJobs = [...jobs].reverse() const lastIndex = reversedJobs.findIndex((x) => x.type == Job.type.QUEUED_JOB) - 1 let ts = lastIndex >= 0 ? reversedJobs[lastIndex].created_at : undefined @@ -110,9 +128,17 @@ } } + let sync = true onMount(() => { loadPaths() intervalId = setInterval(syncer, 5000) + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + sync = false + } else { + sync = true + } + }) }) let paths: string[] = [] @@ -146,6 +172,12 @@ function onSearchPathChange() { goto(`/runs/${searchPath}?${$page.url.searchParams.toString()}`) } + + let argFilter: any = undefined + let resultFilter: any = undefined + + let argError = '' + let resultError = '' @@ -210,28 +242,47 @@ -
- {#key path} - - {/key} - +
+ +
+
- - + {#if jobs}