mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
Adding or editing environment variables in the worker group config drawer did not enable the "Apply changes" button. `hasChanges` compares the original `config` prop against the local `nconfig` copy, but env var edits only mutate the local `customEnvVars` array — they aren't synced into `nconfig.env_vars_static`/`env_vars_allowlist` until the Apply handler runs. This left the button stuck disabled despite real changes. Add a `hasEnvVarChanges` derived that reconstructs the original env vars from `config` and compares them to the current `customEnvVars`, and include it in the Apply button's disabled condition. Fixes WIN-2023 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1417 lines
42 KiB
Svelte
1417 lines
42 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
TriangleAlert,
|
|
Plus,
|
|
RefreshCcwIcon,
|
|
RotateCcw,
|
|
Settings,
|
|
Trash,
|
|
Power,
|
|
X,
|
|
ExternalLink,
|
|
FileCode
|
|
} from 'lucide-svelte'
|
|
import { Alert, Button, Drawer } from './common'
|
|
import Badge from './common/badge/Badge.svelte'
|
|
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
|
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import {
|
|
ConfigService,
|
|
SettingService,
|
|
WorkspaceService,
|
|
type WorkerPing,
|
|
type Workspace
|
|
} from '$lib/gen'
|
|
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import {
|
|
emptyString,
|
|
pluralize,
|
|
cleanValueProperties,
|
|
orderedJsonStringify,
|
|
replaceFalseWithUndefined
|
|
} from '$lib/utils'
|
|
import { enterpriseLicense, superadmin, devopsRole } from '$lib/stores'
|
|
import Tooltip from './meltComponents/Tooltip.svelte'
|
|
import Editor from './Editor.svelte'
|
|
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
|
import Section from './Section.svelte'
|
|
import Label from './Label.svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
import {
|
|
cleanWorkerGroupConfig,
|
|
defaultTags,
|
|
nativeTags,
|
|
type AutoscalingConfig
|
|
} from './worker_group'
|
|
import AutoscalingConfigEditor from './AutoscalingConfigEditor.svelte'
|
|
import TagsToListenTo from './TagsToListenTo.svelte'
|
|
import Select from './select/Select.svelte'
|
|
import MultiSelect from './select/MultiSelect.svelte'
|
|
import { safeSelectItems } from './select/utils.svelte'
|
|
import TextInput from './text_input/TextInput.svelte'
|
|
import Dropdown from './DropdownV2.svelte'
|
|
import TagList from './TagList.svelte'
|
|
import DedicatedWorkersSelector from './DedicatedWorkersSelector.svelte'
|
|
import { computeHashedTag } from './dedicated_worker'
|
|
|
|
function computeVCpuAndMemory(workers: [string, WorkerPing[]][]) {
|
|
let vcpus = 0
|
|
let memory = 0
|
|
for (const [_, pings] of workers) {
|
|
for (const ping of pings) {
|
|
if (ping.vcpus) {
|
|
vcpus += ping.vcpus
|
|
}
|
|
if (ping.memory) {
|
|
memory += ping.memory
|
|
}
|
|
}
|
|
}
|
|
return { vcpus, memory }
|
|
}
|
|
|
|
function hasWorkersWithoutIsolation(workers: [string, WorkerPing[]][]): boolean {
|
|
return workers.some(([_, pings]) =>
|
|
pings.some((w) => !w.job_isolation || w.job_isolation === 'none')
|
|
)
|
|
}
|
|
|
|
function getWorkersWithoutIsolation(workers: [string, WorkerPing[]][]): WorkerPing[] {
|
|
return workers.flatMap(([_, pings]) =>
|
|
pings.filter((w) => !w.job_isolation || w.job_isolation === 'none')
|
|
)
|
|
}
|
|
|
|
let nconfig: {
|
|
dedicated_worker?: string
|
|
dedicated_workers?: string[]
|
|
worker_tags?: string[]
|
|
priority_tags?: Record<string, number>
|
|
cache_clear?: number
|
|
init_bash?: string
|
|
periodic_script_bash?: string
|
|
periodic_script_interval_seconds?: number
|
|
env_vars_static?: Map<string, string>
|
|
env_vars_allowlist?: string[]
|
|
additional_python_paths?: string[]
|
|
pip_local_dependencies?: string[]
|
|
min_alive_workers_alert_threshold?: number
|
|
autoscaling?: AutoscalingConfig
|
|
native_mode?: boolean
|
|
} = $state({})
|
|
|
|
function loadNConfig() {
|
|
nconfig = config
|
|
? config.worker_tags != undefined ||
|
|
config.dedicated_worker != undefined ||
|
|
config.dedicated_workers != undefined
|
|
? config
|
|
: {
|
|
worker_tags: []
|
|
}
|
|
: {
|
|
worker_tags: []
|
|
}
|
|
if (nconfig.priority_tags === undefined) {
|
|
nconfig.priority_tags = {}
|
|
}
|
|
|
|
// Convert legacy dedicated_worker to dedicated_workers array
|
|
if (nconfig.dedicated_worker && !nconfig.dedicated_workers?.length) {
|
|
nconfig.dedicated_workers = [nconfig.dedicated_worker]
|
|
nconfig.dedicated_worker = undefined
|
|
}
|
|
|
|
customEnvVars = []
|
|
if (nconfig.env_vars_allowlist === undefined) {
|
|
nconfig.env_vars_allowlist = []
|
|
} else {
|
|
for (const key of nconfig.env_vars_allowlist) {
|
|
customEnvVars.push({ key, type: 'dynamic', value: undefined })
|
|
}
|
|
}
|
|
if (nconfig.env_vars_static === undefined) {
|
|
nconfig.env_vars_static = new Map<string, string>()
|
|
} else {
|
|
for (const [key, value] of Object.entries(nconfig.env_vars_static)) {
|
|
customEnvVars.push({ key, type: 'static', value })
|
|
}
|
|
}
|
|
customEnvVars.sort((a, b) => (a.key < b.key ? -1 : 1))
|
|
}
|
|
|
|
// Clean up priority_tags when worker_tags changes
|
|
$effect(() => {
|
|
// This effect ensures priority_tags only contains tags that exist in worker_tags
|
|
if (nconfig.worker_tags && nconfig.priority_tags) {
|
|
const validTags = new Set(nconfig.worker_tags)
|
|
const currentPriorityKeys = Object.keys(nconfig.priority_tags)
|
|
|
|
// Check if any priority tags are no longer valid
|
|
const hasInvalidTags = currentPriorityKeys.some((tag) => !validTags.has(tag))
|
|
|
|
if (hasInvalidTags) {
|
|
// Filter out invalid tags
|
|
const filteredPriorityTags: Record<string, number> = {}
|
|
for (const [tag, priority] of Object.entries(nconfig.priority_tags)) {
|
|
if (validTags.has(tag)) {
|
|
filteredPriorityTags[tag] = priority
|
|
}
|
|
}
|
|
nconfig.priority_tags = filteredPriorityTags
|
|
}
|
|
}
|
|
})
|
|
|
|
let customEnvVars: {
|
|
key: string
|
|
type: 'static' | 'dynamic'
|
|
value: string | undefined
|
|
}[] = $state([])
|
|
|
|
const aws_env_vars_preset = [
|
|
'AWS_REGION',
|
|
'AWS_DEFAULT_REGION',
|
|
'AWS_ACCESS_KEY_ID',
|
|
'AWS_SECRET_ACCESS_KEY',
|
|
'AWS_ENDPOINT_URL'
|
|
]
|
|
const ssl_env_vars_preset = [
|
|
'DENO_CERT',
|
|
'PIP_INDEX_CERT',
|
|
'REQUESTS_CA_BUNDLE',
|
|
'SSL_CERT_FILE',
|
|
'SSL_CERT_DIR'
|
|
]
|
|
|
|
let workspaceTag = $state('')
|
|
interface Props {
|
|
name: string
|
|
config:
|
|
| undefined
|
|
| {
|
|
dedicated_worker?: string
|
|
dedicated_workers?: string[]
|
|
worker_tags?: string[]
|
|
priority_tags?: Record<string, number>
|
|
cache_clear?: number
|
|
init_bash?: string
|
|
env_vars_static?: Map<string, string>
|
|
env_vars_allowlist?: string[]
|
|
additional_python_paths?: string[]
|
|
pip_local_dependencies?: string[]
|
|
min_alive_workers_alert_threshold?: number
|
|
autoscaling?: AutoscalingConfig
|
|
periodic_script_bash?: string
|
|
periodic_script_interval_seconds?: number
|
|
native_mode?: boolean
|
|
}
|
|
activeWorkers: number
|
|
customTags: string[] | undefined
|
|
workers: [string, WorkerPing[]][]
|
|
isAgent?: boolean
|
|
defaultTagPerWorkspace?: boolean | undefined
|
|
width: number
|
|
shouldAutoOpenDrawer?: boolean
|
|
onDrawerOpened?: () => void
|
|
onDeleted?: (deletedGroupName: string) => void
|
|
onOpenYamlEditor?: () => void
|
|
selectGroup?: import('svelte').Snippet
|
|
}
|
|
|
|
let {
|
|
name,
|
|
config = $bindable(),
|
|
activeWorkers,
|
|
customTags,
|
|
workers,
|
|
isAgent = false,
|
|
defaultTagPerWorkspace = undefined,
|
|
width = 0,
|
|
shouldAutoOpenDrawer = false,
|
|
onDrawerOpened = () => {},
|
|
onDeleted = () => {},
|
|
onOpenYamlEditor = undefined,
|
|
selectGroup = undefined
|
|
}: Props = $props()
|
|
|
|
let workspaces: Workspace[] = $state([])
|
|
async function listWorkspaces() {
|
|
try {
|
|
workspaces = await WorkspaceService.listWorkspacesAsSuperAdmin()
|
|
} catch (e) {
|
|
console.error('Failed to list workspaces', e)
|
|
}
|
|
}
|
|
|
|
// Centralized permission logic
|
|
let canEditConfig = $derived($superadmin || $devopsRole)
|
|
let hasEnterpriseFeatures = $derived($enterpriseLicense)
|
|
let canEditEEConfig = $derived(canEditConfig && hasEnterpriseFeatures)
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
async function deleteWorkerGroup() {
|
|
await ConfigService.deleteConfig({ name: 'worker__' + name })
|
|
dispatch('reload')
|
|
onDeleted(name)
|
|
}
|
|
|
|
let hasChanges = $derived.by(() => {
|
|
if (!config && !nconfig) return false
|
|
if (!config || !nconfig) return true
|
|
|
|
const cleaned1 = cleanValueProperties(config)
|
|
const cleaned2 = cleanValueProperties(nconfig)
|
|
|
|
return (
|
|
orderedJsonStringify(replaceFalseWithUndefined(cleaned1)) !==
|
|
orderedJsonStringify(replaceFalseWithUndefined(cleaned2))
|
|
)
|
|
})
|
|
|
|
// Env var edits only mutate the local `customEnvVars` array; they aren't synced back into
|
|
// `nconfig.env_vars_*` until the Apply handler runs, so `hasChanges` can't see them. Detect
|
|
// those edits separately by reconstructing the original env vars from `config`.
|
|
let hasEnvVarChanges = $derived.by(() => {
|
|
let originalEnvVars: typeof customEnvVars = []
|
|
if (config?.env_vars_allowlist) {
|
|
for (const key of config.env_vars_allowlist) {
|
|
originalEnvVars.push({ key, type: 'dynamic' as const, value: undefined })
|
|
}
|
|
}
|
|
if (config?.env_vars_static) {
|
|
for (const [key, value] of Object.entries(config.env_vars_static)) {
|
|
originalEnvVars.push({ key, type: 'static' as const, value })
|
|
}
|
|
}
|
|
originalEnvVars.sort((a, b) => (a.key < b.key ? -1 : 1))
|
|
return JSON.stringify(customEnvVars) !== JSON.stringify(originalEnvVars)
|
|
})
|
|
let openDelete = $state(false)
|
|
let openClean = $state(false)
|
|
let openRestart = $state(false)
|
|
|
|
// Compute hashed tags for display (actual tags used by the worker)
|
|
let hashedDedicatedTags: Map<string, string> = $state(new Map())
|
|
$effect(() => {
|
|
const dws =
|
|
config?.dedicated_workers ?? (config?.dedicated_worker ? [config.dedicated_worker] : [])
|
|
if (dws.length > 0) {
|
|
Promise.all(dws.map(async (dw) => [dw, await computeHashedTag(dw)] as const)).then(
|
|
(entries) => {
|
|
hashedDedicatedTags = new Map(entries)
|
|
}
|
|
)
|
|
} else {
|
|
hashedDedicatedTags = new Map()
|
|
}
|
|
})
|
|
|
|
let drawer: Drawer | undefined = $state()
|
|
let vcpus_memory = $derived(computeVCpuAndMemory(workers))
|
|
let selected = $derived(
|
|
nconfig?.dedicated_worker != undefined || nconfig?.dedicated_workers != undefined
|
|
? 'dedicated'
|
|
: 'normal'
|
|
)
|
|
let isNativeMode = $derived(
|
|
config?.native_mode === true ||
|
|
name === 'native' ||
|
|
(workers.length > 0 &&
|
|
workers.some(([_, pings]) => pings.some((p) => p.native_mode === true)))
|
|
)
|
|
let nonNativeTags = $derived(
|
|
(nconfig?.worker_tags ?? []).filter(
|
|
(t) =>
|
|
!nativeTags.some((nt) => t === nt || t.startsWith(`${nt}-`)) &&
|
|
t !== 'flow' &&
|
|
!t.startsWith('flow-')
|
|
)
|
|
)
|
|
let isAutoNativeMode = $derived(name === 'native')
|
|
let isNativeModeEnabled = $derived(nconfig?.native_mode === true || isAutoNativeMode)
|
|
$effect(() => {
|
|
;($superadmin || $devopsRole) && listWorkspaces()
|
|
})
|
|
|
|
$effect(() => {
|
|
if (shouldAutoOpenDrawer) {
|
|
loadNConfig()
|
|
drawer?.openDrawer()
|
|
// Dispatch event to parent to clear the signal
|
|
onDrawerOpened()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<ConfirmationModal
|
|
open={openDelete}
|
|
title="Delete worker group"
|
|
confirmationText="Remove"
|
|
on:canceled={() => {
|
|
openDelete = false
|
|
}}
|
|
on:confirmed={async () => {
|
|
deleteWorkerGroup()
|
|
openDelete = false
|
|
}}
|
|
>
|
|
<div class="flex flex-col w-full space-y-4">
|
|
<span>Are you sure you want to remove this worker group {name}?</span>
|
|
</div>
|
|
</ConfirmationModal>
|
|
|
|
<ConfirmationModal
|
|
open={openClean}
|
|
title="Clear cache"
|
|
confirmationText="Remove"
|
|
on:canceled={() => {
|
|
openClean = false
|
|
}}
|
|
on:confirmed={async () => {
|
|
const ndate = Math.floor(Date.now() / 1000)
|
|
const withCacheConfig = { ...nconfig, cache_clear: ndate }
|
|
await ConfigService.updateConfig({
|
|
name: 'worker__' + name,
|
|
requestBody: withCacheConfig
|
|
})
|
|
if (config) {
|
|
config.cache_clear = ndate
|
|
}
|
|
sendUserToast('Worker caches clearing in 5s. Require a restart.')
|
|
dispatch('reload')
|
|
openClean = false
|
|
}}
|
|
>
|
|
<div class="flex flex-col w-full space-y-4">
|
|
<span
|
|
>Are you sure you want to clean the cache of all workers of this worker group (will also
|
|
restart the workers and expect supervisor to restart them) ?</span
|
|
>
|
|
</div>
|
|
</ConfirmationModal>
|
|
|
|
<ConfirmationModal
|
|
open={openRestart}
|
|
title="Restart workers"
|
|
confirmationText="Restart"
|
|
on:canceled={() => {
|
|
openRestart = false
|
|
}}
|
|
on:confirmed={async () => {
|
|
try {
|
|
await SettingService.restartWorkerGroup({ workerGroup: name })
|
|
sendUserToast(`Restart signal sent to worker group '${name}'`)
|
|
dispatch('reload')
|
|
} catch (e) {
|
|
sendUserToast(`Failed to restart worker group: ${e}`, true)
|
|
}
|
|
openRestart = false
|
|
}}
|
|
>
|
|
<div class="flex flex-col w-full space-y-4">
|
|
<span
|
|
>Are you sure you want to restart all workers in worker group '{name}'? Workers will be
|
|
gracefully shut down and are expected to be restarted by their supervisor.</span
|
|
>
|
|
</div>
|
|
</ConfirmationModal>
|
|
|
|
<Drawer bind:this={drawer} size="800px">
|
|
<DrawerContent
|
|
on:close={() => drawer?.closeDrawer()}
|
|
title={canEditConfig ? `Edit worker config '${name}'` : `Worker config '${name}'`}
|
|
>
|
|
{#if !hasEnterpriseFeatures}
|
|
<Alert type="info" title="Enterprise features" class="mb-4">
|
|
Some worker management features require the enterprise edition. In CE, you can configure
|
|
worker tags.
|
|
</Alert>
|
|
{:else if !canEditConfig}
|
|
<Alert type="info" title="Read-only mode" class="mb-4">
|
|
Only superadmin or devops role can edit the worker config.
|
|
</Alert>
|
|
{/if}
|
|
<Label label="Workers assignment" eeOnly={!hasEnterpriseFeatures}>
|
|
<ToggleButtonGroup
|
|
{selected}
|
|
disabled={!canEditEEConfig}
|
|
on:selected={(e) => {
|
|
if (nconfig == undefined) {
|
|
nconfig = {}
|
|
}
|
|
if (e.detail == 'dedicated') {
|
|
nconfig.dedicated_workers = nconfig.dedicated_workers ?? []
|
|
nconfig.dedicated_worker = undefined
|
|
nconfig.worker_tags = undefined
|
|
} else {
|
|
nconfig.dedicated_worker = undefined
|
|
nconfig.dedicated_workers = undefined
|
|
nconfig.worker_tags = []
|
|
}
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="normal" label="Any jobs within worker tags" {item} />
|
|
<ToggleButton value="dedicated" label="Dedicated to scripts/flows" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</Label>
|
|
|
|
<div class="mt-8"></div>
|
|
{#if selected == 'normal'}
|
|
<Label label="Tags to listen to">
|
|
{#snippet action()}
|
|
{#if nconfig?.worker_tags != undefined}
|
|
{@const resetTags = isNativeModeEnabled ? nativeTags : defaultTags.concat(nativeTags)}
|
|
{@const dropdownResetToAllTags = [
|
|
{
|
|
label: isNativeModeEnabled
|
|
? 'Reset to all tags'
|
|
: 'Reset to all tags minus native ones',
|
|
onClick: () => {
|
|
if (nconfig != undefined) {
|
|
nconfig.worker_tags = isNativeModeEnabled
|
|
? defaultTagPerWorkspace && workspaceTag
|
|
? defaultTags.concat(nativeTags).map((nt) => `${nt}-${workspaceTag}`)
|
|
: defaultTags.concat(nativeTags)
|
|
: defaultTagPerWorkspace && workspaceTag
|
|
? defaultTags.map((nt) => `${nt}-${workspaceTag}`)
|
|
: defaultTags
|
|
}
|
|
},
|
|
disabled: !canEditConfig,
|
|
tooltip: (defaultTagPerWorkspace
|
|
? (isNativeModeEnabled ? defaultTags.concat(nativeTags) : defaultTags).map(
|
|
(nt) => `${nt}-${workspaceTag}`
|
|
)
|
|
: isNativeModeEnabled
|
|
? defaultTags.concat(nativeTags)
|
|
: defaultTags
|
|
).join(', ')
|
|
},
|
|
...(!isNativeModeEnabled
|
|
? [
|
|
{
|
|
label: 'Reset to native tags',
|
|
onClick: () => {
|
|
if (nconfig != undefined) {
|
|
nconfig.worker_tags =
|
|
defaultTagPerWorkspace && workspaceTag
|
|
? nativeTags.map((nt) => `${nt}-${workspaceTag}`)
|
|
: nativeTags
|
|
}
|
|
},
|
|
disabled: !canEditConfig,
|
|
tooltip: (defaultTagPerWorkspace && workspaceTag
|
|
? nativeTags.map((nt) => `${nt}-${workspaceTag}`)
|
|
: nativeTags
|
|
).join(', ')
|
|
}
|
|
]
|
|
: []),
|
|
{
|
|
label: 'Clear tags',
|
|
onClick: () => {
|
|
if (nconfig != undefined) {
|
|
nconfig.worker_tags = []
|
|
}
|
|
},
|
|
disabled: !canEditConfig
|
|
}
|
|
]}
|
|
<div class="flex flex-wrap items-center gap-1">
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="sm"
|
|
on:click={() => {
|
|
if (nconfig != undefined) {
|
|
nconfig.worker_tags =
|
|
defaultTagPerWorkspace && workspaceTag
|
|
? resetTags.map((nt) => `${nt}-${workspaceTag}`)
|
|
: resetTags
|
|
}
|
|
}}
|
|
dropdownItems={dropdownResetToAllTags}
|
|
dropdownWidth={300}
|
|
startIcon={{ icon: RotateCcw }}
|
|
disabled={!canEditConfig}
|
|
>
|
|
{isNativeModeEnabled ? 'Reset to native tags' : 'Reset to all tags'}
|
|
<Tooltip>
|
|
{#snippet text()}
|
|
{(defaultTagPerWorkspace && workspaceTag
|
|
? resetTags.map((nt) => `${nt}-${workspaceTag}`)
|
|
: resetTags
|
|
).join(', ')}
|
|
{/snippet}
|
|
</Tooltip>
|
|
</Button>
|
|
|
|
{#if defaultTagPerWorkspace}
|
|
<Select
|
|
bind:value={workspaceTag}
|
|
items={workspaces.map((w) => ({ value: w.id }))}
|
|
onCreateItem={(c) => (workspaceTag = c)}
|
|
placeholder="Workspace ID"
|
|
disabled={!canEditConfig}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{/snippet}
|
|
{#if nconfig?.worker_tags != undefined}
|
|
<TagsToListenTo
|
|
bind:worker_tags={nconfig.worker_tags}
|
|
{customTags}
|
|
disabled={!canEditConfig}
|
|
/>
|
|
{/if}
|
|
</Label>
|
|
|
|
{#if nconfig?.worker_tags !== undefined && nconfig?.worker_tags.length > 0}
|
|
<div class="mt-8"></div>
|
|
<Label label="High-priority tags" eeOnly={!hasEnterpriseFeatures}>
|
|
{#snippet header()}
|
|
<Tooltip>
|
|
{#snippet text()}
|
|
Jobs with the following high-priority tags will be picked up in priority by this
|
|
worker.
|
|
{#if !enterpriseLicense}
|
|
This is a feature only available in enterprise edition.
|
|
{/if}
|
|
{/snippet}
|
|
</Tooltip>
|
|
{/snippet}
|
|
<MultiSelect
|
|
disabled={!canEditEEConfig}
|
|
bind:value={
|
|
() => (nconfig.priority_tags ? Object.keys(nconfig.priority_tags) : []),
|
|
(v) => {
|
|
nconfig.priority_tags = Object.fromEntries(v.map((k) => [k, 100]))
|
|
}
|
|
}
|
|
items={safeSelectItems(nconfig?.worker_tags)}
|
|
/>
|
|
</Label>
|
|
{/if}
|
|
|
|
{#if nconfig !== undefined}
|
|
<div class="mt-8"></div>
|
|
<Label label="Native mode">
|
|
{#snippet header()}
|
|
<Tooltip>
|
|
{#snippet text()}
|
|
When enabled, the worker will only accept native jobs (nativets, postgresql, mysql,
|
|
etc.) and automatically runs with 8 subworkers for optimal throughput. Non-native
|
|
jobs will be failed.
|
|
{/snippet}
|
|
</Tooltip>
|
|
{/snippet}
|
|
<Toggle
|
|
size="sm"
|
|
options={{
|
|
right: isAutoNativeMode ? 'Native mode (automatically enabled)' : 'Enable native mode'
|
|
}}
|
|
checked={nconfig?.native_mode === true || isAutoNativeMode}
|
|
on:change={(ev) => {
|
|
if (nconfig !== undefined) {
|
|
nconfig.native_mode = ev.detail ? true : undefined
|
|
}
|
|
}}
|
|
disabled={!canEditConfig || isAutoNativeMode}
|
|
/>
|
|
{#if isNativeModeEnabled}
|
|
<p class="text-xs text-secondary mt-1"
|
|
>8 subworkers will automatically be used for optimal throughput.</p
|
|
>
|
|
{/if}
|
|
{#if isNativeModeEnabled && nonNativeTags.length > 0}
|
|
<Alert size="xs" type="warning" title="Non-native tags detected">
|
|
This worker group has native mode enabled but includes non-native tags: {nonNativeTags.join(
|
|
', '
|
|
)}. This is fine if jobs sent to those tags are native only, otherwise they will be
|
|
failed.
|
|
</Alert>
|
|
{/if}
|
|
{#if isNativeModeEnabled && nconfig?.worker_tags != undefined && !nconfig.worker_tags.includes(defaultTagPerWorkspace && workspaceTag ? `flow-${workspaceTag}` : 'flow')}
|
|
<Alert size="xs" type="info" title="Flow tag not included">
|
|
Adding the flow tag to native workers can improve efficiency, but may cause issues
|
|
with large inputs.
|
|
<div class="mt-2 w-fit">
|
|
<Button
|
|
variant="default"
|
|
on:click={() => {
|
|
if (nconfig?.worker_tags != undefined) {
|
|
const flowTag =
|
|
defaultTagPerWorkspace && workspaceTag ? `flow-${workspaceTag}` : 'flow'
|
|
nconfig.worker_tags = [...nconfig.worker_tags, flowTag]
|
|
}
|
|
}}
|
|
disabled={!canEditConfig}
|
|
startIcon={{ icon: Plus }}
|
|
>
|
|
Add flow tag
|
|
</Button>
|
|
</div>
|
|
</Alert>
|
|
{/if}
|
|
</Label>
|
|
{/if}
|
|
|
|
{#if nconfig !== undefined}
|
|
<div class="mt-8"></div>
|
|
<Label
|
|
label="Alerts"
|
|
tooltip="Alert is sent to the configured critical error channels"
|
|
eeOnly={!hasEnterpriseFeatures}
|
|
>
|
|
<Toggle
|
|
size="sm"
|
|
options={{
|
|
right: 'Send an alert when the number of alive workers falls below a given threshold'
|
|
}}
|
|
checked={nconfig?.min_alive_workers_alert_threshold !== undefined}
|
|
on:change={(ev) => {
|
|
if (nconfig !== undefined) {
|
|
nconfig.min_alive_workers_alert_threshold = ev.detail ? 1 : undefined
|
|
}
|
|
}}
|
|
disabled={!canEditEEConfig}
|
|
/>
|
|
{#if nconfig.min_alive_workers_alert_threshold !== undefined}
|
|
<div class="flex flex-row items-center text-xs gap-2">
|
|
<p>Triggered when number of workers in group is lower than</p>
|
|
<input
|
|
type="number"
|
|
class="!w-14 text-center"
|
|
disabled={!canEditEEConfig}
|
|
min="1"
|
|
bind:value={nconfig.min_alive_workers_alert_threshold}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</Label>
|
|
{/if}
|
|
{:else if selected == 'dedicated'}
|
|
<div class="flex flex-col gap-4">
|
|
{#if $superadmin || $devopsRole}
|
|
<div class="py-2">
|
|
<Alert
|
|
size="xs"
|
|
type="info"
|
|
title="The 'dedicated worker' runtime setting of the runnables must be enabled to be selected here"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
<p class="text-xs text-secondary"
|
|
>Workers will get killed upon detecting changes. It is assumed they are in an environment
|
|
where the supervisor will restart them.</p
|
|
>
|
|
|
|
{#if nconfig !== undefined}
|
|
<DedicatedWorkersSelector
|
|
selectedTags={nconfig.dedicated_workers ?? []}
|
|
disabled={!canEditEEConfig}
|
|
onchange={(tags) => {
|
|
if (nconfig) {
|
|
nconfig.dedicated_workers = tags
|
|
nconfig.dedicated_worker = undefined
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="mt-8"></div>
|
|
|
|
<Label
|
|
label="Environment variables passed to jobs"
|
|
tooltip="Add static and dynamic environment variables that will be passed to jobs handled by this worker group. Dynamic environment variable values will be loaded from the worker host environment variables while static environment variables will be set directly from their values below."
|
|
eeOnly={!hasEnterpriseFeatures}
|
|
>
|
|
<div class="flex flex-col gap-y-2 pb-2 max-w">
|
|
{#each customEnvVars as envvar, i}
|
|
<div class="flex gap-1 items-center">
|
|
<input
|
|
disabled={!canEditEEConfig}
|
|
type="text"
|
|
placeholder="ENV_VAR_NAME"
|
|
bind:value={envvar.key}
|
|
onkeypress={(e) => {}}
|
|
/>
|
|
<ToggleButtonGroup
|
|
disabled={!canEditEEConfig}
|
|
class="w-128"
|
|
bind:selected={envvar.type}
|
|
on:selected={(e) => {
|
|
if (e.detail === 'dynamic') {
|
|
envvar.value = undefined
|
|
}
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="dynamic" label="Dynamic" {item} disabled={!canEditEEConfig} />
|
|
<ToggleButton value="static" label="Static" {item} disabled={!canEditEEConfig} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
<TextInput
|
|
inputProps={{
|
|
type: 'text',
|
|
disabled: !canEditEEConfig || envvar.type === 'dynamic',
|
|
placeholder:
|
|
envvar.type === 'dynamic' ? 'value read from worker env var' : 'static value'
|
|
}}
|
|
bind:value={envvar.value}
|
|
/>
|
|
{#if canEditEEConfig}
|
|
<Button
|
|
wrapperClasses="ml-2"
|
|
variant="subtle"
|
|
unifiedSize="md"
|
|
aria-label="Clear"
|
|
onClick={() => {
|
|
if (nconfig.env_vars_static?.[envvar.key] !== undefined) {
|
|
delete nconfig.env_vars_static[envvar.key]
|
|
}
|
|
if (nconfig.env_vars_allowlist?.includes(envvar.key)) {
|
|
nconfig.env_vars_allowlist = nconfig.env_vars_allowlist.filter(
|
|
(k) => k != envvar.key
|
|
)
|
|
}
|
|
customEnvVars.splice(i, 1)
|
|
customEnvVars = [...customEnvVars]
|
|
}}
|
|
startIcon={{ icon: Trash }}
|
|
iconOnly
|
|
destructive
|
|
disabled={!canEditEEConfig}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{#if canEditEEConfig}
|
|
<div class="flex flex-col gap-2">
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="md"
|
|
startIcon={{ icon: Plus }}
|
|
on:click={() => {
|
|
customEnvVars.push({ key: '', type: 'dynamic', value: undefined })
|
|
customEnvVars = [...customEnvVars]
|
|
}}
|
|
disabled={!canEditEEConfig}
|
|
>
|
|
Add environment variable
|
|
</Button>
|
|
|
|
<span class="text-secondary text-xs">
|
|
Set up env variables for AWS or SSL using our
|
|
<Dropdown
|
|
placement="bottom-start"
|
|
class="inline-block"
|
|
items={[
|
|
{
|
|
displayName: `AWS (${aws_env_vars_preset.join(', ')})`,
|
|
action: () => {
|
|
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()}
|
|
<button class="text-accent font-medium"> presets </button>
|
|
{/snippet}
|
|
</Dropdown>
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Label>
|
|
<div class="mt-8"></div>
|
|
|
|
<AutoscalingConfigEditor
|
|
worker_tags={config?.worker_tags}
|
|
bind:config={nconfig.autoscaling}
|
|
disabled={!canEditEEConfig}
|
|
eeOnly={!hasEnterpriseFeatures}
|
|
/>
|
|
|
|
<div class="mt-8"></div>
|
|
<Section
|
|
label="Python dependencies overrides"
|
|
collapsable={true}
|
|
class="flex flex-col gap-y-6"
|
|
eeOnly={!hasEnterpriseFeatures}
|
|
>
|
|
<Label
|
|
label="Additional Python paths"
|
|
tooltip="Paths to add to the Python path for it to search dependencies, useful if you have packages pre-installed on the workers at a given path."
|
|
class="mt-2"
|
|
>
|
|
{#if nconfig.additional_python_paths}
|
|
{#each nconfig.additional_python_paths as _, i}
|
|
<div class="flex gap-1 items-center">
|
|
<input
|
|
type="text"
|
|
disabled={!canEditEEConfig}
|
|
placeholder="/path/to/python3.X/site-packages"
|
|
bind:value={nconfig.additional_python_paths![i]}
|
|
/>
|
|
{#if canEditEEConfig}
|
|
<button
|
|
class="rounded-full bg-surface/60 hover:bg-surface-hover"
|
|
aria-label="Clear"
|
|
onclick={() => {
|
|
if (
|
|
nconfig.additional_python_paths === undefined ||
|
|
nconfig.additional_python_paths.length == 0
|
|
) {
|
|
return
|
|
}
|
|
nconfig.additional_python_paths.splice(i, 1)
|
|
nconfig.additional_python_paths = [...nconfig.additional_python_paths]
|
|
}}
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
{#if canEditEEConfig}
|
|
<div class="flex">
|
|
<Button
|
|
variant="default"
|
|
size="xs"
|
|
startIcon={{ icon: Plus }}
|
|
on:click={() => {
|
|
if (nconfig.additional_python_paths === undefined) {
|
|
nconfig.additional_python_paths = []
|
|
}
|
|
nconfig.additional_python_paths.push('')
|
|
nconfig.additional_python_paths = [...nconfig.additional_python_paths]
|
|
}}
|
|
>
|
|
Add additional Python path
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
</Label>
|
|
<Label
|
|
label="Local dependencies import names to skip during resolution"
|
|
tooltip="uv will not try to resolve dependencies for these packages, useful if you have packages pre-installed on the workers at a given path."
|
|
>
|
|
{#if nconfig.pip_local_dependencies}
|
|
{#each nconfig.pip_local_dependencies as _, i}
|
|
<div class="flex gap-1 items-center">
|
|
<input
|
|
disabled={!canEditEEConfig}
|
|
type="text"
|
|
placeholder="httpx"
|
|
bind:value={nconfig.pip_local_dependencies[i]}
|
|
/>
|
|
{#if canEditEEConfig}
|
|
<button
|
|
class="rounded-full bg-surface/60 hover:bg-surface-hover"
|
|
aria-label="Clear"
|
|
onclick={() => {
|
|
if (
|
|
nconfig.pip_local_dependencies === undefined ||
|
|
nconfig.pip_local_dependencies.length == 0
|
|
) {
|
|
return
|
|
}
|
|
nconfig.pip_local_dependencies.splice(i, 1)
|
|
nconfig.pip_local_dependencies = [...nconfig.pip_local_dependencies]
|
|
}}
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
{#if canEditEEConfig}
|
|
<div class="flex">
|
|
<Button
|
|
variant="default"
|
|
size="xs"
|
|
startIcon={{ icon: Plus }}
|
|
on:click={() => {
|
|
if (nconfig.pip_local_dependencies === undefined) {
|
|
nconfig.pip_local_dependencies = []
|
|
}
|
|
nconfig.pip_local_dependencies.push('')
|
|
nconfig.pip_local_dependencies = [...nconfig.pip_local_dependencies]
|
|
}}
|
|
>
|
|
Add PIP local dependency
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
</Label>
|
|
</Section>
|
|
|
|
<div class="mt-8"></div>
|
|
|
|
<Section
|
|
label="Init script"
|
|
tooltip="Bash script run at start of the workers. More lightweight than requiring custom worker images."
|
|
collapsable
|
|
initiallyCollapsed={nconfig.init_bash === undefined}
|
|
>
|
|
{#snippet header()}
|
|
<div class="ml-4 flex flex-row gap-2 items-center">
|
|
{#if nconfig.init_bash !== undefined}
|
|
<Badge color="green">Enabled</Badge>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
{#if (nconfig.init_bash ?? '') !== (config?.init_bash ?? '')}
|
|
<div class="mb-2">
|
|
<Alert size="xs" type="info" title="Worker restart required">
|
|
Workers will get killed upon detecting changes to the init script. It is assumed they
|
|
are in an environment where the supervisor will restart them.
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
<p class="text-xs text-secondary mb-2">
|
|
Execution logs are visible on the runs page of the admins workspace. Use the script editor
|
|
with Bash to iterate on your script before setting it here.
|
|
</p>
|
|
<div class="border w-full h-40">
|
|
<Editor
|
|
fixedOverflowWidgets={true}
|
|
disabled={!canEditConfig}
|
|
class="flex flex-1 grow h-full w-full"
|
|
automaticLayout
|
|
scriptLang={'bash'}
|
|
useWebsockets={false}
|
|
code={config?.init_bash ?? ''}
|
|
on:change={(e) => {
|
|
if (config) {
|
|
const code = e.detail
|
|
if (code != '') {
|
|
nconfig.init_bash = code?.replace(/\r\n/g, '\n')
|
|
} else {
|
|
nconfig.init_bash = undefined
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</Section>
|
|
|
|
<div class="mt-8"></div>
|
|
|
|
<Section
|
|
label="Periodic script"
|
|
tooltip="Bash script run periodically at configurable intervals. Useful for maintenance tasks like cleaning disk space."
|
|
collapsable
|
|
eeOnly={!hasEnterpriseFeatures}
|
|
initiallyCollapsed={nconfig.periodic_script_bash === undefined}
|
|
>
|
|
{#snippet header()}
|
|
<div class="ml-4 flex flex-row gap-2 items-center">
|
|
{#if nconfig.periodic_script_bash !== undefined}
|
|
<Badge color="green">Enabled</Badge>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
{#if (nconfig.periodic_script_bash ?? '') !== (config?.periodic_script_bash ?? '') || (nconfig.periodic_script_interval_seconds ?? 0) !== (config?.periodic_script_interval_seconds ?? 0)}
|
|
<div class="mb-2">
|
|
<Alert size="xs" type="info" title="Worker restart required">
|
|
Workers will get killed upon detecting changes to the periodic script or interval. It is
|
|
assumed they are in an environment where the supervisor will restart them.
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex gap-4 items-center mb-4">
|
|
<Label label="Execution interval (seconds)" for="periodic-script-interval-seconds">
|
|
<TextInput
|
|
inputProps={{
|
|
disabled: !canEditEEConfig,
|
|
type: 'number',
|
|
min: '60',
|
|
placeholder: '3600',
|
|
class: '!w-24 '
|
|
}}
|
|
bind:value={nconfig.periodic_script_interval_seconds}
|
|
/>
|
|
<span class="text-2xs text-hint">Minimum: 60 seconds</span>
|
|
</Label>
|
|
</div>
|
|
|
|
<div class="border w-full h-40">
|
|
<Editor
|
|
disabled={!canEditEEConfig}
|
|
class="flex flex-1 grow h-full w-full"
|
|
automaticLayout
|
|
scriptLang={'bash'}
|
|
useWebsockets={false}
|
|
fixedOverflowWidgets={false}
|
|
code={config?.periodic_script_bash ?? ''}
|
|
on:change={(e) => {
|
|
if (config) {
|
|
const code = e.detail
|
|
if (code != '') {
|
|
nconfig.periodic_script_bash = code?.replace(/\r\n/g, '\n')
|
|
} else {
|
|
nconfig.periodic_script_bash = undefined
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</Section>
|
|
|
|
<div class="mt-8">
|
|
<Section label="Set via API" collapsable headerClass="text-secondary">
|
|
<p class="text-xs text-tertiary">Requires a superadmin token.</p>
|
|
<pre
|
|
class="mt-1 p-2 bg-surface-secondary rounded text-xs overflow-x-auto whitespace-pre-wrap break-all"
|
|
>curl -X POST '{window.location.origin}/api/configs/update/worker__{name}' \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'Authorization: Bearer <token>' \
|
|
-d '{JSON.stringify(cleanWorkerGroupConfig(nconfig ?? {}), null, 2)}'</pre
|
|
>
|
|
</Section>
|
|
</div>
|
|
{#snippet actions()}
|
|
<div class="flex gap-4 items-center">
|
|
{#if onOpenYamlEditor}
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="md"
|
|
startIcon={{ icon: FileCode }}
|
|
onClick={() => {
|
|
drawer?.closeDrawer()
|
|
onOpenYamlEditor?.()
|
|
}}
|
|
>
|
|
YAML editor
|
|
</Button>
|
|
{/if}
|
|
<div class="flex gap-2 items-center">
|
|
{#if canEditConfig}
|
|
<Button
|
|
variant="accent"
|
|
on:click={async () => {
|
|
if (
|
|
nconfig?.min_alive_workers_alert_threshold &&
|
|
nconfig?.min_alive_workers_alert_threshold < 1
|
|
) {
|
|
sendUserToast('Minimum alive workers alert threshold must be at least 1', true)
|
|
return
|
|
}
|
|
if (
|
|
nconfig?.periodic_script_bash &&
|
|
(!nconfig?.periodic_script_interval_seconds ||
|
|
nconfig?.periodic_script_interval_seconds < 60)
|
|
) {
|
|
sendUserToast('Periodic script interval must be at least 60 seconds', true)
|
|
return
|
|
}
|
|
// Remove duplicate env vars by keeping only the last occurrence of each key
|
|
const seenKeys = new Set()
|
|
customEnvVars = customEnvVars
|
|
.reverse()
|
|
.filter((envVar) => {
|
|
if (envVar.key == '') {
|
|
return false
|
|
}
|
|
if (seenKeys.has(envVar.key)) {
|
|
return false
|
|
}
|
|
seenKeys.add(envVar.key)
|
|
return true
|
|
})
|
|
.reverse()
|
|
nconfig.env_vars_static = new Map()
|
|
nconfig.env_vars_allowlist = []
|
|
customEnvVars.forEach((envvar) => {
|
|
if (
|
|
nconfig.env_vars_static !== undefined &&
|
|
nconfig.env_vars_allowlist !== undefined &&
|
|
!emptyString(envvar.key)
|
|
) {
|
|
if (envvar.type === 'dynamic') {
|
|
nconfig.env_vars_allowlist.push(envvar.key)
|
|
} else {
|
|
nconfig.env_vars_static[envvar.key] = envvar.value
|
|
}
|
|
}
|
|
})
|
|
|
|
await ConfigService.updateConfig({ name: 'worker__' + name, requestBody: nconfig })
|
|
sendUserToast('Configuration set')
|
|
dispatch('reload')
|
|
}}
|
|
disabled={(!hasChanges &&
|
|
!hasEnvVarChanges &&
|
|
nconfig?.dedicated_worker == undefined) ||
|
|
!canEditConfig}
|
|
>
|
|
Apply changes
|
|
</Button>
|
|
{:else}
|
|
<span class="text-secondary text-xs">Read only</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<div
|
|
class="flex flex-col gap-6 items-center justify-between mt-2 rounded-md border border-light p-4 bg-surface-tertiary"
|
|
>
|
|
<div class="flex gap-2 justify-between w-full">
|
|
<div class="text-xs flex flex-row gap-2 items-center">
|
|
<div class="flex flex-row gap-1 items-center">
|
|
{#if selectGroup}
|
|
{@render selectGroup()}
|
|
<span class="ml-r"></span>
|
|
{:else}
|
|
<span class="text-secondary text-xs">Worker group:</span>
|
|
<span class="text-emphasis font-semibold text-sm">{name} - </span>
|
|
{/if}
|
|
<span class="inline-flex">
|
|
{`${pluralize(activeWorkers, 'worker')} `}
|
|
<Tooltip
|
|
>{#snippet text()}Number of active workers of this group in the last 15 seconds{/snippet}</Tooltip
|
|
></span
|
|
>
|
|
{#if isNativeMode}
|
|
<Badge color="blue" small>Native</Badge>
|
|
{/if}
|
|
</div>
|
|
|
|
{#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 hasWorkersWithoutIsolation(workers)}
|
|
{@const unsafeWorkers = getWorkersWithoutIsolation(workers)}
|
|
<div class="flex justify-end">
|
|
<Tooltip placement="bottom">
|
|
<TriangleAlert size={14} class="text-yellow-600" />
|
|
|
|
{#snippet text()}
|
|
<div class="flex flex-col gap-2 text-xs max-w-md p-4">
|
|
<div class="font-semibold text-emphasis">Workers without job isolation</div>
|
|
<p class="text-primary">
|
|
{unsafeWorkers.length}
|
|
{unsafeWorkers.length === 1 ? 'worker' : 'workers'} in this group
|
|
{unsafeWorkers.length === 1 ? 'is' : 'are'} running without job isolation (nsjail/unshare).
|
|
</p>
|
|
<div class="flex flex-wrap gap-1">
|
|
{#each unsafeWorkers as worker}
|
|
<Badge color="orange" small>
|
|
{worker.worker}
|
|
</Badge>
|
|
{/each}
|
|
</div>
|
|
<a href="https://www.windmill.dev/docs/advanced/security_isolation" target="_blank">
|
|
Learn more about job isolation <ExternalLink size={12} class="inline-block" />
|
|
</a>
|
|
</div>
|
|
{/snippet}
|
|
</Tooltip>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{#if !isAgent}
|
|
<div class="flex gap-4 items-center justify-end flex-row">
|
|
{#if canEditConfig}
|
|
{#if width > 1000}
|
|
{#if config}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
on:click={() => {
|
|
if (!hasEnterpriseFeatures) {
|
|
sendUserToast('Worker Management UI is an EE feature', true)
|
|
} else {
|
|
openDelete = true
|
|
}
|
|
}}
|
|
startIcon={{ icon: Trash }}
|
|
destructive
|
|
>
|
|
Delete config
|
|
</Button>
|
|
{/if}
|
|
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
on:click={() => {
|
|
loadNConfig()
|
|
openClean = true
|
|
}}
|
|
startIcon={{ icon: RefreshCcwIcon }}
|
|
destructive
|
|
>
|
|
Clean cache
|
|
</Button>
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
on:click={() => {
|
|
openRestart = true
|
|
}}
|
|
startIcon={{ icon: Power }}
|
|
destructive
|
|
>
|
|
Restart workers
|
|
</Button>
|
|
{:else}
|
|
<Dropdown
|
|
items={[
|
|
{
|
|
displayName: 'Clean cache',
|
|
action: () => {
|
|
loadNConfig()
|
|
openClean = true
|
|
},
|
|
disabled: !config,
|
|
type: 'delete'
|
|
},
|
|
{
|
|
displayName: 'Restart workers',
|
|
action: () => {
|
|
openRestart = true
|
|
},
|
|
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}
|
|
<Button
|
|
variant="accent"
|
|
unifiedSize="sm"
|
|
on:click={() => {
|
|
loadNConfig()
|
|
drawer?.openDrawer()
|
|
}}
|
|
startIcon={{ icon: config == undefined ? Plus : Settings }}
|
|
>
|
|
<div class="flex flex-row gap-1 items-center">
|
|
{config == undefined ? 'Create' : 'Edit'} config
|
|
</div>
|
|
</Button>
|
|
{:else if config}
|
|
<Button
|
|
unifiedSize="md"
|
|
variant="accent"
|
|
on:click={() => {
|
|
loadNConfig()
|
|
drawer?.openDrawer()
|
|
}}
|
|
>
|
|
See config
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if config?.worker_tags && config.worker_tags.length > 0}
|
|
<div class="flex flex-row items-start gap-2 w-full">
|
|
<div class="text-secondary text-xs mt-1">Tags:</div>
|
|
<TagList tags={config.worker_tags} maxVisible={25} class="flex-wrap" />
|
|
</div>
|
|
{:else if config?.dedicated_workers && config.dedicated_workers.length > 0}
|
|
<div class="flex flex-row items-start gap-2 w-full min-w-0">
|
|
<div class="text-secondary text-xs mt-1 flex-shrink-0">Dedicated to:</div>
|
|
<div class="flex flex-wrap gap-1 min-w-0">
|
|
{#each config.dedicated_workers as dw}
|
|
<div
|
|
class="text-xs bg-surface-secondary px-2 py-1 rounded text-primary font-mono truncate max-w-xs"
|
|
title={hashedDedicatedTags.get(dw) ?? dw}
|
|
>
|
|
{hashedDedicatedTags.get(dw) ?? dw}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{:else if config?.dedicated_worker}
|
|
<div class="flex flex-row items-start gap-2 w-full min-w-0">
|
|
<div class="text-secondary text-xs mt-1 flex-shrink-0">Dedicated to:</div>
|
|
<div
|
|
class="text-xs bg-surface-secondary px-2 py-1 rounded text-primary font-mono truncate max-w-xs"
|
|
title={hashedDedicatedTags.get(config.dedicated_worker) ?? config.dedicated_worker}
|
|
>
|
|
{hashedDedicatedTags.get(config.dedicated_worker) ?? config.dedicated_worker}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if isAgent}
|
|
<div class="mt-4">
|
|
<Alert type="info" title="Agent Worker Group" size="xs">
|
|
{#snippet children()}
|
|
This group is formed with agent workers, there is no associated config. {#if $superadmin || $devopsRole}
|
|
To modify the tags, generate a new <a
|
|
href="https://www.windmill.dev/docs/core_concepts/agent_workers#quickstart"
|
|
target="_blank"
|
|
class="underline">JWT token <ExternalLink size={12} class="inline-block" /></a
|
|
>.{/if}
|
|
{/snippet}
|
|
</Alert>
|
|
</div>
|
|
{/if}
|