diff --git a/frontend/src/lib/components/AssignableTagsInner.svelte b/frontend/src/lib/components/AssignableTagsInner.svelte index 4b59d56539..cfca54bc99 100644 --- a/frontend/src/lib/components/AssignableTagsInner.svelte +++ b/frontend/src/lib/components/AssignableTagsInner.svelte @@ -8,8 +8,16 @@ import { superadmin, devopsRole } from '$lib/stores' import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte' import { CUSTOM_TAGS_SETTING } from '$lib/consts' - import { base } from '$lib/base' import { createEventDispatcher } from 'svelte' + import TextInput from './text_input/TextInput.svelte' + import { twMerge } from 'tailwind-merge' + import Badge from './common/badge/Badge.svelte' + + interface Props { + variant?: 'popover' | 'drawer' + } + + let { variant = 'popover' }: Props = $props() let newTag: string = $state('') let customTags: string[] | undefined = $state(undefined) @@ -58,16 +66,48 @@ }) loadCustomTags() + + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' && newTag.trim() !== '' && tagEditor) { + e.stopPropagation() + e.preventDefault() + saveCustomTag(newTag) + } + } + + async function saveCustomTag(tag: string, restoreCustomTags: boolean = false) { + try { + await SettingService.setGlobal({ + key: CUSTOM_TAGS_SETTING, + requestBody: { value: [...(customTags ?? []), tag.trim().replaceAll(' ', '_')] } + }) + dispatch('refresh') + loadCustomTags() + sendUserToast(restoreCustomTags ? 'Tag restored' : 'Tag added') + if (!restoreCustomTags) { + newTag = '' + } + } catch (err) { + sendUserToast(`Could not ${restoreCustomTags ? 'restore' : 'save'} custom tag: ${err}`, true) + } + } -
+ + +
{#if customTags == undefined} {:else} -
+
{#each customTags as customTag} -
{customTag}
+ + {customTag} + {#if tagEditor} {/if} -
+ {/each}
- + +
+ + +
{#if extractedCustomTag} -
+
Workspace specific tag
Tag: @@ -140,55 +201,40 @@ {/if} {/if} - - Configure worker groups to listen to tags - For tags specific to some workspaces, use
tag(workspace1+workspace2)
- To exclude 'workspace1' and 'workspace2' from a tag, use
tag(^workspace1^workspace2)
- For tag(workspace1+workspace2) +
{#if variant !== 'drawer'}
{/if} + To exclude 'workspace1' and 'workspace2' from a tag, use +
tag(^workspace1^workspace2)
+
{#if variant !== 'drawer'}
{/if} + For +
dynamic tagsdynamic tags - based on the workspace, use
$workspace
, e.g: -
tag-$workspace
- For $workspace, e.g: +
tag-$workspace

+ {#if variant !== 'drawer'}
{/if} + + For +
dynamic tagsdynamic tags - based on args input, use
$args[a.b.c]
where -
a.b.c
is the path to the value in the args object
+ based on args input, use
$args[a.b.c]
where +
a.b.c
is the path to the value in the args object. + {/if}
diff --git a/frontend/src/lib/components/AutoscalingConfigEditor.svelte b/frontend/src/lib/components/AutoscalingConfigEditor.svelte index 37e8dcf4c4..82f260f502 100644 --- a/frontend/src/lib/components/AutoscalingConfigEditor.svelte +++ b/frontend/src/lib/components/AutoscalingConfigEditor.svelte @@ -5,26 +5,35 @@ import Tooltip from './Tooltip.svelte' import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' - import { Button } from './common' + import { Alert, Button } from './common' import { ExternalLink } from 'lucide-svelte' - import { createEventDispatcher } from 'svelte' + import TextInput from './text_input/TextInput.svelte' import Label from './Label.svelte' import MultiSelect from './select/MultiSelect.svelte' import { safeSelectItems } from './select/utils.svelte' import { ConfigService } from '$lib/gen' + import Select from './select/Select.svelte' + import ScriptPicker from './ScriptPicker.svelte' + import Badge from './common/badge/Badge.svelte' interface Props { config: AutoscalingConfig | undefined worker_tags: string[] | undefined + disabled: boolean } - let { config = $bindable(), worker_tags }: Props = $props() - - const dispatch = createEventDispatcher() + let { config = $bindable(), worker_tags, disabled }: Props = $props() let test_input: number = $state(3) let healthCheckLoading: boolean = $state(false) let healthCheckResult: { success: boolean; error?: string } | null = $state(null) + function validateMinMax(): string | undefined { + if (config?.min_workers && config?.max_workers && config.min_workers > config.max_workers) { + return 'Minimum cannot be greater than maximum' + } + return undefined + } + async function checkKubernetesHealth() { if (!config?.integration || config.integration.type !== 'kubernetes') return @@ -43,366 +52,258 @@ healthCheckLoading = false } } + + let collapsed: boolean = $state(true) -
-
-
Rules
- { - dispatch('dirty') - if (e.detail) { - if (!config) { - config = { - enabled: true, - min_workers: 3, - max_workers: 10, - integration: { type: 'dryrun' } +
+ {#snippet labelExtra()} + Beta + {/snippet} + {#snippet header()} +
+ { + if (e.detail) { + collapsed = false + if (!config) { + config = { + enabled: true, + min_workers: 3, + max_workers: 10, + integration: { type: 'dryrun' } + } + } else { + config.enabled = true } } else { - config.enabled = true + config = { + ...(config ?? { + min_workers: 3, + max_workers: 10, + integration: { type: 'dryrun' } + }), + enabled: false + } } - } else { - config = { - ...(config ?? { - min_workers: 3, - max_workers: 10, - integration: { type: 'dryrun' } - }), - enabled: false - } - } - }} - /> - -
+ {/snippet} + +
+ - - + -
-
-
- - - - - - + +
- - - - - - - - +
-
-
-
-
Integration
- {#if config?.integration} - dispatch('dirty')} - bind:selected={config.integration.type} - class="mb-4 mt-2" - > - {#snippet children({ item })} - - - - - - {/snippet} - - {#if config.integration.type === 'script'} - - - - -
- - -
-
- -
- - workers -
-
- {/if} - - {#if config.integration.type === 'kubernetes'} -
- Kubernetes configuration is automatically inferred from the cluster environment. The - worker group name and namespace will be detected automatically. -
- -
-
- - -
- - {#if healthCheckResult !== null} -
- {#if healthCheckResult.success} - Kubernetes autoscaling is healthy - {:else} - {healthCheckResult.error} - {#if healthCheckResult.error?.includes('permissions') || healthCheckResult.error?.includes('role')} -
Please follow the setup guide above to configure proper RBAC permissions.Create from template + {disabled} + + {:else} + {/if} +
+ Script must be in the 'admins' workspace + + +
+
+ +
+ {:else if config.integration.type === 'kubernetes'} +
+
+ Kubernetes configuration is automatically inferred from the cluster environment. The + worker group name and namespace will be detected automatically. +
+ +
+
+
+ + +
+ +
+ +
+ + workers +
+
+
+ + {#if healthCheckResult !== null} + + {#if healthCheckResult.success} + Kubernetes autoscaling is healthy + {:else} + {healthCheckResult.error} + {#if healthCheckResult.error?.includes('permissions') || healthCheckResult.error?.includes('role')} +
Please follow the setup guide above to configure proper RBAC permissions. + {/if} + {/if} +
{/if}
- {/if} - -
- -
- - workers -
-
- {/if} + {:else if config.integration.type === 'dryrun'} +
+ + In dry run mode, autoscaling will be simulated and events will be logged but no actual + scaling will be performed. + +
+ {/if} +
{:else} {#snippet children({ item })} @@ -414,10 +315,186 @@ {/snippet} - + {/if} -
-
+ + +
+ + + + + + + + +
+ + + +
+ + + + + +
+ diff --git a/frontend/src/lib/components/AutoscalingEvents.svelte b/frontend/src/lib/components/AutoscalingEvents.svelte index 7a9a6e313f..47626dbb44 100644 --- a/frontend/src/lib/components/AutoscalingEvents.svelte +++ b/frontend/src/lib/components/AutoscalingEvents.svelte @@ -1,11 +1,14 @@ -
-
Autoscaling events {#if $enterpriseLicense}(5 last) - -
+
+
+
+

Autoscaling events

+ {#if $enterpriseLicense && events && events.length > 0} + Showing last {Math.min(limit, events.length)} + {/if} +
+ {#if $enterpriseLicense} +
+ {#if !$enterpriseLicense} -
Autoscaling is an EE feature
+
Autoscaling is an EE feature
{:else if loading} {:else if events} {#if events.length == 0} -
No events, is autoscaling set in the worker group config?
- {:else} -
- {#each events as event} -
-
{event.event_type} to {event.desired_workers}
-
{event.reason}
-
-
- {/each} +
+ No events. Is autoscaling configured in the worker group config?
+ {:else} + + + + Event type + Desired workers + Reason + Time + + + + {#each events as event} + + {event.event_type ?? 'N/A'} + {event.desired_workers} + {event.reason ?? 'N/A'} + + + + + {/each} + + + + {#if events.length >= limit && limit < 100} +
+ +
+ {/if} {/if} -
- -
+ {#if limit > 50} -
- Note that autoscaling events are only stored for the last 30 days. +
+ Note: Autoscaling events are only stored for the last 30 days.
{/if} {/if} diff --git a/frontend/src/lib/components/CenteredPage.svelte b/frontend/src/lib/components/CenteredPage.svelte index b6b85933d6..79143e5266 100644 --- a/frontend/src/lib/components/CenteredPage.svelte +++ b/frontend/src/lib/components/CenteredPage.svelte @@ -1,9 +1,18 @@ -
-
- -
+
{@render children?.({ width })}
diff --git a/frontend/src/lib/components/CollapseLink.svelte b/frontend/src/lib/components/CollapseLink.svelte index a723eea703..09cf94f301 100644 --- a/frontend/src/lib/components/CollapseLink.svelte +++ b/frontend/src/lib/components/CollapseLink.svelte @@ -11,7 +11,7 @@
- {/snippet} - {#snippet content()} - - {/snippet} - diff --git a/frontend/src/lib/components/DefaultTagsInner.svelte b/frontend/src/lib/components/DefaultTagsInner.svelte index 610da15112..ab2d403ec8 100644 --- a/frontend/src/lib/components/DefaultTagsInner.svelte +++ b/frontend/src/lib/components/DefaultTagsInner.svelte @@ -1,21 +1,40 @@ -
- {#if !$enterpriseLicense} -
- - EE only Enterprise Edition only feature -
- {/if} +
+
+ Jobs that have not been specifically assigned custom tags will use a default tags based on the language they are in or their kind. +
+ + {#snippet action()} + {#if !$enterpriseLicense} + Read only + {:else} + + {/if} + {/snippet} + {#if defaultTagPerWorkspace == undefined || defaultTags == undefined} - {:else} -
- {#each defaultTags.sort() as tag (tag)} -
{tag}
→ -
-
+ {:else if !$enterpriseLicense} + +
+ {#each $state.snapshot(defaultTags).sort() as tag (tag)} + {defaultTagPerWorkspace ? `${tag}-$workspace` : tag} {/each}
+ {:else} +
- +
+ +
{#if defaultTagPerWorkspace} - + {#if limitToWorkspaces} {/if} {/if}
- - When tags use
$workspace
, the final tag has -
$workspace
replaced with the workspace id, allowing multi-vpc setup with - more ease, without having to assign a specific tag each time.
+
+
Job language or kind
+
+
Default tag
+
+ + +
+ {#each $state.snapshot(defaultTags).sort() as tag (tag)} +
+
+ {tag} +
+ +
+
+ {defaultTagPerWorkspace ? `${tag}-$workspace` : tag} +
+
+ {/each} +
{/if} -
+
diff --git a/frontend/src/lib/components/DropdownV2Inner.svelte b/frontend/src/lib/components/DropdownV2Inner.svelte index bbf97a35ec..ebb12077d1 100644 --- a/frontend/src/lib/components/DropdownV2Inner.svelte +++ b/frontend/src/lib/components/DropdownV2Inner.svelte @@ -4,6 +4,7 @@ import { twMerge } from 'tailwind-merge' import type { MenubarMenuElements } from '@melt-ui/svelte' import type { Item } from '$lib/utils' + import { Tooltip } from './meltComponents' interface Props { aiId?: string @@ -53,6 +54,13 @@ {item.displayName}

{@render item.extra?.()} + {#if item.tooltip} + + {#snippet text()} + {item.tooltip} + {/snippet} + + {/if} {/each}
diff --git a/frontend/src/lib/components/EEOnly.svelte b/frontend/src/lib/components/EEOnly.svelte new file mode 100644 index 0000000000..0bf54b7b68 --- /dev/null +++ b/frontend/src/lib/components/EEOnly.svelte @@ -0,0 +1,33 @@ + + + + + {#snippet text()} + {#if children} + {@render children()} + {:else} + Enterprise Edition only feature + {/if} + {/snippet} + diff --git a/frontend/src/lib/components/FlowViewerInner.svelte b/frontend/src/lib/components/FlowViewerInner.svelte index 7fab797408..07052a42d4 100644 --- a/frontend/src/lib/components/FlowViewerInner.svelte +++ b/frontend/src/lib/components/FlowViewerInner.svelte @@ -5,7 +5,7 @@ import { Tab, Tabs, Button } from './common' import { copyToClipboard } from '../utils' - import { ArrowDown, Clipboard } from 'lucide-svelte' + import { ArrowDown, Copy } from 'lucide-svelte' import YAML from 'yaml' import { yaml } from 'svelte-highlight/languages' import HighlightTheme from './HighlightTheme.svelte' @@ -80,7 +80,7 @@ color="light" variant="border" size="xs" - startIcon={{ icon: Clipboard }} + startIcon={{ icon: Copy }} btnClasses="absolute top-2 right-2 w-min z-20" iconOnly /> diff --git a/frontend/src/lib/components/HttpAgentWorkerDrawer.svelte b/frontend/src/lib/components/HttpAgentWorkerDrawer.svelte index fd0a14dab7..ed6abfda70 100644 --- a/frontend/src/lib/components/HttpAgentWorkerDrawer.svelte +++ b/frontend/src/lib/components/HttpAgentWorkerDrawer.svelte @@ -1,12 +1,18 @@ {#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null) && !(setting.hiddenIfEmpty && !$values[setting.key])} {#if setting.ee_only != undefined && !$enterpriseLicense} -
- - EE only {#if setting.ee_only != ''}{setting.ee_only}{/if} -
+ + {#if setting.ee_only != ''}{setting.ee_only}{/if} + {/if} {#if setting.fieldType == 'select'}
@@ -571,13 +561,17 @@ {@const currentTeam = $values['critical_error_channels'][i]?.teams_channel ? { team_id: $values['critical_error_channels'][i]?.teams_channel?.team_id, - team_name: $values['critical_error_channels'][i]?.teams_channel?.team_name + team_name: + $values['critical_error_channels'][i]?.teams_channel?.team_name } : undefined} - {@const currentChannel = $values['critical_error_channels'][i]?.teams_channel?.channel_id + {@const currentChannel = $values['critical_error_channels'][i]?.teams_channel + ?.channel_id ? { - channel_id: $values['critical_error_channels'][i]?.teams_channel?.channel_id, - channel_name: $values['critical_error_channels'][i]?.teams_channel?.channel_name + channel_id: + $values['critical_error_channels'][i]?.teams_channel?.channel_id, + channel_name: + $values['critical_error_channels'][i]?.teams_channel?.channel_name } : undefined}
diff --git a/frontend/src/lib/components/Label.svelte b/frontend/src/lib/components/Label.svelte index 42474e5b77..e00ee53cdc 100644 --- a/frontend/src/lib/components/Label.svelte +++ b/frontend/src/lib/components/Label.svelte @@ -2,6 +2,8 @@ import { twMerge } from 'tailwind-merge' import Required from './Required.svelte' import Tooltip from './Tooltip.svelte' + import { enterpriseLicense } from '$lib/stores' + import EEOnly from './EEOnly.svelte' interface Props { label?: string | undefined @@ -12,6 +14,7 @@ class?: string | undefined for?: string | undefined tooltip?: string | undefined + eeOnly?: boolean header?: import('svelte').Snippet error?: import('svelte').Snippet action?: import('svelte').Snippet @@ -27,6 +30,7 @@ class: clazz = undefined, for: forAttr = undefined, tooltip = undefined, + eeOnly = false, header, error, action, @@ -49,6 +53,11 @@ {tooltip} {/if} + {#if eeOnly} + {#if !$enterpriseLicense} + + {/if} + {/if} {@render header?.()}
{/if} diff --git a/frontend/src/lib/components/ManageTagsDrawer.svelte b/frontend/src/lib/components/ManageTagsDrawer.svelte new file mode 100644 index 0000000000..f6fee2fe38 --- /dev/null +++ b/frontend/src/lib/components/ManageTagsDrawer.svelte @@ -0,0 +1,71 @@ + + + + drawer?.closeDrawer?.()}> +
+ +
+ Tags determine which worker group will execute a given job. Workers process only those jobs + whose tags match those defined in their worker group + configuration. +
+ + +
+ +
+ { + if (onRefresh) { + onRefresh() + } + }} + /> +
+ + + + + +
+
+
+
+
diff --git a/frontend/src/lib/components/OccupancyBars.svelte b/frontend/src/lib/components/OccupancyBars.svelte new file mode 100644 index 0000000000..7efbf8433e --- /dev/null +++ b/frontend/src/lib/components/OccupancyBars.svelte @@ -0,0 +1,46 @@ + + +
+ {#each rates as rate} + +
+ {#if rate.value !== undefined && rate.value > 0} + {@const heightPercent = Math.min(rate.value * 100, 100)} + {@const minHeight = heightPercent > 0 && heightPercent < 3 ? 1 : heightPercent} +
+ {/if} +
+ {#snippet text()} + {rate.label}: {rate.value ? displayOccupancyRate(rate.value) : '--'} + {/snippet} +
+ {/each} +
diff --git a/frontend/src/lib/components/QueueAlerts.svelte b/frontend/src/lib/components/QueueAlerts.svelte new file mode 100644 index 0000000000..1f147f52dd --- /dev/null +++ b/frontend/src/lib/components/QueueAlerts.svelte @@ -0,0 +1,572 @@ + + +
+ {#snippet action()} + {#if $enterpriseLicense} + + {#snippet trigger()} + + {/snippet} + {#snippet content()} +
+

Add queue alert

+ +
+ + + Tags that identify which workers to monitor for this alert + +
+ + {#if newAlertForm.tags_to_monitor.length === 0} + + {/if} +
+ {#if formErrors.tags_to_monitor} + {formErrors.tags_to_monitor} + {/if} +
+ +
+ + + Trigger alert when queue exceeds this many jobs + + + {#if formErrors.jobs_num_threshold} + {formErrors.jobs_num_threshold} + {/if} +
+ +
+ + + Wait time between alerts for the same condition + + + {#if formErrors.alert_cooldown_seconds} + {formErrors.alert_cooldown_seconds} + {/if} +
+ +
+ + + How long the condition must persist before alerting + + + {#if formErrors.alert_time_threshold_seconds} + {formErrors.alert_time_threshold_seconds} + {/if} +
+ +
+ + +
+
+ {/snippet} +
+ {/if} + {/snippet} + + {#if !$enterpriseLicense} +
+ Queue Metric Alerts is an enterprise feature allowing you to monitor queues for waiting jobs. + Please upgrade to access this functionality. Learn more about our plans +
+ {:else if queueAlertConfig.length === 0} +
+

No queue alerts configured

+

Add your first alert to monitor queue conditions

+
+ {:else} +
+ + + + + + + + + + + + {#each queueAlertConfig as config, index} + + + + + + + + {/each} + +
+ + Worker Tags + Tags that identify which workers to monitor for this alert + + + + Jobs Threshold + Trigger alert when queue exceeds this many jobs + + + + Cooldown (s) + Wait time between alerts for the same condition + + + + Time Threshold (s) + How long the condition must persist before alerting + + Actions
+ {#if editingRowIndex === index} +
+ { + if (!editForm.tags_to_monitor.includes(tag)) { + editForm.tags_to_monitor = [...editForm.tags_to_monitor, tag] + } + }} + createText="Press Enter to add custom tag" + placeholder="Select or create tags..." + class="flex-1" + /> + {#if editForm.tags_to_monitor.length === 0} + + {/if} +
+ {:else} + {@const isExpanded = expandedTagRows.includes(index)} + {@const tagsToShow = + isExpanded || config.tags_to_monitor.length <= MAX_NUMBER_OF_TAGS_DISPLAYED + ? config.tags_to_monitor + : config.tags_to_monitor.slice(0, MAX_NUMBER_OF_TAGS_DISPLAYED)} + +
+ {#each tagsToShow as tag} + + {tag} + + {/each} + + {#if config.tags_to_monitor.length > MAX_NUMBER_OF_TAGS_DISPLAYED && !isExpanded} + { + expandedTagRows = [...expandedTagRows, index] + }} + > + + {config.tags_to_monitor.length} + + {/if} +
+ {/if} +
+ {#if editingRowIndex === index} + + {:else} + {config.jobs_num_threshold} + {/if} + + {#if editingRowIndex === index} + + {:else} + {config.alert_cooldown_seconds} + {/if} + + {#if editingRowIndex === index} + + {:else} + {config.alert_time_threshold_seconds} + {/if} + + {#if editingRowIndex === index} +
+ + +
+ {:else} +
+ + +
+ {/if} +
+
+ {/if} +
diff --git a/frontend/src/lib/components/QueueMetricsDrawer.svelte b/frontend/src/lib/components/QueueMetricsDrawer.svelte index dd4ad426fa..4b05c41899 100644 --- a/frontend/src/lib/components/QueueMetricsDrawer.svelte +++ b/frontend/src/lib/components/QueueMetricsDrawer.svelte @@ -1,423 +1,26 @@ - + -
- {#if $enterpriseLicense} - {#if changesMade} -
Non applied changes
- {/if} -
- - -
+ - {#if alerts.length > 0} -
-
- - - - - - - - - - - - {#each alerts as alert, index} - - - - - - - - {/each} - -
- Queue Tags to Monitor - - - Jobs - - - Cooldown (s) - - - Time (s) - - Actions
- {#if editingIndex === index} -
- {#each alert.tags_to_monitor as tag} - - {tag} - - - {/each} -
-
- filterTags(e)} - disabled={workerTags.length === alert.tags_to_monitor.length} - class="p-1 flex-grow mr-1" - /> - -
- - - {#if filteredTags.length > 0} -
    - {#each filteredTags as tag} - {#if !alert.tags_to_monitor.includes(tag)} -
  • - -
  • - {/if} - {/each} -
- {/if} - {:else} -
- {#each alert.tags_to_monitor as tag} - {tag} - {/each} -
- {/if} -
- {#if editingIndex === index} - - {:else} - {alert.jobs_num_threshold} - {/if} - - {#if editingIndex === index} - - {:else} - {alert.alert_cooldown_seconds} - {/if} - - {#if editingIndex === index} - - {:else} - {alert.alert_time_threshold_seconds} - {/if} - -
- {#if editingIndex === index} - - {:else} - - - {/if} -
-
-
-
- {/if} +
- -
- -
- {:else} -

- Queue Metric Alerts are an enterprise feature allowing you to monitor queues for waiting - jobs. Please upgrade to access this functionality. - Learn more about our plans. -

- {/if} -
-

Queue Metrics

-
- -
+ + +
diff --git a/frontend/src/lib/components/QueueMetricsDrawerInner.svelte b/frontend/src/lib/components/QueueMetricsDrawerInner.svelte index 76e0b0c72b..f6cc720b88 100644 --- a/frontend/src/lib/components/QueueMetricsDrawerInner.svelte +++ b/frontend/src/lib/components/QueueMetricsDrawerInner.svelte @@ -20,6 +20,7 @@ import Skeleton from './common/skeleton/Skeleton.svelte' import DarkModeObserver from './DarkModeObserver.svelte' import Alert from './common/alert/Alert.svelte' + import { Section } from './common' let loading: boolean = true @@ -186,87 +187,89 @@ -{#if loading} - -{:else if noMetrics} -

No jobs delayed by more than 3 seconds in the last 14 days

-{:else} -
- {#if countData} - 3s)' - } - }, - scales: { - x: { - type: 'time', - min: minDate.toISOString(), - max: new Date().toISOString() - }, - y: { +
+ {#if loading} + + {:else if noMetrics} +

No jobs delayed by more than 3 seconds in the last 14 days

+ {:else} +
+ {#if countData} + 3s)' } - } - } - }} - /> - {/if} - {#if delayData} - 3s)' }, - tooltip: { - callbacks: { - label: function (context) { - // @ts-ignore - if (context.raw.y === 1) { - return context.dataset.label + ': 0' - } else { - // @ts-ignore - return context.dataset.label + ': ' + context.raw.y - } + scales: { + x: { + type: 'time', + min: minDate.toISOString(), + max: new Date().toISOString() + }, + y: { + title: { + display: true, + text: 'count' } } } - }, - scales: { - x: { - type: 'time', - min: minDate.toISOString(), - max: new Date().toISOString() - }, - - y: { - type: 'logarithmic', + }} + /> + {/if} + {#if delayData} + 3s)' }, - ticks: { - callback: (value, _) => (value === 1 ? '0' : value) + tooltip: { + callbacks: { + label: function (context) { + // @ts-ignore + if (context.raw.y === 1) { + return context.dataset.label + ': 0' + } else { + // @ts-ignore + return context.dataset.label + ': ' + context.raw.y + } + } + } + } + }, + scales: { + x: { + type: 'time', + min: minDate.toISOString(), + max: new Date().toISOString() + }, + + y: { + type: 'logarithmic', + title: { + display: true, + text: 'delay (s)' + }, + ticks: { + callback: (value, _) => (value === 1 ? '0' : value) + } } } - } - }} - /> - {/if} - - Only tags for jobs that have been delayed by more than 3 seconds in the last 14 days are - included in the graph. - -
-{/if} + }} + /> + {/if} + + Only tags for jobs that have been delayed by more than 3 seconds in the last 14 days are + included in the graph. + +
+ {/if} + diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index fd2326cb23..8d2b022e3b 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -12,7 +12,7 @@ PostgresTriggerService, CaptureService, type ScriptLang, - WorkerService, + WorkerService } from '$lib/gen' import { inferArgs } from '$lib/infer' import { @@ -189,7 +189,7 @@ : undefined ) const simplifiedPoll = writable(false) - + export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) { primaryScheduleStore.set(schedule) loadTriggers() @@ -1046,7 +1046,7 @@ {#snippet content()}
-
+
{#snippet action()} {#if customUi?.settingsPanel?.metadata?.disableMute !== true} @@ -1214,7 +1214,7 @@
-
+
{#snippet header()}
{/if} -
{#if $enterpriseLicense && initialPath != ''} diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index e77d91ca63..d042f3ec03 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -23,7 +23,7 @@ import Modal from './common/modal/Modal.svelte' import DiffEditor from './DiffEditor.svelte' import { - Clipboard, + Copy, CornerDownLeft, ExternalLink, Github, @@ -556,7 +556,7 @@ {:else} {label} + {@render labelExtra?.()} {/if} {@render header?.()} @@ -79,10 +84,7 @@ {/if} {#if eeOnly} {#if !$enterpriseLicense} -
- - EE only Enterprise Edition only feature -
+ {/if} {/if} @@ -94,10 +96,17 @@ {/if} {#if !collapsable || !collapsed}
- {@render children?.()} + {#if description} +
{description}
+ {/if} +
+
+ {@render children?.()} +
+
{/if}
diff --git a/frontend/src/lib/components/Subsection.svelte b/frontend/src/lib/components/Subsection.svelte index b40e78e83e..bee078cf44 100644 --- a/frontend/src/lib/components/Subsection.svelte +++ b/frontend/src/lib/components/Subsection.svelte @@ -1,8 +1,9 @@ + +{#if tags.length > 0} +
+ + {#each visibleTags as tag (tag)} + + {tag} + + {/each} + + + {#if hasExtraTags} + + {#snippet trigger()} + + +{extraTags.length} + + {/snippet} + {#snippet content()} +
+ {#each extraTags as tag (tag)} + {tag} + {/each} +
+ {/snippet} +
+ {/if} +
+{:else} + No tags +{/if} diff --git a/frontend/src/lib/components/TagsToListenTo.svelte b/frontend/src/lib/components/TagsToListenTo.svelte index e99c67223b..f90b045438 100644 --- a/frontend/src/lib/components/TagsToListenTo.svelte +++ b/frontend/src/lib/components/TagsToListenTo.svelte @@ -1,37 +1,75 @@ worker_tags, - (w) => ((worker_tags = w.map((s) => s.replaceAll(' ', '_'))), dispatch('dirty')) - } + bind:this={multiSelect} + bind:value={() => worker_tags, (w) => (worker_tags = w.map((s) => s.replaceAll(' ', '_')))} {disabled} - class={disabled ? 'border-0' : ''} + class={twMerge(disabled ? 'border-0' : '', clazz)} allowClear={!disabled} - onCreateItem={(c) => { - worker_tags.push(c) - dispatch('dirty') - }} - createText="Press Enter to use this tag" + onCreateItem={createCustomTag} + createText={searchText ? `Create custom tag: ${searchText}` : 'Create custom tag'} /> diff --git a/frontend/src/lib/components/TooltipInner.svelte b/frontend/src/lib/components/TooltipInner.svelte index b8c435f167..28dab0595f 100644 --- a/frontend/src/lib/components/TooltipInner.svelte +++ b/frontend/src/lib/components/TooltipInner.svelte @@ -8,7 +8,7 @@
{#if markdownTooltip}
diff --git a/frontend/src/lib/components/WorkerGroup.svelte b/frontend/src/lib/components/WorkerGroup.svelte index d22e8c8539..0626dffabc 100644 --- a/frontend/src/lib/components/WorkerGroup.svelte +++ b/frontend/src/lib/components/WorkerGroup.svelte @@ -1,17 +1,32 @@ drawer?.closeDrawer()} - title={$superadmin || $devopsRole ? `Edit worker config '${name}'` : `Worker config '${name}'`} + title={canEditConfig ? `Edit worker config '${name}'` : `Worker config '${name}'`} + eeOnly > - {#if !$enterpriseLicense} - + {#if !hasEnterpriseFeatures} + Workers can still have their WORKER_TAGS, INIT_SCRIPT and WHITELIST_ENVS passed as env. Dedicated workers are an enterprise only feature. -
+ {:else if !canEditConfig} + + Only superadmin or devops role can edit the worker config. + {/if} + - { - dirty = true - if (nconfig == undefined) { - nconfig = {} - } - if (e.detail == 'dedicated') { - nconfig.dedicated_worker = '' - nconfig.worker_tags = undefined - } else { - nconfig.dedicated_worker = undefined - nconfig.worker_tags = [] - } - }} - class="mb-4" - > - {#snippet children({ item })} - - - {/snippet} - +
{#if selected == 'normal'} -
- {#if nconfig?.worker_tags != undefined} - { - dirty = true - }} - on:deletePriorityTag={(e) => { - const tag = e.detail - if (nconfig.priority_tags) { - delete nconfig.priority_tags[tag] - } - }} - bind:worker_tags={nconfig.worker_tags} - {customTags} - /> - -
- - - - - {#if defaultTagPerWorkspace} - ({ value: w.id }))} + onCreateItem={(c) => (workspaceTag = c)} + placeholder="Workspace ID" + disabled={!canEditConfig} /> - - {/if} -
+ {/if} +
+ {/if} + {/snippet} + {#if nconfig?.worker_tags != undefined} + {/if} - + + + {#if nconfig?.worker_tags !== undefined && nconfig?.worker_tags.length > 0} +
+ + {/if} + {#if nconfig !== undefined}
-
+
+ {/if} {:else if selected == 'dedicated'}
@@ -429,17 +505,15 @@ {/if} {#if nconfig?.dedicated_worker != undefined}

Workers will get killed upon detecting changes. It is assumed they are in an environment where the supervisor will restart them.

{ - dirty = true - }} + onchange={() => {}} bind:value={nconfig.dedicated_worker} />
@@ -449,52 +523,51 @@
-
-
+
{#each customEnvVars as envvar, i}
{ - dirty = true - }} + onkeypress={(e) => {}} /> { - dirty = true if (e.detail === 'dynamic') { envvar.value = undefined } }} > {#snippet children({ item })} - - + + {/snippet} - - {#if $superadmin || $devopsRole} - + startIcon={{ icon: Trash }} + iconOnly + destructive + disabled={!canEditConfig} + /> {/if}
{/each} - {#if $superadmin || $devopsRole} -
+ {#if canEditConfig} +
+ + + Set up env variables for AWS or SSL using our + { + let updated = false + aws_env_vars_preset.forEach((envvar) => { + if (!customEnvVars.some((e) => e.key === envvar)) { + updated = true + customEnvVars.push({ + key: envvar, + type: 'dynamic', + value: undefined + }) + } + }) + if (updated) { + customEnvVars = [...customEnvVars] + } + } + }, + { + displayName: `SSL (${ssl_env_vars_preset.join(', ')})`, + action: () => { + let updated = false + ssl_env_vars_preset.forEach((envvar) => { + if (!customEnvVars.some((e) => e.key === envvar)) { + updated = true + customEnvVars.push({ + key: envvar, + type: 'dynamic', + value: undefined + }) + } + }) + if (updated) { + customEnvVars = [...customEnvVars] + } + } + } + ]} + > + {#snippet buttonReplacement()} + + {/snippet} + +
{/if}
- {#if !($superadmin || $devopsRole)} -
- + {/if} +
+ {/each} + {/if} + {#if canEditConfig} +
+ - +
+ {/if} + +
+ {/if} +
-
- -
- {#snippet header()} -
- Beta - {#if nconfig.autoscaling?.enabled} - Enabled - {/if} -
- {/snippet} - (dirty = true)} - worker_tags={config?.worker_tags} - bind:config={nconfig.autoscaling} - /> -
- -
-
-
- - {#if nconfig.additional_python_paths} - {#each nconfig.additional_python_paths as _, i} -
- - {#if $superadmin || $devopsRole} - - {/if} -
- {/each} - {/if} - {#if $superadmin || $devopsRole} -
- -
- {/if} -
- - {#if nconfig.pip_local_dependencies} - {#each nconfig.pip_local_dependencies as _, i} -
- - {#if $superadmin || $devopsRole} - - {/if} -
- {/each} - {/if} - {#if $superadmin || $devopsRole} -
- -
- {/if} -
-
- {#if $superadmin || $devopsRole} + {#if canEditConfig}
- + Workers will get killed upon detecting any changes in this section (scripts or interval). It is assumed they are in an environment where the supervisor will restart them. @@ -737,13 +795,14 @@
-
+
Run at start of the workers. More lightweight than requiring custom worker images.
- {#snippet header()}
@@ -755,7 +814,7 @@
{ if (config) { - dirty = true const code = e.detail if (code != '') { nconfig.init_bash = code?.replace(/\r\n/g, '\n') @@ -774,17 +832,18 @@ }} />
- +
-
+
Run periodically at configurable intervals. Useful for maintenance tasks like cleaning disk space.
- {#snippet header()}
@@ -795,24 +854,24 @@ {/snippet}
- - { - dirty = true - }} - /> - Minimum: 60 seconds +
{ if (config) { - dirty = true const code = e.detail if (code != '') { nconfig.periodic_script_bash = code?.replace(/\r\n/g, '\n') @@ -832,215 +890,282 @@ }} />
- +
{#snippet actions()} -
+
- {#if dirty} -
Non applied changes
- {/if} - + await ConfigService.updateConfig({ name: 'worker__' + name, requestBody: nconfig }) + sendUserToast('Configuration set') + dispatch('reload') + }} + disabled={(!hasChanges && nconfig?.dedicated_worker == undefined) || !canEditConfig} + > + Apply changes + + {:else} + Read only + {/if}
{/snippet} -
- {#if hasWorkersWithoutIsolation(workers)} - {@const unsafeWorkers = getWorkersWithoutIsolation(workers)} -
- - {#snippet trigger()} - - {/snippet} - {#snippet content()} -
-
Workers without job isolation
-

- {unsafeWorkers.length} - {unsafeWorkers.length === 1 ? 'worker' : 'workers'} in this group - {unsafeWorkers.length === 1 ? 'is' : 'are'} running without job isolation (nsjail/unshare). -

-
- {#each unsafeWorkers as worker} - - {worker.worker} - - {/each} -
- - Learn more about job isolation → - -
- {/snippet} -
-
- {/if} +
+
+
+
+ {#if selectGroup} + {@render selectGroup()} + + {:else} + Worker group: + {name} - + {/if} + + {`${pluralize(activeWorkers, 'worker')} `} + {#snippet text()}Number of active workers of this group in the last 15 seconds{/snippet} +
-
-
{pluralize(activeWorkers, 'worker')} {#if vcpus_memory?.vcpus} - {(vcpus_memory?.vcpus / 100000).toFixed(2)} vCPUs{/if} {#if vcpus_memory?.memory} - - {((vcpus_memory?.memory * 1.0) / 1024 / 1024 / 1024).toFixed(2)} GB{/if}
-
- {#if $superadmin} - - - - - {#if config} - - {/if} + - - {:else if config} - + {#snippet text()} +
+
Workers without job isolation
+

+ {unsafeWorkers.length} + {unsafeWorkers.length === 1 ? 'worker' : 'workers'} in this group + {unsafeWorkers.length === 1 ? 'is' : 'are'} running without job isolation (nsjail/unshare). +

+
+ {#each unsafeWorkers as worker} + + {worker.worker} + + {/each} +
+ + Learn more about job isolation + +
+ {/snippet} + +
{/if}
+ {#if !isAgent} +
+ {#if canEditConfig} + {#if width > 1000} + {#if config} + + {/if} + + + + {#if config} + + {/if} + {:else} + { + navigator.clipboard.writeText(YAML.stringify({ name, ...config })) + sendUserToast('Worker config copied to clipboard as YAML') + }, + disabled: !config + }, + { + displayName: 'Clean cache', + action: () => { + loadNConfig() + openClean = true + }, + disabled: !config, + type: 'delete' + }, + { + displayName: 'Delete config', + action: () => { + if (!hasEnterpriseFeatures) { + sendUserToast('Worker Management UI is an EE feature', true) + } else { + openDelete = true + } + }, + disabled: !config, + type: 'delete' + } + ]} + /> + {/if} + + {:else if config} + + {/if} +
+ {/if}
+ + {#if config?.worker_tags && config.worker_tags.length > 0} +
+
Tags:
+ +
+ {:else if config?.dedicated_worker} +
+
Dedicated to:
+
+ {config.dedicated_worker} +
+
+ {/if}
+ +{#if isAgent} +
+ + {#snippet children()} + This group is formed with agent workers, there is no associated config. {#if $superadmin || $devopsRole} + To modify the tags, generate a new JWT token .{/if} + {/snippet} + +
+{/if} diff --git a/frontend/src/lib/components/apps/components/display/AppText.svelte b/frontend/src/lib/components/apps/components/display/AppText.svelte index 7d1e09bd06..a7f98089f8 100644 --- a/frontend/src/lib/components/apps/components/display/AppText.svelte +++ b/frontend/src/lib/components/apps/components/display/AppText.svelte @@ -3,7 +3,7 @@ const bubble = createBubbler() import Tooltip from '$lib/components/Tooltip.svelte' - import { Clipboard } from 'lucide-svelte' + import { Copy } from 'lucide-svelte' import { getContext, untrack } from 'svelte' import { twMerge } from 'tailwind-merge' import { copyToClipboard, isCodeInjection } from '../../../../utils' @@ -273,7 +273,7 @@ btnClasses="!p-1" on:click={() => copyToClipboard(result)} > - +
{/if} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte index 9b6f5a9b2f..81f59a5028 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte @@ -2,7 +2,7 @@ import { Alert } from '$lib/components/common' import Toggle from '$lib/components/Toggle.svelte' import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' - import { Loader2, AlertTriangle } from 'lucide-svelte' + import { Loader2 } from 'lucide-svelte' import Tooltip from '$lib/components/Tooltip.svelte' @@ -13,6 +13,7 @@ import { computeSecretUrl } from './appDeploy.svelte' import { base } from '$lib/base' import { isCloudHosted } from '$lib/cloud' + import EEOnly from '$lib/components/EEOnly.svelte' let { policy, @@ -216,11 +217,9 @@
{/if} + {#if !$enterpriseLicense} -
- - EE only Enterprise Edition only feature -
+ {/if} { diff --git a/frontend/src/lib/components/apps/editor/AppExportButton.svelte b/frontend/src/lib/components/apps/editor/AppExportButton.svelte index 0bc4cf4b10..2090323725 100644 --- a/frontend/src/lib/components/apps/editor/AppExportButton.svelte +++ b/frontend/src/lib/components/apps/editor/AppExportButton.svelte @@ -7,7 +7,7 @@ import { Highlight } from 'svelte-highlight' import json from 'svelte-highlight/languages/json' import { Button } from '../../common' - import { Clipboard } from 'lucide-svelte' + import { Copy } from 'lucide-svelte' import { yaml } from 'svelte-highlight/languages' import YAML from 'yaml' import Tabs from '$lib/components/common/tabs/Tabs.svelte' @@ -45,7 +45,7 @@ )} variant="accent" size="sm" - startIcon={{ icon: Clipboard }} + startIcon={{ icon: Copy }} btnClasses="absolute top-2 right-2 w-min z-20" iconOnly /> diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/CssSettings.svelte b/frontend/src/lib/components/apps/editor/componentsPanel/CssSettings.svelte index 6fd86c9ad7..d081549dbd 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/CssSettings.svelte +++ b/frontend/src/lib/components/apps/editor/componentsPanel/CssSettings.svelte @@ -1,6 +1,6 @@
-
+
{tooltip} {/if} + {#if eeOnly && !$enterpriseLicense} + + {/if}
{#if actions}
diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index 6c184563df..08822ad433 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -23,7 +23,7 @@ Pen, Share, Trash, - Clipboard + Copy } from 'lucide-svelte' import { goto as gotoUrl } from '$app/navigation' import { page } from '$app/stores' @@ -218,7 +218,7 @@ }, { displayName: 'Copy path', - icon: Clipboard, + icon: Copy, action: () => { copyToClipboard(path) } diff --git a/frontend/src/lib/components/common/table/FlowRow.svelte b/frontend/src/lib/components/common/table/FlowRow.svelte index 755d311996..4ed3429618 100644 --- a/frontend/src/lib/components/common/table/FlowRow.svelte +++ b/frontend/src/lib/components/common/table/FlowRow.svelte @@ -28,7 +28,7 @@ Calendar, Share, Archive, - Clipboard, + Copy, Eye, HistoryIcon } from 'lucide-svelte' @@ -211,7 +211,7 @@ }, { displayName: 'Copy path', - icon: Clipboard, + icon: Copy, action: () => { copyToClipboard(path) } diff --git a/frontend/src/lib/components/details/ClipboardPanel.svelte b/frontend/src/lib/components/details/ClipboardPanel.svelte index cb5e0d535f..289c7da416 100644 --- a/frontend/src/lib/components/details/ClipboardPanel.svelte +++ b/frontend/src/lib/components/details/ClipboardPanel.svelte @@ -1,6 +1,6 @@ - - + +
{ + onclick={(e) => { if (disabled) { return } @@ -23,9 +28,13 @@ }} >
- +
-
- +
+
diff --git a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte index c245fd9b41..ad736e40e0 100644 --- a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte +++ b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte @@ -83,7 +83,7 @@
{#if flowModuleValue} diff --git a/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte b/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte index d724f241e8..6f1a1e2db0 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte @@ -42,7 +42,7 @@ /> {:else}
+ {/if} +
-
+ +
{#if mcpUrl} - - - + {:else} - - - + {/if}
-
-

+ +
+ +

{displayTitle}

- -
-
- - -
-
- - {tokenOrUrl} - - -
-
-
+ +
+ + + + +
{info} - {#if mcpUrl} -
-

- Next steps: Use this URL in your MCP-compatible client (like Claude Desktop) to access your Windmill scripts and flows as tools. -

-
- {/if}
- + + {#if mcpUrl} + +
+

+ Next steps: Use this URL in your MCP-compatible client (like Claude Desktop) + to access your Windmill scripts and flows as tools. +

+
+ {/if} +
-

\ No newline at end of file +
diff --git a/frontend/src/lib/components/sidebar/CriticalAlertTable.svelte b/frontend/src/lib/components/sidebar/CriticalAlertTable.svelte index edbb3329bd..468acfc423 100644 --- a/frontend/src/lib/components/sidebar/CriticalAlertTable.svelte +++ b/frontend/src/lib/components/sidebar/CriticalAlertTable.svelte @@ -63,9 +63,9 @@ Acked
{/key} {/key} @@ -452,7 +452,7 @@ done` }} > - +
{/key}{/key}{/key}{/key} {/key} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 14d3719463..853bd8332c 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1343,6 +1343,7 @@ export type Item = { hide?: boolean | undefined extra?: Snippet id?: string + tooltip?: string } export function isObjectTooBig(obj: any): boolean { diff --git a/frontend/src/routes/(root)/(logged)/workers/+page.svelte b/frontend/src/routes/(root)/(logged)/workers/+page.svelte index e127ba9a36..32625a5bd8 100644 --- a/frontend/src/routes/(root)/(logged)/workers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workers/+page.svelte @@ -1,14 +1,14 @@ {#if $superadmin || $devopsRole} + { + loadCustomTags() + }} + /> {/if} @@ -346,7 +448,12 @@ fixedOverflowWidgets={false} /> {#snippet actions()} - + {/snippet} @@ -385,25 +492,27 @@
{:else} - - {#if $superadmin || $devopsRole} -
-
- { - loadCustomTags() - }} + {#snippet children({ width })} + + {#if $superadmin || $devopsRole} +
+
-
- -
-
+ startIcon={{ + icon: Tags + }} + on:click={() => { + manageTagsDrawer?.openDrawer() + }} + > + Manage tags + + -
-
- -
-
- {/if} - - {#if workers != undefined} - {#if groupedWorkers.length == 0} -

No workers seem to be available

- {/if} - -

{groupWorkers?.length} Worker Groups Worker groups are groups of workers that share a config and are meant to be identical. - Worker groups are meant to be used with tags. Tags can be assigned to scripts and flows - and can be seen as dedicated queues. Only the corresponding -

-
- - {#if $superadmin || $devopsRole} -
{ + if ( + e.key === 'Enter' && + newConfigName && + newConfigName.trim() !== '' && + $enterpriseLicense + ) { + addConfig() + } + }} + onClose={() => { + newConfigName = '' + }} + bind:this={newGroupPopover} + targetId="new-group-popover-trigger" > - + {#snippet trigger()}
-
- + {/snippet} + {#snippet content()}
{#if !$enterpriseLicense} -
- - EE only -
+ {/if}
-
+ {/snippet}
- {/if}
+ {/if} + - {#if (groupedWorkers ?? []).length > 5} -
-
Worker group:
- + {#if worker_group} + {@const config = (workerGroups ?? {})[worker_group[0]]} + {@const activeWorkers = worker_group?.[1].flatMap((x) => + x[1]?.filter((y) => (y.last_ping ?? 0) < 15) + )} + + { + loadWorkerGroups() + }} + activeWorkers={activeWorkers?.length ?? 0} + {defaultTagPerWorkspace} + {width} + shouldAutoOpenDrawer={shouldAutoOpenDrawer === worker_group[0]} + onDrawerOpened={() => { + shouldAutoOpenDrawer = '' + }} + onDeleted={handleWorkerGroupDeleted} + selectGroup={(groupedWorkers ?? []).length > 5 ? selectGroup : undefined} /> - -
- {#if worker_group?.[1].length == 0 && search} -
- No workers found. Reset the search to see all workers. + +
+ +
+
Active workers
+
+ + +
- {:else} + Worker - -
- Worker Tags - - If defined, the workers only pull jobs with the same corresponding tag - -
-
- Last ping + {#if !config || !config.worker_tags || config.worker_tags.length === 0} + +
+ Worker tags + + If defined, the workers only pull jobs with the same corresponding tag + +
+
+ {/if} Worker start Jobs ran {#if (!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)} @@ -625,10 +724,10 @@ Memory usage
(Windmill)
Limits Version - Liveness + Status {#if $superadmin || $devopsRole} - Live Shell + Repl

Open a live shell to execute bash commands on the machine where the worker @@ -639,173 +738,279 @@ {/if} - - {#each worker_group[1] as [section, workers]} - {@const hostname = section?.split(splitter)?.[0]} - - -

-
- Host: - {hostname} -
- IP: - {workers[0].ip} + + {#if worker_group?.[1].length > 0} + {#each worker_group[1] as [section, workers], groupIdx} + {@const hostname = section?.split(splitter)?.[0]} + + +
+
+ Host: + {hostname} +
+ IP: + {workers[0].ip} - {#if workers?.length > 1} - {workers?.length} Workers - {/if} + {#if workers?.length > 1} + {workers?.length} Workers + {/if} +
+
+ + {#if workers} + {#each workers as { worker, custom_tags, last_ping, started_at, jobs_executed, last_job_id, last_job_workspace_id, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, occupancy_rate, wm_version, vcpus, memory, memory_usage, wm_memory_usage }} + {@const isWorkerAlive = isWorkerMaybeAlive(last_ping)} + {@const tagMismatchInfo = getTagMismatchInfo( + custom_tags, + config?.worker_tags + )} + + + {@const underscorePos = worker.search('_')} +
+ + {#if underscorePos === -1} + {worker} + {:else} + {truncate(worker, underscorePos)} + {worker} + {/if} + + {#if config && tagMismatchInfo.hasMismatch} + + + + + + {#snippet text()} +
+
Tag configuration mismatch
+

This worker has tags that differ from the config. This + could be due to a recent update of the config or because the + worker has tags defined as environment variables.

+ + {#if tagMismatchInfo.added.length > 0} +
+
Additional tags:
+ +
+ {/if} + + {#if tagMismatchInfo.removed.length > 0} +
+
Missing tags:
+ +
+ {/if} +
+ {/snippet} +
+ {/if} +
+
+ {#if !config || !config.worker_tags || config.worker_tags.length === 0} + + + + {/if} + {displayDate(started_at)} + {jobs_executed} + {#if (!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)} + + {#if last_job_id} + + View last job + + +
+ {last_job_workspace_id} + {/if} +
+ + + + {/if} + +
+
+ {memory_usage + ? Math.round(memory_usage / 1024 / 1024) + 'MB' + : '--'} +
+
+ ({wm_memory_usage + ? Math.round(wm_memory_usage / 1024 / 1024) + 'MB' + : '--'}) +
+
+
+ +
+
+ {vcpus ? (vcpus / 100000).toFixed(2) + ' vCPUs' : '--'} +
+
+ {memory ? Math.round(memory / 1024 / 1024) + 'MB' : '--'} +
+
+
+ +
+ {wm_version.split('-')[0]} +
+
+ + {@const pingText = + last_ping != undefined + ? `${last_ping + timeSinceLastPing}s ago` + : 'Unknown'} + {@const statusText = + isWorkerAlive != undefined + ? isWorkerAlive + ? 'Alive' + : 'Dead' + : 'Unknown'} +
+ + {statusText} + +
{pingText}
+
+
+ {#if $superadmin || $devopsRole} + + + + {/if} + + {/each} + {/if} + {/each} + {:else if search} + + +
+ No active workers found matching the search query
- {#if workers} - {#each workers as { worker, custom_tags, last_ping, started_at, jobs_executed, last_job_id, last_job_workspace_id, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, occupancy_rate, wm_version, vcpus, memory, memory_usage, wm_memory_usage }} - {@const isWorkerAlive = isWorkerMaybeAlive(last_ping)} - - - {@const underscorePos = worker.search('_')} - {#if underscorePos === -1} - {worker} - {:else} - {truncate(worker, underscorePos)} - {worker} - {/if} - - - {#if custom_tags && custom_tags?.length > 2} - {truncate(custom_tags?.join(', ') ?? '', 10)} - {custom_tags?.join(', ')} - {:else} - {custom_tags?.join(', ') ?? ''} - {/if} - - {last_ping != undefined ? last_ping + timeSinceLastPing : -1}s ago - {displayDate(started_at)} - {jobs_executed} - {#if (!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)} - - {#if last_job_id} - - View last job - -
- (workspace {last_job_workspace_id}) - {/if} -
- - {displayOccupancyRate(occupancy_rate_15s)}/{displayOccupancyRate( - occupancy_rate_5m - )}/{displayOccupancyRate(occupancy_rate_30m)}/{displayOccupancyRate( - occupancy_rate - )} - - {/if} - -
-
- {memory_usage ? Math.round(memory_usage / 1024 / 1024) + 'MB' : '--'} -
-
- ({wm_memory_usage - ? Math.round(wm_memory_usage / 1024 / 1024) + 'MB' - : '--'}) -
-
-
- -
-
- {vcpus ? (vcpus / 100000).toFixed(2) + ' vCPUs' : '--'} -
-
- {memory ? Math.round(memory / 1024 / 1024) + 'MB' : '--'} -
-
-
- -
- {wm_version.split('-')[0]}{wm_version} -
-
- - - {isWorkerAlive != undefined - ? isWorkerAlive - ? 'Alive' - : 'Dead' - : 'Unknown'} - - - {#if $superadmin || $devopsRole} - - - - {/if} - - {/each} - {/if} - {/each} + {:else} + + +
+ No active workers found for the group '{worker_group[0]}' +
+
+ + {/if} - {/if} - {:else} - {@const worker_group = Object.entries(workerGroups ?? {}) - .filter((x) => !groupedWorkers.some((y) => y[0] == x[0])) - .find((x) => x[0] == selectedTab)} + {:else} + {@const worker_group = Object.entries(workerGroups ?? {}) + .filter((x) => !groupedWorkers.some((y) => y[0] == x[0])) + .find((x) => x[0] == selectedTab)} - {#if worker_group} - { - loadWorkerGroups() - }} - name={worker_group[0]} - config={worker_group[1]} - activeWorkers={0} - /> -
No workers currently in this worker group
+ {#if worker_group} + { + loadWorkerGroups() + }} + name={worker_group[0]} + config={worker_group[1]} + activeWorkers={0} + {width} + shouldAutoOpenDrawer={shouldAutoOpenDrawer === worker_group[0]} + onDrawerOpened={() => { + shouldAutoOpenDrawer = '' + }} + onDeleted={handleWorkerGroupDeleted} + /> +
No workers currently in this worker group
+ {/if} {/if} +
+
+ + {#if worker_group?.[2] === false} + {/if} -
-
- - {:else} -
- {#each new Array(4) as _} - - {/each} -
- {/if} + {:else} +
+ {#each new Array(4) as _} + + {/each} +
+ {/if} + {/snippet} {/if} + +{#snippet selectGroup()} +
+
Worker group + Worker groups are groups of workers that share a config and are meant to be identical. + Worker groups are meant to be used with tags. Tags can be assigned to scripts and flows and + can be seen as dedicated queues. Only the corresponding + +
+