From d7d93c7112ff9bc20d0e5e57085cca51a7e95c2b Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 8 Aug 2025 16:40:48 +0000 Subject: [PATCH] fix: display if tag has an active workers attached to it in tag select --- backend/windmill-api/openapi.yaml | 16 ++-- backend/windmill-api/src/workers.rs | 36 ++++++--- .../src/lib/components/WorkerTagSelect.svelte | 76 ++++++++++++++++++- .../runs/NoWorkerWithTagWarning.svelte | 6 +- .../src/lib/components/select/Select.svelte | 7 +- .../components/select/SelectDropdown.svelte | 5 +- 6 files changed, 121 insertions(+), 25 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index bcf5f267a5..d6fbe4b72a 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2448,7 +2448,6 @@ paths: schema: type: string - /w/{workspace}/workspaces/edit_large_file_storage_config: post: summary: edit large file storage settings @@ -11352,25 +11351,28 @@ paths: items: $ref: "#/components/schemas/WorkerPing" - /workers/exists_worker_with_tag: + /workers/exists_workers_with_tags: get: - summary: exists worker with tag - operationId: existsWorkerWithTag + summary: exists workers with tags + operationId: existsWorkersWithTags tags: - worker parameters: - - name: tag + - name: tags in: query required: true + description: comma separated list of tags schema: type: string responses: "200": - description: whether a worker with the tag exists + description: map of tags to whether at least one worker with the tag exists content: application/json: schema: - type: boolean + type: object + additionalProperties: + type: boolean /workers/queue_metrics: get: diff --git a/backend/windmill-api/src/workers.rs b/backend/windmill-api/src/workers.rs index 65db431d69..8e6b7a86fd 100644 --- a/backend/windmill-api/src/workers.rs +++ b/backend/windmill-api/src/workers.rs @@ -28,7 +28,7 @@ use crate::{db::ApiAuthed, utils::require_super_admin}; pub fn global_service() -> Router { Router::new() .route("/list", get(list_worker_pings)) - .route("/exists_worker_with_tag", get(exists_worker_with_tag)) + .route("/exists_workers_with_tags", get(exists_workers_with_tags)) .route("/custom_tags", get(get_custom_tags)) .route( "/is_default_tags_per_workspace", @@ -113,24 +113,38 @@ async fn list_worker_pings( } #[derive(Serialize, Deserialize)] -struct TagQuery { - tag: String, +struct TagsQuery { + tags: String, } -async fn exists_worker_with_tag( +async fn exists_workers_with_tags( authed: ApiAuthed, Extension(user_db): Extension, - Query(tag_query): Query, -) -> JsonResult { + Query(tags_query): Query, +) -> JsonResult> { let mut tx = user_db.begin(&authed).await?; - let row = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM worker_ping WHERE custom_tags @> $1 AND ping_at > now() - interval '1 minute')", - &[tag_query.tag] + let mut result = std::collections::HashMap::new(); + + // Create a query that checks all tags at once using unnest + let tags = tags_query + .tags + .split(',') + .map(|s| s.to_string()) + .collect::>(); + let rows = sqlx::query!( + "SELECT tag::text, EXISTS(SELECT 1 FROM worker_ping WHERE custom_tags @> ARRAY[tag] AND ping_at > now() - interval '1 minute') as exists + FROM unnest($1::text[]) as tag", + tags.as_slice() ) - .fetch_one(&mut *tx) + .fetch_all(&mut *tx) .await?; + + for row in rows { + result.insert(row.tag.unwrap_or_default(), row.exists.unwrap_or(false)); + } + tx.commit().await?; - Ok(Json(row.exists.unwrap_or(false))) + Ok(Json(result)) } #[derive(Deserialize)] diff --git a/frontend/src/lib/components/WorkerTagSelect.svelte b/frontend/src/lib/components/WorkerTagSelect.svelte index d0c7ec4faa..b98f2f89ce 100644 --- a/frontend/src/lib/components/WorkerTagSelect.svelte +++ b/frontend/src/lib/components/WorkerTagSelect.svelte @@ -2,12 +2,13 @@ import { workerTags, workspaceStore } from '$lib/stores' import { WorkerService } from '$lib/gen' - import { createEventDispatcher } from 'svelte' + import { createEventDispatcher, onDestroy, onMount } from 'svelte' import Select from './select/Select.svelte' import { safeSelectItems } from './select/utils.svelte' import { Button } from './common' import { RotateCw } from 'lucide-svelte' import { sendUserToast } from '$lib/toast' + import Popover from './Popover.svelte' let { tag = $bindable(), @@ -28,6 +29,20 @@ } = $props() let loading = $state(false) + let visible = $state(false) + let timeout: NodeJS.Timeout | undefined = undefined + let tagsToWorkerExists = $state | undefined>(undefined) + + onMount(() => { + visible = true + }) + + onDestroy(() => { + visible = false + if (timeout) { + clearTimeout(timeout) + } + }) loadWorkerGroups() @@ -51,9 +66,67 @@ ...($workerTags ?? []) ]) + let lastCheck: number | undefined = undefined + async function loadTagsToWorkerExists(tags: string[]) { + if (lastCheck && Date.now() - lastCheck < 5000) { + return + } + if (timeout) { + clearTimeout(timeout) + } + if (open) { + tagsToWorkerExists = await WorkerService.existsWorkersWithTags({ tags: tags.join(',') }) + lastCheck = Date.now() + if (visible) { + timeout = setTimeout(() => { + loadTagsToWorkerExists(tags) + }, 5000) + } + } + } + + // let finalItems = $derived( + // items.map((item) => { + // if (tagsToWorkerExists) { + // return { + // value: item, + // __select_group: tagsToWorkerExists[item] + // ? `${placeholder ?? 'Worker'}s available` + // : `No ${placeholder ?? 'Worker'}s` + // } + // } + // return item + // }) + // ) + + $effect(() => { + if ($workerTags && open) { + loadTagsToWorkerExists($workerTags) + } + }) + let open = $state(false) +{#snippet startSnippet({ item })} + {#if tagsToWorkerExists} + {#if tagsToWorkerExists[item.value]} + + {#snippet text()} + At least one worker with this tag exists and is running. + {/snippet} +
+
+ {:else} + + {#snippet text()} + No workers with this tag exist or is running. + {/snippet} +
+
+ {/if} + {/if} +{/snippet}
{#if !noLabel}
{placeholder ?? 'tag'}
@@ -67,6 +140,7 @@ placeholder={nullTag ? nullTag : (placeholder ?? 'lang default')} items={safeSelectItems(items)} bind:value={() => tag, (value) => ((tag = value), dispatch('change', value))} + {startSnippet} /> {#if open}
diff --git a/frontend/src/lib/components/runs/NoWorkerWithTagWarning.svelte b/frontend/src/lib/components/runs/NoWorkerWithTagWarning.svelte index 58287ace29..b801237935 100644 --- a/frontend/src/lib/components/runs/NoWorkerWithTagWarning.svelte +++ b/frontend/src/lib/components/runs/NoWorkerWithTagWarning.svelte @@ -17,14 +17,14 @@ let visible = true async function lookForTag(): Promise { try { - const existsWorkerWithTag = await WorkerService.existsWorkerWithTag({ tag }) - noWorkerWithTag = !existsWorkerWithTag + const existsWorkerWithTag = await WorkerService.existsWorkersWithTags({ tags: tag }) + noWorkerWithTag = !existsWorkerWithTag[tag] if (noWorkerWithTag) { timeout = setTimeout(() => { if (visible) { lookForTag() } - }, 1000) + }, 2500) } } catch (err) { console.error(err) diff --git a/frontend/src/lib/components/select/Select.svelte b/frontend/src/lib/components/select/Select.svelte index 9dbf33a46d..9dca4fabec 100644 --- a/frontend/src/lib/components/select/Select.svelte +++ b/frontend/src/lib/components/select/Select.svelte @@ -3,7 +3,7 @@ import { twMerge } from 'tailwind-merge' import CloseButton from '../common/CloseButton.svelte' import { Loader2 } from 'lucide-svelte' - import { untrack } from 'svelte' + import { untrack, type Snippet } from 'svelte' import { getLabel, processItems, type ProcessedItem } from './utils.svelte' import SelectDropdown from './SelectDropdown.svelte' import { deepEqual } from 'fast-equals' @@ -33,7 +33,8 @@ onFocus, onBlur, onClear, - onCreateItem + onCreateItem, + startSnippet }: { items?: Item[] value: Value | undefined @@ -58,6 +59,7 @@ onBlur?: () => void onClear?: () => void onCreateItem?: (value: string) => void + startSnippet?: Snippet<[{ item: ProcessedItem }]> } = $props() let disabled = $derived(_disabled || (loading && !value)) @@ -147,5 +149,6 @@ getInputRect={inputEl && (() => inputEl!.getBoundingClientRect())} {listAutoWidth} {noItemsMsg} + {startSnippet} />
diff --git a/frontend/src/lib/components/select/SelectDropdown.svelte b/frontend/src/lib/components/select/SelectDropdown.svelte index ee98bee964..30938d93dc 100644 --- a/frontend/src/lib/components/select/SelectDropdown.svelte +++ b/frontend/src/lib/components/select/SelectDropdown.svelte @@ -18,7 +18,8 @@ ulClass = '', header, getInputRect, - onSelectValue + onSelectValue, + startSnippet }: { processedItems?: ProcessedItem[] value: T | undefined @@ -33,6 +34,7 @@ header?: Snippet getInputRect?: () => DOMRect onSelectValue: (item: ProcessedItem) => void + startSnippet?: Snippet<[{ item: ProcessedItem }]> } = $props() let processedItems = $derived( @@ -137,6 +139,7 @@ onSelectValue(item) }} > + {@render startSnippet?.({ item })} {item.label || '\xa0'} {#if item.subtitle}
{item.subtitle}