Files
windmill/frontend/src/lib/components/AutoscalingConfigEditor.svelte
T
Diego Imbert fa8d1b47db fix: new MultiSelect component (#5979)
* rename all multiselects to Legacy

* move Select

* Separate SelectDropdown

* fix click outside for select with portal

* multiselect v0

* multiselect clear btn

* move filterText logic to SelectDropdown

* ui nits

* console.log

* draggable

* Draggable multiselect

* multiselect search

* nit refacto

* autofocus multiselect input

* Replace in AppMultiSelectV2

* search icon

* app multi select nits

* arginput update multiselect

* fix autofocus scrolling up

* replace High priority tags multiselect

* autoscaling config editor multiselect replace

* fix clear btn not in border

* replace multiselect in cron input

* replace multiselect in savedinputs

* replace EventHandlerItem multiselect

* select dropdown shadow

* more multiselect migration

* hover opacity on drag

* TokensTable UI fixes + replace multiselect

* ai settings replace multiselect

* DefaultTags Multiselect replace

* prevent multiselect from opening on drag

* nit

* app multiselect css + simplify

* console log

* safeSelectItems cleanup

* Remove svelte-multiselect

* clip when wrap not allowed

* hide duplicate app components

* CSS works better  with multiselect in app editor

* allowOverflow

* allowClear

* fix custom createText messed up with search
2025-06-19 14:37:17 +02:00

345 lines
9.8 KiB
Svelte

<script lang="ts">
import type { AutoscalingConfig } from './worker_group'
import Toggle from './Toggle.svelte'
import Section from './Section.svelte'
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 { ExternalLink } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Label from './Label.svelte'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
interface Props {
config: AutoscalingConfig | undefined
worker_tags: string[] | undefined
}
let { config = $bindable(), worker_tags }: Props = $props()
const dispatch = createEventDispatcher()
let test_input: number = $state(3)
</script>
<div class="flex flex-row gap-16 pt-2">
<div class="space-y-4 flex flex-col gap-1 max-w-xs text-sm">
<h5>Rules</h5>
<Toggle
checked={config?.enabled ?? false}
options={{ right: 'Enabled' }}
on:change={(e) => {
dispatch('dirty')
if (e.detail) {
if (!config) {
config = {
enabled: true,
min_workers: 3,
max_workers: 10,
integration: { type: 'dryrun' }
}
} else {
config.enabled = true
}
} else {
config = {
...(config ?? {
min_workers: 3,
max_workers: 10,
integration: { type: 'dryrun' }
}),
enabled: false
}
}
}}
/>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Min # of Workers
{#if config !== undefined}
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.min_workers} />
{#if config.min_workers !== undefined && config.min_workers != undefined && config.min_workers > config.max_workers}
<div class="text-red-600 text-xs whitespace-nowrap"
>Minimum cannot be {'>'} to Maximum</div
>
{/if}
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Max # of Workers
{#if config !== undefined}
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.max_workers} />
{:else}
<input type="number" disabled />
{/if}
</label>
<div class="p-2">
<Section label="Advanced" small collapsable={true}>
<div class="flex flex-col gap-2 text-2xs">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after an incremental scale-in/out
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
placeholder="300"
bind:value={config.cooldown_seconds}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after a full scale out
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
placeholder="1500"
bind:value={config.full_scale_cooldown_seconds}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger an incremental scale-out
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
bind:value={config.inc_scale_num_jobs_waiting}
placeholder="1"
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger a full scale out <Tooltip
>Default: max_workers, full scale out = scale out to max workers</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
placeholder="max workers"
bind:value={config.full_scale_jobs_waiting}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate % threshold to go below to trigger a scale-in (decrease) <Tooltip
>Default: 25%, need to go below average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
max="100"
placeholder="25"
bind:value={config.dec_scale_occupancy_rate}
/>
{:else}
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate threshold to exceed to trigger an incremental scale-out (increase) <Tooltip
>Default: 75%, need to exceed average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
max="100"
placeholder="75"
bind:value={config.inc_scale_occupancy_rate}
/>
{:else}
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num workers to scale-in/out by when incremental <Tooltip
>Default: (max_workers - min_workers) / 5</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="1"
placeholder="(max_workers - min_workers) / 5"
bind:value={config.inc_num_workers}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<Label label="Custom tags to autoscale on">
{#snippet header()}
<Tooltip>
By default, autoscaling will apply to the tags the worker group is assigned to but
you can override this here.
</Tooltip>
{/snippet}
{#if config}
{#if config.custom_tags}
<MultiSelect
bind:value={
() => config?.custom_tags ?? [],
(v) => {
config && (config.custom_tags = v.length ? v : undefined)
dispatch('dirty')
}
}
items={safeSelectItems(worker_tags)}
placeholder="Tags"
/>
{:else}
<Button
color="light"
size="xs"
variant="contained"
on:click={() => {
if (config) {
config.custom_tags = []
dispatch('dirty')
}
}}>Add custom tags</Button
>
{/if}
{/if}
</Label>
</div>
</Section>
</div>
</div>
<div class="flex flex-col gap-1 max-w-xs text-sm">
<h5>Integration</h5>
{#if config?.integration}
<ToggleButtonGroup
on:selected={(e) => dispatch('dirty')}
bind:selected={config.integration.type}
class="mb-4 mt-2"
>
{#snippet children({ item })}
<ToggleButton
value="dryrun"
label="Dry run"
tooltip="See autoscaling events but not actual scaling actions will be performed"
{item}
/>
<ToggleButton
value="script"
label="Custom script"
tooltip="Run a custom script to scale your worker group"
{item}
/>
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
{/snippet}
</ToggleButtonGroup>
{#if config.integration.type === 'script'}
<label>
Script path on the 'admins' workspace
<input
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.path}
/>
</label>
<label>
Custom tag for executing script (optional)
{#if config.integration.tag}
<input
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.tag}
/>
{:else}
<Button
color="light"
size="xs"
variant="contained"
on:click={() => {
if (config?.integration?.type === 'script') {
config.integration.tag = 'bash'
dispatch('dirty')
}
}}>Set tag</Button
>
{/if}
</label>
<div class="flex mt-6 gap-2">
<Button
color="dark"
target="_blank"
endIcon={{ icon: ExternalLink }}
href="/scripts/add?hub=hub%2F9204%2Fhelper%2FScale%20a%20worker%20group%20deployed%20as%20a%20kubernetes%20service&workspace=admins"
>Create from template</Button
>
<Button
color="dark"
target="_blank"
href={`/runs/${config.integration.path}?workspace=admins`}
endIcon={{ icon: ExternalLink }}
>
See jobs
</Button>
</div>
<div class="flex flex-row gap-2 mt-4">
<Button color="light" size="xs" variant="contained">Test scaling</Button>
<div class="flex text-xs flex-row gap-2 items-center">
<input class="!w-16" type="number" bind:value={test_input} />
workers
</div>
</div>
{/if}
{:else}
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
{#snippet children({ item })}
<ToggleButton value="dryrun" label="Dry run" {item} />
<ToggleButton value="script" label="Custom script" {item} />
<ToggleButton value="ecs" label="ECS (soon)" {item} />
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
{/snippet}
</ToggleButtonGroup>
<label>
Script path on the 'admins' workspace
<input type="text" disabled />
</label>
{/if}
</div>
</div>