fix: display if tag has an active workers attached to it in tag select

This commit is contained in:
Ruben Fiszel
2025-08-08 16:40:48 +00:00
parent 7fab48ce76
commit d7d93c7112
6 changed files with 121 additions and 25 deletions
+9 -7
View File
@@ -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:
+25 -11
View File
@@ -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<UserDB>,
Query(tag_query): Query<TagQuery>,
) -> JsonResult<bool> {
Query(tags_query): Query<TagsQuery>,
) -> JsonResult<std::collections::HashMap<String, bool>> {
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::<Vec<String>>();
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)]
@@ -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<Record<string, boolean> | 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)
</script>
{#snippet startSnippet({ item })}
{#if tagsToWorkerExists}
{#if tagsToWorkerExists[item.value]}
<Popover>
{#snippet text()}
At least one worker with this tag exists and is running.
{/snippet}
<div class="rounded-full inline-block bg-green-500 text-white text-xs w-2 h-2 mr-1"></div>
</Popover>
{:else}
<Popover>
{#snippet text()}
No workers with this tag exist or is running.
{/snippet}
<div class="rounded-full inline-block bg-red-500 text-white text-xs w-2 h-2 mr-1"></div>
</Popover>
{/if}
{/if}
{/snippet}
<div class="flex gap-1 items-center relative">
{#if !noLabel}
<div class="text-tertiary text-2xs">{placeholder ?? 'tag'}</div>
@@ -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}
<div class="absolute top-0 -right-12">
@@ -17,14 +17,14 @@
let visible = true
async function lookForTag(): Promise<void> {
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)
@@ -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<Value> }]>
} = $props()
let disabled = $derived(_disabled || (loading && !value))
@@ -147,5 +149,6 @@
getInputRect={inputEl && (() => inputEl!.getBoundingClientRect())}
{listAutoWidth}
{noItemsMsg}
{startSnippet}
/>
</div>
@@ -18,7 +18,8 @@
ulClass = '',
header,
getInputRect,
onSelectValue
onSelectValue,
startSnippet
}: {
processedItems?: ProcessedItem<T>[]
value: T | undefined
@@ -33,6 +34,7 @@
header?: Snippet
getInputRect?: () => DOMRect
onSelectValue: (item: ProcessedItem<T>) => void
startSnippet?: Snippet<[{ item: ProcessedItem<T> }]>
} = $props()
let processedItems = $derived(
@@ -137,6 +139,7 @@
onSelectValue(item)
}}
>
{@render startSnippet?.({ item })}
{item.label || '\xa0'}
{#if item.subtitle}
<div class="text-xs text-tertiary">{item.subtitle}</div>