mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 08:05:44 +00:00
fix: display if tag has an active workers attached to it in tag select
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user