mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 08:02:43 +00:00
refactor: harmonize flow and script settings layout (#12013)
* refactor: harmonize flow and script settings layout Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep the priority input open while it is edited Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep settings rows open while edited and name the deploying user for non-admins Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: write a setting before its row opens and show its real state while edited Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: slide settings rows open to their full height Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: remount a settings row's fields when its toggle turns it on Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: put the AI prompt behind a settings toggle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: summarize active runtime and permission settings on the metadata tab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: move the active settings chips into the settings header Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: bring back the active setting icons on the settings tabs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
co-authored by
Claude Opus 5.5
Ruben Fiszel
parent
4a34dc69f5
commit
0249bb94ac
@@ -414,12 +414,18 @@ input[type='time']::-webkit-calendar-picker-indicator {
|
||||
}
|
||||
|
||||
/* Settings search highlight (used by both the drawer and the setup page) */
|
||||
[data-setting-key] {
|
||||
[data-setting-key],
|
||||
[data-setting] {
|
||||
transition: outline 0.8s ease;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[data-setting-key].setting-highlight {
|
||||
[data-setting].setting-highlight {
|
||||
outline-offset: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
[data-setting-key].setting-highlight,
|
||||
[data-setting].setting-highlight {
|
||||
outline: 2px solid rgb(var(--color-border-accent, 59 130 246));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Toggle from './Toggle.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ScopeSelector from './settings/ScopeSelector.svelte'
|
||||
import SettingToggle from './itemSettings/SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
/** `null`/`undefined`: the job token has the full permissions of the identity it runs as. */
|
||||
value: string[] | null | undefined
|
||||
kind: 'script' | 'flow' | 'step'
|
||||
/** Match the toggles listed around it. */
|
||||
size?: 'xs' | 'sm'
|
||||
textClass?: string
|
||||
}
|
||||
|
||||
let { value = $bindable(), kind, size = 'sm', textClass }: Props = $props()
|
||||
let { value = $bindable(), kind }: Props = $props()
|
||||
|
||||
const OIDC_ONLY = ['oidc:write']
|
||||
|
||||
@@ -32,58 +29,48 @@
|
||||
|
||||
function setMode(next: Mode) {
|
||||
customPicked = next === 'custom'
|
||||
value =
|
||||
next === 'oidc_only' ? [...OIDC_ONLY] : next === 'no_api' ? [] : [...(value ?? [])]
|
||||
value = next === 'oidc_only' ? [...OIDC_ONLY] : next === 'no_api' ? [] : [...(value ?? [])]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Toggle
|
||||
{size}
|
||||
{textClass}
|
||||
checked={value != null}
|
||||
on:change={() => {
|
||||
if (value != null) {
|
||||
value = null
|
||||
} else {
|
||||
setMode('oidc_only')
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Restrict the job token',
|
||||
rightTooltip:
|
||||
"Limit what the WM_TOKEN of this " +
|
||||
kind +
|
||||
"'s jobs can do through the API, e.g. only mint an OIDC token. Use it for code that handles untrusted input, such as AI agents."
|
||||
}}
|
||||
/>
|
||||
{#if value != null}
|
||||
<ToggleButtonGroup selected={mode} onSelected={(v) => setMode(v)}>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="oidc_only" label="OIDC token only" {item} />
|
||||
<ToggleButton value="no_api" label="No API access" {item} />
|
||||
<ToggleButton value="custom" label="Custom" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if mode === 'custom'}
|
||||
<ScopeSelector
|
||||
bind:selectedScopes={() => value ?? [], (scopes) => (value = [...scopes])}
|
||||
emptyLabel="No scopes selected. The job token can only reach its own job."
|
||||
/>
|
||||
{/if}
|
||||
<p class="text-2xs text-secondary">
|
||||
{#if kind === 'step'}
|
||||
The <code>WM_TOKEN</code> of every job this step runs can only do what these scopes allow, on top
|
||||
of the flow's own restriction. Loop and branch bodies, and AI agent tools, inherit it.
|
||||
{:else}
|
||||
The <code>WM_TOKEN</code> of every job of this {kind} can only do what these scopes allow. Jobs
|
||||
it starts, {kind === 'flow' ? 'its steps, ' : ''}and AI agent tools inherit the restriction.
|
||||
{/if}
|
||||
Variables and resources passed as <code>$var:</code>/<code>$res:</code> inputs, an AI agent's provider
|
||||
resource, relative imports and object storage each need a read scope, and script state
|
||||
(<code>getState</code>/<code>setState</code>) needs <code>resources:write</code>. Write scopes on scripts,
|
||||
flows, schedules or triggers let a job escape the restriction. Jobs with a restricted token never
|
||||
run on dedicated workers.
|
||||
</p>
|
||||
<SettingToggle
|
||||
setting="job-token"
|
||||
label="Restrict the job token"
|
||||
tooltip={`Limit what the WM_TOKEN of this ${kind}'s jobs can do through the API, e.g. only mint an OIDC token. Use it for code that handles untrusted input, such as AI agents.`}
|
||||
checked={value != null}
|
||||
onchange={(on) => {
|
||||
if (on) {
|
||||
setMode('oidc_only')
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButtonGroup selected={mode} onSelected={(v) => setMode(v)}>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="oidc_only" label="OIDC token only" {item} />
|
||||
<ToggleButton value="no_api" label="No API access" {item} />
|
||||
<ToggleButton value="custom" label="Custom" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if mode === 'custom'}
|
||||
<ScopeSelector
|
||||
bind:selectedScopes={() => value ?? [], (scopes) => (value = [...scopes])}
|
||||
emptyLabel="No scopes selected. The job token can only reach its own job."
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-2xs text-secondary">
|
||||
{#if kind === 'step'}
|
||||
The <code>WM_TOKEN</code> of every job this step runs can only do what these scopes allow, on top
|
||||
of the flow's own restriction. Loop and branch bodies, and AI agent tools, inherit it.
|
||||
{:else}
|
||||
The <code>WM_TOKEN</code> of every job of this {kind} can only do what these scopes allow. Jobs
|
||||
it starts, {kind === 'flow' ? 'its steps, ' : ''}and AI agent tools inherit the restriction.
|
||||
{/if}
|
||||
Variables and resources passed as <code>$var:</code>/<code>$res:</code> inputs, an AI agent's
|
||||
provider resource, relative imports and object storage each need a read scope, and script state
|
||||
(<code>getState</code>/<code>setState</code>) needs <code>resources:write</code>. Write scopes
|
||||
on scripts, flows, schedules or triggers let a job escape the restriction. Jobs with a
|
||||
restricted token never run on dedicated workers.
|
||||
</p>
|
||||
</SettingToggle>
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createBubbler } from 'svelte/legacy'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import { SecondsInput } from '$lib/components/common'
|
||||
import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte'
|
||||
import DebounceLimit from '$lib/components/flows/DebounceLimit.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { ScriptAdvancedSettingsFields } from './scriptSettings'
|
||||
|
||||
const bubble = createBubbler()
|
||||
|
||||
interface Props {
|
||||
script: ScriptAdvancedSettingsFields
|
||||
// Workspace to read worker tags from (defaults to $workspaceStore inside the picker).
|
||||
workspaceId?: string | undefined
|
||||
}
|
||||
|
||||
let { script = $bindable(), workspaceId = undefined }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<Section label="Worker group tag (queue)">
|
||||
{#snippet header()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups">
|
||||
The script will be executed on a worker configured to listen to this worker group tag
|
||||
(queue). For instance, you could setup an "highmem", or "gpu" tag.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<WorkerTagPicker bind:tag={script.tag} {workspaceId} />
|
||||
</Section>
|
||||
|
||||
<Section label="Concurrency limits" eeOnly>
|
||||
{#snippet header()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits">
|
||||
Allowed concurrency within a given timeframe
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={Boolean(script.concurrent_limit)}
|
||||
on:change={() => {
|
||||
if (script.concurrent_limit && script.concurrent_limit != undefined) {
|
||||
script.concurrent_limit = undefined
|
||||
script.concurrency_time_window_s = undefined
|
||||
script.concurrency_key = undefined
|
||||
} else {
|
||||
script.concurrent_limit = 1
|
||||
}
|
||||
}}
|
||||
options={{ right: 'Concurrency limits' }}
|
||||
/>
|
||||
{#if Boolean(script.concurrent_limit)}
|
||||
<div class="flex flex-col gap-4 mt-2">
|
||||
<Label label="Max number of executions within the time window">
|
||||
<div class="flex flex-row gap-2 max-w-sm whitespace-nowrap">
|
||||
<input
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:value={script.concurrent_limit}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<Label label="Time window in seconds">
|
||||
<SecondsInput
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:seconds={script.concurrency_time_window_s}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Custom concurrency key (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits#custom-concurrency-key"
|
||||
>
|
||||
Concurrency keys are global, you can have them be workspace specific using the
|
||||
variable `$workspace`. You can also use an argument's value using `$args[name_of_arg]`</Tooltip
|
||||
>
|
||||
{/snippet}
|
||||
<input
|
||||
disabled={!$enterpriseLicense}
|
||||
type="text"
|
||||
bind:value={script.concurrency_key}
|
||||
placeholder={`$workspace/script/${script.path ?? ''}-$args[foo]`}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<Section label="Cache">
|
||||
{#snippet header()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/caching">
|
||||
Cache the results for each possible inputs
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<div class="flex gap-2 shrink flex-col">
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:checked={() => !!script.cache_ttl, (v) => (script.cache_ttl = v ? 300 : undefined)}
|
||||
options={{ right: 'Cache the results for each possible inputs' }}
|
||||
/>
|
||||
{#if script.cache_ttl}
|
||||
<div class="text-2xs text-secondary">How long to keep the cache valid</div>
|
||||
<SecondsInput bind:seconds={script.cache_ttl} />
|
||||
<Toggle
|
||||
size="2xs"
|
||||
bind:checked={
|
||||
() => script.cache_ignore_s3_path, (v) => (script.cache_ignore_s3_path = v || undefined)
|
||||
}
|
||||
options={{
|
||||
right: 'Ignore S3 Object paths for caching purposes',
|
||||
rightTooltip:
|
||||
'If two S3 objects passed as input have the same content, they will hit the same cache entry, regardless of their path.'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section label="Timeout">
|
||||
{#snippet header()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/script_editor/settings#timeout">
|
||||
Add a custom timeout for this script
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<div class="flex gap-2 shrink flex-col">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={Boolean(script.timeout)}
|
||||
on:change={() => {
|
||||
if (script.timeout && script.timeout != undefined) {
|
||||
script.timeout = undefined
|
||||
} else {
|
||||
script.timeout = 300
|
||||
}
|
||||
}}
|
||||
options={{ right: 'Add a custom timeout for this script' }}
|
||||
/>
|
||||
{#if Boolean(script.timeout)}
|
||||
<span class="text-xs font-semibold text-emphasis leading-none mt-2">Timeout duration</span>
|
||||
<SecondsInput bind:seconds={script.timeout} />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section label="Debouncing">
|
||||
{#snippet header()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/job_debouncing">
|
||||
Debounce Jobs
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<DebounceLimit
|
||||
size="sm"
|
||||
bind:debounce_delay_s={script.debounce_delay_s}
|
||||
bind:debounce_key={script.debounce_key}
|
||||
bind:debounce_args_to_accumulate={script.debounce_args_to_accumulate}
|
||||
bind:max_total_debouncing_time={script.max_total_debouncing_time}
|
||||
bind:max_total_debounces_amount={script.max_total_debounces_amount}
|
||||
schema={script.schema as Schema}
|
||||
placeholder={`$workspace/script/${script.path ?? ''}-$args[foo]`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="Perpetual script">
|
||||
{#snippet header()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/script_editor/perpetual_scripts">
|
||||
Restart the script upon ending unless cancelled
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={Boolean(script.restart_unless_cancelled)}
|
||||
on:change={() => {
|
||||
script.restart_unless_cancelled = script.restart_unless_cancelled ? undefined : true
|
||||
}}
|
||||
options={{ right: 'Restart upon ending unless cancelled' }}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="Dedicated workers" eeOnly>
|
||||
{#snippet header()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/dedicated_workers">
|
||||
In this mode, the script is meant to be run on dedicated workers that run the script at
|
||||
native speed. Can reach >1500rps per dedicated worker. Only available on enterprise
|
||||
edition and for Python3, Deno, Bun and Bunnative.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense ||
|
||||
isCloudHosted() ||
|
||||
(script.language != 'bun' &&
|
||||
script.language != 'bunnative' &&
|
||||
script.language != 'python3' &&
|
||||
script.language != 'deno')}
|
||||
size="sm"
|
||||
checked={Boolean(script.dedicated_worker)}
|
||||
on:change={() => {
|
||||
script.dedicated_worker = script.dedicated_worker ? undefined : true
|
||||
}}
|
||||
options={{ right: 'Script is run on dedicated workers' }}
|
||||
/>
|
||||
{#if script.dedicated_worker}
|
||||
<div class="py-2">
|
||||
<Alert type="info" title="Require dedicated workers">
|
||||
A worker group needs to be configured to listen to this script. Select it in the dedicated
|
||||
workers section of the worker group configuration.
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<Section label="Delete after completion" eeOnly>
|
||||
{#snippet header()}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/script_editor/settings#delete-after-use"
|
||||
>
|
||||
The logs, arguments and results of the job will be completely deleted from Windmill after
|
||||
the specified delay once it is complete. Set to 0 for immediate deletion. The deletion is
|
||||
irreversible. This settings ONLY applies when the script is used within a flow or triggered
|
||||
synchronously.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<div class="flex gap-2 shrink flex-col">
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense}
|
||||
size="sm"
|
||||
checked={script.delete_after_secs != null}
|
||||
on:change={() => {
|
||||
script.delete_after_secs = script.delete_after_secs != null ? undefined : 0
|
||||
}}
|
||||
options={{ right: 'Delete logs, arguments and results after completion' }}
|
||||
/>
|
||||
{#if script.delete_after_secs != null}
|
||||
<SecondsInput bind:seconds={script.delete_after_secs} disabled={!$enterpriseLicense} />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{#if !isCloudHosted()}
|
||||
<Section label="High priority script" eeOnly>
|
||||
{#snippet header()}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/jobs#high-priority-jobs"
|
||||
>
|
||||
Jobs from script labeled as high priority take precedence over the other jobs when in the
|
||||
jobs queue.
|
||||
{#if !$enterpriseLicense}This is a feature only available on enterprise edition.{/if}
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
size="sm"
|
||||
checked={script.priority !== undefined && script.priority > 0}
|
||||
on:change={() => {
|
||||
script.priority = script.priority ? undefined : 100
|
||||
}}
|
||||
options={{ right: 'Label as high priority' }}
|
||||
>
|
||||
{#snippet right()}
|
||||
<input
|
||||
type="number"
|
||||
class="!w-16 ml-4"
|
||||
disabled={script.priority === undefined}
|
||||
bind:value={script.priority}
|
||||
onfocus={bubble('focus')}
|
||||
onchange={() => {
|
||||
if (script.priority && script.priority > 100) {
|
||||
script.priority = 100
|
||||
} else if (script.priority && script.priority < 0) {
|
||||
script.priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
</Toggle>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Label from '../Label.svelte'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
import SettingToggle from '../itemSettings/SettingToggle.svelte'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
|
||||
interface Props {
|
||||
@@ -9,12 +9,22 @@
|
||||
}
|
||||
|
||||
let { prompt = $bindable(), type = 'script' }: Props = $props()
|
||||
|
||||
// An empty prompt is stored as undefined, so the row keeps its own open state to show the field
|
||||
// between turning the toggle on and typing.
|
||||
let open = $state(false)
|
||||
</script>
|
||||
|
||||
{#if !$copilotInfo.workspaceDisabled}
|
||||
<Label
|
||||
<SettingToggle
|
||||
setting="ai-prompt"
|
||||
label="AI prompt"
|
||||
tooltip="An AI choosing this {type}'s inputs already reads its description and each field's description. Anything written here is given to it as extra guidance: mention specific fields and how they interact."
|
||||
checked={open || !!prompt}
|
||||
onchange={(on) => {
|
||||
open = on
|
||||
if (!on) prompt = undefined
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
underlyingInputEl="textarea"
|
||||
@@ -24,5 +34,5 @@
|
||||
rows: 3
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</SettingToggle>
|
||||
{/if}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
kind: 'script' | 'flow'
|
||||
scriptOrFlowPath: string
|
||||
errorHandlerMuted: boolean | undefined
|
||||
textDisabled?: boolean
|
||||
color?: 'nord' | 'red' | 'blue' | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
kind,
|
||||
scriptOrFlowPath,
|
||||
errorHandlerMuted = $bindable(),
|
||||
textDisabled = false,
|
||||
color = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let toggleState = $state(errorHandlerMuted)
|
||||
|
||||
async function toggleErrorHandler(): Promise<void> {
|
||||
toggleState = !toggleState
|
||||
if ($operatingWorkspace !== undefined) {
|
||||
try {
|
||||
if (kind === 'flow') {
|
||||
await FlowService.toggleWorkspaceErrorHandlerForFlow({
|
||||
workspace: $operatingWorkspace,
|
||||
path: scriptOrFlowPath,
|
||||
requestBody: {
|
||||
muted: !errorHandlerMuted
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await ScriptService.toggleWorkspaceErrorHandlerForScript({
|
||||
workspace: $operatingWorkspace,
|
||||
path: scriptOrFlowPath,
|
||||
requestBody: {
|
||||
muted: !errorHandlerMuted
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
`Error while toggling Workspace Error Handler: ${error.body || error.message}`,
|
||||
true
|
||||
)
|
||||
toggleState = false
|
||||
return
|
||||
}
|
||||
errorHandlerMuted = !errorHandlerMuted
|
||||
toggleState = errorHandlerMuted
|
||||
sendUserToast(
|
||||
errorHandlerMuted ? 'Workspace error handler muted' : 'Workspace error handler active',
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Toggle
|
||||
textClass="font-medium"
|
||||
{color}
|
||||
size="xs"
|
||||
checked={toggleState}
|
||||
on:change={toggleErrorHandler}
|
||||
options={{
|
||||
right: 'Mute',
|
||||
rightTooltip: 'Disable workspace error handler, EE only',
|
||||
rightDocumentationLink:
|
||||
'https://www.windmill.dev/docs/core_concepts/error_handling#workspace-error-handler'
|
||||
}}
|
||||
{textDisabled}
|
||||
/>
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { SecondsInput } from '$lib/components/common'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import Label from '../Label.svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { slideDynamic } from '$lib/transitions'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import SettingToggle from '$lib/components/itemSettings/SettingToggle.svelte'
|
||||
|
||||
let {
|
||||
debounce_delay_s = $bindable(),
|
||||
@@ -14,11 +15,7 @@
|
||||
max_total_debouncing_time = $bindable(),
|
||||
max_total_debounces_amount = $bindable(),
|
||||
schema = undefined,
|
||||
placeholder,
|
||||
size = 'xs',
|
||||
color = undefined,
|
||||
fontClass = 'font-normal',
|
||||
indentContent = false
|
||||
placeholder
|
||||
}: {
|
||||
debounce_delay_s: number | undefined
|
||||
debounce_key: string | undefined
|
||||
@@ -27,11 +24,6 @@
|
||||
max_total_debounces_amount: number | undefined
|
||||
schema?: Schema
|
||||
placeholder: string
|
||||
size: 'xs' | 'sm'
|
||||
color?: 'nord' | undefined
|
||||
fontClass?: string
|
||||
/** Align the fields under the toggle's label, as the flow step settings do. */
|
||||
indentContent?: boolean
|
||||
} = $props()
|
||||
|
||||
// Check if an originalType like "string | string[]" is a top-level
|
||||
@@ -90,89 +82,83 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Presence, not truthiness: a single backspace takes the seeded 1s to 0 while the
|
||||
// user is still typing, and reading that as off would disable the field mid-edit.
|
||||
let off = $derived(!$enterpriseLicense || debounce_delay_s === undefined)
|
||||
let disabled = $derived(!$enterpriseLicense)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Toggle
|
||||
textClass={fontClass}
|
||||
{color}
|
||||
{size}
|
||||
disabled={!$enterpriseLicense}
|
||||
checked={debounce_delay_s !== undefined}
|
||||
on:change={() => {
|
||||
if (debounce_delay_s !== undefined) {
|
||||
debounce_delay_s = undefined
|
||||
debounce_key = undefined
|
||||
} else {
|
||||
debounce_delay_s = 1
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Debouncing',
|
||||
rightTooltip: 'Consolidate multiple executions into a single run within a time window',
|
||||
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/job_debouncing'
|
||||
}}
|
||||
class="py-1"
|
||||
eeOnly={true}
|
||||
<!-- Presence, not truthiness: a single backspace takes the seeded 1s to 0 while the user is
|
||||
still typing, and reading that as off would close the fields mid-edit. -->
|
||||
<SettingToggle
|
||||
setting="debounce"
|
||||
label="Debouncing"
|
||||
tooltip="Consolidate multiple executions into a single run within a time window"
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/job_debouncing"
|
||||
eeOnly
|
||||
{disabled}
|
||||
checked={debounce_delay_s !== undefined}
|
||||
onchange={(on) => {
|
||||
if (on) {
|
||||
debounce_delay_s = 1
|
||||
} else {
|
||||
debounce_delay_s = undefined
|
||||
debounce_key = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Label label="Delay in seconds">
|
||||
<SecondsInput {disabled} bind:seconds={debounce_delay_s} />
|
||||
</Label>
|
||||
<Label label="Custom debounce key (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
Debounce keys are global. Use `$workspace` to make one workspace specific, and
|
||||
`$args[name_of_arg]` to use an argument's value.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<TextInput bind:value={debounce_key} inputProps={{ type: 'text', disabled, placeholder }} />
|
||||
</Label>
|
||||
<Label label="Argument to accumulate (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
A list argument whose values are appended together across the debounced executions.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<!-- A saved argument the schema no longer has stays listed, so it can still be cleared. -->
|
||||
<Select
|
||||
items={(selectedArg && !arrayArgs.includes(selectedArg)
|
||||
? [...arrayArgs, selectedArg]
|
||||
: arrayArgs
|
||||
).map((arg) => ({ value: arg, label: arg }))}
|
||||
bind:value={() => selectedArg || undefined, (v) => (selectedArg = v ?? '')}
|
||||
placeholder="None"
|
||||
clearable
|
||||
disabled={disabled || (arrayArgs.length === 0 && !selectedArg)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if !off}
|
||||
<div class="flex flex-col gap-4 mt-2 {indentContent ? 'pl-9' : ''}" transition:slideDynamic>
|
||||
<Label label="Delay in seconds">
|
||||
<SecondsInput disabled={off} bind:seconds={debounce_delay_s} />
|
||||
</Label>
|
||||
<Label label="Custom debounce key (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
Debounce keys are global, you can have them be workspace specific using the variable
|
||||
`$workspace`. You can also use an argument's value using `$args[name_of_arg]`</Tooltip
|
||||
>
|
||||
{/snippet}
|
||||
<input type="text" disabled={off} bind:value={debounce_key} {placeholder} />
|
||||
</Label>
|
||||
<Label label="Argument to accumulate (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
Select a list-type argument to accumulate across debounced executions. Values from each
|
||||
debounced execution will be appended together.</Tooltip
|
||||
>
|
||||
{/snippet}
|
||||
<select disabled={off} bind:value={selectedArg}>
|
||||
<option value="">None</option>
|
||||
{#each arrayArgs as arg}
|
||||
<option value={arg}>{arg}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if arrayArgs.length === 0}
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
No list-type arguments found in schema. Define arguments with type "array" to enable
|
||||
accumulation.
|
||||
</div>
|
||||
{/if}
|
||||
</Label>
|
||||
<Label label="Max total debouncing time (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
Maximum total time (in seconds) that a job can be debounced before it must execute. Once
|
||||
this time is reached, the job will run regardless of ongoing debouncing.</Tooltip
|
||||
>
|
||||
{/snippet}
|
||||
<SecondsInput disabled={off} bind:seconds={max_total_debouncing_time} />
|
||||
</Label>
|
||||
<Label label="Max total debounces amount (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
Maximum number of times a job can be debounced before it must execute. Once this count
|
||||
is reached, the job will run regardless of ongoing debouncing.</Tooltip
|
||||
>
|
||||
{/snippet}
|
||||
<input type="number" disabled={off} bind:value={max_total_debounces_amount} min="0" />
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if arrayArgs.length === 0}
|
||||
<div class="text-2xs text-secondary">
|
||||
No list argument in the schema. Add an argument of type "array" to accumulate it.
|
||||
</div>
|
||||
{/if}
|
||||
</Label>
|
||||
<Label label="Max total debouncing time (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
The longest a run can be held back by debouncing. Past it, the run starts whatever
|
||||
debouncing is ongoing.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<SecondsInput {disabled} bind:seconds={max_total_debouncing_time} />
|
||||
</Label>
|
||||
<Label label="Max total debounces amount (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip>
|
||||
The most times a run can be debounced. Past it, the run starts whatever debouncing is
|
||||
ongoing.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<TextInput
|
||||
class="w-24"
|
||||
bind:value={max_total_debounces_amount}
|
||||
inputProps={{ type: 'number', min: 0, disabled }}
|
||||
/>
|
||||
</Label>
|
||||
</SettingToggle>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
resolvedCount={resolvedModuleIds.length}
|
||||
/>
|
||||
{:else if selectedId?.startsWith('settings')}
|
||||
<FlowSettings {enableAi} {noEditor} />
|
||||
<FlowSettings {enableAi} {noEditor} {selectedId} {newFlow} />
|
||||
{:else if selectedId === 'Input'}
|
||||
<FlowInput
|
||||
{noEditor}
|
||||
|
||||
@@ -55,7 +55,4 @@
|
||||
bind:max_total_debounces_amount
|
||||
schema={flowStateStore.val[selectedId]?.schema}
|
||||
placeholder={`$workspace/flow/<flow_path>-${flowModule.id}`}
|
||||
size="xs"
|
||||
fontClass="text-xs font-normal text-primary"
|
||||
indentContent
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import FlowModuleDebounce from './FlowModuleDebounce.svelte'
|
||||
import FlowModuleMock from './FlowModuleMock.svelte'
|
||||
import JobTokenScopesSetting from '$lib/components/JobTokenScopesSetting.svelte'
|
||||
import SettingGroup from '$lib/components/itemSettings/SettingGroup.svelte'
|
||||
import WorkspaceScriptSettingInfo from './WorkspaceScriptSettingInfo.svelte'
|
||||
import { slideDynamic } from '$lib/transitions'
|
||||
|
||||
@@ -117,21 +118,13 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
{#snippet sectionHeader(title: string)}
|
||||
<div class="text-[11px] font-medium uppercase tracking-[0.04em] text-hint">
|
||||
{title}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div
|
||||
bind:this={rootEl}
|
||||
class="flex flex-col gap-8 {embedded ? '' : 'flex-1 min-h-0 overflow-auto p-4 pb-8'}"
|
||||
style={embedded ? undefined : 'scrollbar-gutter: stable'}
|
||||
>
|
||||
{#if !isFailure}
|
||||
<section class="flex flex-col gap-3">
|
||||
{@render sectionHeader('Flow control')}
|
||||
|
||||
<SettingGroup label="Flow control">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div data-setting="skip">
|
||||
<FlowModuleSkip bind:flowModule {parentModule} {previousModule} />
|
||||
@@ -148,13 +141,11 @@
|
||||
<div data-setting="sleep">
|
||||
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule {isAgentTool} />
|
||||
</div>
|
||||
</div></section
|
||||
</div></SettingGroup
|
||||
>
|
||||
{/if}
|
||||
|
||||
<section class="flex flex-col gap-3">
|
||||
{@render sectionHeader('Execution policy')}
|
||||
|
||||
<SettingGroup label="Execution policy">
|
||||
<div class="flex flex-col gap-6">
|
||||
{#if !loopSubset}
|
||||
<div data-setting="retries">
|
||||
@@ -348,48 +339,42 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div data-setting="job-token">
|
||||
<JobTokenScopesSetting
|
||||
bind:value={flowModule.job_token_scopes}
|
||||
kind="step"
|
||||
size="xs"
|
||||
textClass="text-xs font-normal text-primary"
|
||||
/>
|
||||
</div>
|
||||
</div></section
|
||||
<JobTokenScopesSetting bind:value={flowModule.job_token_scopes} kind="step" />
|
||||
</div></SettingGroup
|
||||
>
|
||||
|
||||
{#if s3Language && onApplyS3Snippet && !isFailure}
|
||||
<section class="flex flex-col gap-3" data-setting="s3">
|
||||
{@render sectionHeader('S3 snippets')}
|
||||
<p class="text-xs text-tertiary">
|
||||
Read and write S3 objects, and use Polars or DuckDB to run efficient ETL processes.
|
||||
</p>
|
||||
<div class="flex flex-row items-center justify-between gap-2">
|
||||
<ToggleButtonGroup bind:selected={s3Kind} class="w-auto">
|
||||
{#snippet children({ item })}
|
||||
{#if s3Language === 'deno'}
|
||||
<ToggleButton value="s3_client" small label="S3 lite client" {item} />
|
||||
{:else}
|
||||
<ToggleButton value="s3_client" small label="Boto3" {item} />
|
||||
<ToggleButton value="polars" small label="Polars" {item} />
|
||||
<ToggleButton value="duckdb" small label="DuckDB" {item} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
on:click={() => s3Snippet && onApplyS3Snippet?.(s3Snippet)}
|
||||
>
|
||||
Apply snippet
|
||||
</Button>
|
||||
</div>
|
||||
{#if s3Snippet}
|
||||
<div class="overflow-auto max-h-64 border rounded-md">
|
||||
<HighlightCode language={s3Language} code={s3Snippet} />
|
||||
<div data-setting="s3">
|
||||
<SettingGroup label="S3 snippets" class="flex flex-col gap-3">
|
||||
<p class="text-xs text-tertiary">
|
||||
Read and write S3 objects, and use Polars or DuckDB to run efficient ETL processes.
|
||||
</p>
|
||||
<div class="flex flex-row items-center justify-between gap-2">
|
||||
<ToggleButtonGroup bind:selected={s3Kind} class="w-auto">
|
||||
{#snippet children({ item })}
|
||||
{#if s3Language === 'deno'}
|
||||
<ToggleButton value="s3_client" small label="S3 lite client" {item} />
|
||||
{:else}
|
||||
<ToggleButton value="s3_client" small label="Boto3" {item} />
|
||||
<ToggleButton value="polars" small label="Polars" {item} />
|
||||
<ToggleButton value="duckdb" small label="DuckDB" {item} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
unifiedSize="xs"
|
||||
variant="default"
|
||||
on:click={() => s3Snippet && onApplyS3Snippet?.(s3Snippet)}
|
||||
>
|
||||
Apply snippet
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{#if s3Snippet}
|
||||
<div class="overflow-auto max-h-64 border rounded-md">
|
||||
<HighlightCode language={s3Language} code={s3Snippet} />
|
||||
</div>
|
||||
{/if}
|
||||
</SettingGroup>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
import { ScriptService, type Preview, type Script } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { Loader2, Save, DiffIcon, Settings } from 'lucide-svelte'
|
||||
import ScriptAdvancedSettings from '$lib/components/ScriptAdvancedSettings.svelte'
|
||||
import ScriptRuntimeSettings from '$lib/components/itemSettings/ScriptRuntimeSettings.svelte'
|
||||
import ScriptSettingsBadges from '$lib/components/ScriptSettingsBadges.svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import {
|
||||
@@ -348,17 +348,17 @@
|
||||
/>
|
||||
|
||||
<Drawer bind:this={settingsDrawer} size="600px">
|
||||
<DrawerContent title="Script settings" on:close={() => settingsDrawer?.closeDrawer()}>
|
||||
<DrawerContent title="Runtime settings" on:close={() => settingsDrawer?.closeDrawer()}>
|
||||
{#if script}
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-xs text-secondary">
|
||||
These runtime settings are saved together with the script when you press Save.
|
||||
</p>
|
||||
<ScriptAdvancedSettings {script} workspaceId={opWs} />
|
||||
<ScriptRuntimeSettings {script} workspaceId={opWs} />
|
||||
</div>
|
||||
{/if}
|
||||
{#snippet actions()}
|
||||
<Button variant="border" on:click={() => settingsDrawer?.closeDrawer()}>Done</Button>
|
||||
<Button variant="default" on:click={() => settingsDrawer?.closeDrawer()}>Done</Button>
|
||||
{/snippet}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import ScriptAdvancedSettings from '$lib/components/ScriptAdvancedSettings.svelte'
|
||||
import ScriptRuntimeSettings from '$lib/components/itemSettings/ScriptRuntimeSettings.svelte'
|
||||
import ScriptSettingsBadges from '$lib/components/ScriptSettingsBadges.svelte'
|
||||
import { ScriptService, type Script } from '$lib/gen'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
// cache_ignore_s3_path lives on NewScript but not the returned Script type;
|
||||
// ScriptAdvancedSettings still edits it, so widen the local type to keep it.
|
||||
// ScriptRuntimeSettings still edits it, so widen the local type to keep it.
|
||||
let script: (Script & { cache_ignore_s3_path?: boolean }) | undefined = $state(undefined)
|
||||
let loading = $state(false)
|
||||
let loadError = $state<string | undefined>(undefined)
|
||||
@@ -105,7 +105,7 @@
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size="600px">
|
||||
<DrawerContent title="Script settings" on:close={() => drawer?.closeDrawer()}>
|
||||
<DrawerContent title="Runtime settings" on:close={() => drawer?.closeDrawer()}>
|
||||
{#if loading}
|
||||
<div class="center-center flex-col h-full text-tertiary">
|
||||
<Loader2 class="animate-spin" size={16} />
|
||||
@@ -118,8 +118,8 @@
|
||||
</Alert>
|
||||
{#if current}
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
unifiedSize="xs"
|
||||
variant="default"
|
||||
on:click={() => current && load(current.path, current.hash)}
|
||||
>
|
||||
Retry
|
||||
@@ -136,7 +136,7 @@
|
||||
Saving creates a new version of the workspace script with these runtime settings. The code
|
||||
is left unchanged.
|
||||
</p>
|
||||
<ScriptAdvancedSettings {script} workspaceId={opWs} />
|
||||
<ScriptRuntimeSettings {script} workspaceId={opWs} />
|
||||
</div>
|
||||
{/if}
|
||||
{#snippet actions()}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script module lang="ts">
|
||||
/** Scrolls to a setting's row and flashes it, once its tab has rendered. */
|
||||
export function revealSetting(root: HTMLElement | undefined, setting: string) {
|
||||
const row = root?.querySelector<HTMLElement>(`[data-setting="${setting}"]`)
|
||||
if (!row) return
|
||||
row.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
row.classList.add('setting-highlight')
|
||||
setTimeout(() => row.classList.remove('setting-highlight'), 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import type { ScriptSettingsBadge } from '$lib/components/scriptSettings'
|
||||
|
||||
// The settings turned on in the Runtime and Permissions tabs, in the settings header so they
|
||||
// show whichever tab is open.
|
||||
|
||||
interface Props {
|
||||
groups: { tab: string; badges: ScriptSettingsBadge[] }[]
|
||||
/** Opens `tab` on the row whose `data-setting` is `setting`. */
|
||||
onopen: (tab: string, setting: string) => void
|
||||
/** Chips shown before the rest collapse into a "+n" popover. */
|
||||
max?: number
|
||||
}
|
||||
|
||||
let { groups, onopen, max = 4 }: Props = $props()
|
||||
|
||||
let chips = $derived(groups.flatMap((g) => g.badges.map((badge) => ({ tab: g.tab, badge }))))
|
||||
let shown = $derived(chips.slice(0, max))
|
||||
let rest = $derived(chips.slice(max))
|
||||
</script>
|
||||
|
||||
{#snippet chip({ tab, badge }: { tab: string; badge: ScriptSettingsBadge })}
|
||||
<Badge
|
||||
small
|
||||
clickable
|
||||
class="whitespace-nowrap"
|
||||
icon={{ icon: badge.icon, position: 'left' }}
|
||||
title="{badge.label}: {badge.detail}"
|
||||
onclick={() => onopen(tab, badge.setting)}
|
||||
>
|
||||
{badge.short}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
{#if chips.length > 0}
|
||||
<div class="flex flex-row flex-nowrap items-center gap-1">
|
||||
{#each shown as c (c.badge.key)}
|
||||
{@render chip(c)}
|
||||
{/each}
|
||||
{#if rest.length > 0}
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}
|
||||
contentClasses="border border-light rounded-lg shadow-lg p-2 surface-tertiary max-w-xs"
|
||||
openOnHover
|
||||
debounceDelay={150}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Badge small clickable>+{rest.length}</Badge>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each rest as c (c.badge.key)}
|
||||
{@render chip(c)}
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { SecondsInput } from '$lib/components/common'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
ttl: number | undefined
|
||||
ignoreS3Path: boolean | undefined
|
||||
}
|
||||
|
||||
let { kind, ttl = $bindable(), ignoreS3Path = $bindable() }: Props = $props()
|
||||
</script>
|
||||
|
||||
<SettingToggle
|
||||
setting="cache"
|
||||
label="Cache results"
|
||||
tooltip={`The result of the ${kind} is kept for each set of inputs, and a run with the same inputs returns it instead of running again.`}
|
||||
documentationLink={kind === 'flow'
|
||||
? 'https://www.windmill.dev/docs/flows/cache#cache-flows'
|
||||
: 'https://www.windmill.dev/docs/core_concepts/caching'}
|
||||
checked={ttl !== undefined && ttl > 0}
|
||||
onchange={(on) => (ttl = on ? 300 : undefined)}
|
||||
>
|
||||
<Label label="How long to keep cache valid">
|
||||
<SecondsInput bind:seconds={ttl} />
|
||||
</Label>
|
||||
<Toggle
|
||||
size="2xs"
|
||||
bind:checked={() => ignoreS3Path, (v) => (ignoreS3Path = v || undefined)}
|
||||
options={{
|
||||
right: 'Ignore S3 object paths',
|
||||
rightTooltip:
|
||||
'Two S3 objects with the same content hit the same cache entry, whatever their path.'
|
||||
}}
|
||||
/>
|
||||
</SettingToggle>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { SecondsInput } from '$lib/components/common'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
limit: number | undefined
|
||||
timeWindowS: number | undefined
|
||||
concurrencyKey: string | undefined
|
||||
keyPlaceholder: string
|
||||
}
|
||||
|
||||
let {
|
||||
kind,
|
||||
limit = $bindable(),
|
||||
timeWindowS = $bindable(),
|
||||
concurrencyKey = $bindable(),
|
||||
keyPlaceholder
|
||||
}: Props = $props()
|
||||
|
||||
let disabled = $derived(!$enterpriseLicense)
|
||||
</script>
|
||||
|
||||
<SettingToggle
|
||||
setting="concurrency"
|
||||
label="Concurrency limit"
|
||||
tooltip={`Caps how many runs of this ${kind} can start within a time window. The runs over the limit wait in the queue.`}
|
||||
documentationLink={kind === 'flow'
|
||||
? 'https://www.windmill.dev/docs/flows/concurrency_limit'
|
||||
: 'https://www.windmill.dev/docs/core_concepts/concurrency_limits'}
|
||||
eeOnly
|
||||
{disabled}
|
||||
checked={limit !== undefined && limit > 0}
|
||||
onchange={(on) => {
|
||||
if (on) {
|
||||
limit = 1
|
||||
} else {
|
||||
limit = undefined
|
||||
// A flow keeps its window and key in its value when the limit goes; a script drops them.
|
||||
if (kind === 'script') {
|
||||
timeWindowS = undefined
|
||||
concurrencyKey = undefined
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Label label="Max number of executions within the time window">
|
||||
<TextInput class="w-24" bind:value={limit} inputProps={{ type: 'number', min: 1, disabled }} />
|
||||
</Label>
|
||||
<Label label="Time window in seconds">
|
||||
<SecondsInput {disabled} bind:seconds={timeWindowS} />
|
||||
</Label>
|
||||
<Label label="Custom concurrency key (optional)">
|
||||
{#snippet header()}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits#custom-concurrency-key"
|
||||
>
|
||||
Concurrency keys are global. Use `$workspace` to make one workspace specific, and
|
||||
`$args[name_of_arg]` to use an argument's value.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<TextInput
|
||||
bind:value={concurrencyKey}
|
||||
inputProps={{ type: 'text', disabled, placeholder: keyPlaceholder }}
|
||||
/>
|
||||
</Label>
|
||||
</SettingToggle>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
dedicatedWorker: boolean | undefined
|
||||
/** Why this item cannot run on dedicated workers, e.g. its language. */
|
||||
unsupportedReason?: string
|
||||
}
|
||||
|
||||
let { kind, dedicatedWorker = $bindable(), unsupportedReason }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#snippet reason()}
|
||||
{unsupportedReason}
|
||||
{/snippet}
|
||||
|
||||
<!-- Not available on the cloud, where the toggle could only ever be disabled. -->
|
||||
{#if !isCloudHosted()}
|
||||
<SettingToggle
|
||||
setting="dedicated-worker"
|
||||
label="Dedicated workers"
|
||||
tooltip={kind === 'flow'
|
||||
? 'The steps of the flow run on workers dedicated to it, which keep them warm so there is no cold start.'
|
||||
: 'The script runs on workers dedicated to it, which keep it warm so there is no cold start. A dedicated worker can reach more than 1500 runs per second.'}
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/dedicated_workers"
|
||||
eeOnly
|
||||
disabled={!$enterpriseLicense || (!!unsupportedReason && !dedicatedWorker)}
|
||||
checked={!!dedicatedWorker}
|
||||
onchange={(on) => (dedicatedWorker = on || undefined)}
|
||||
hint={unsupportedReason ? reason : undefined}
|
||||
>
|
||||
<Alert type="info" title="Requires a dedicated worker group" size="xs">
|
||||
A worker group must listen to this {kind}. Select it in the dedicated workers section of the
|
||||
worker group configuration.
|
||||
</Alert>
|
||||
</SettingToggle>
|
||||
{/if}
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { SecondsInput } from '$lib/components/common'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
deleteAfterSecs: number | undefined
|
||||
}
|
||||
|
||||
let { kind, deleteAfterSecs = $bindable() }: Props = $props()
|
||||
</script>
|
||||
|
||||
<SettingToggle
|
||||
setting="lifetime"
|
||||
label="Delete after use"
|
||||
tooltip={kind === 'flow'
|
||||
? 'The logs, arguments and results of every step are deleted this long after the flow completes. 0 deletes them right away. The deletion is irreversible.'
|
||||
: 'The logs, arguments and result of each run are deleted this long after it completes and returns its result. 0 deletes them right away. Only applies to synchronous webhooks and to the script run as a flow step. The deletion is irreversible.'}
|
||||
documentationLink={kind === 'script'
|
||||
? 'https://www.windmill.dev/docs/script_editor/settings#delete-after-use'
|
||||
: undefined}
|
||||
eeOnly
|
||||
disabled={!$enterpriseLicense}
|
||||
checked={deleteAfterSecs != null}
|
||||
onchange={(on) => (deleteAfterSecs = on ? 0 : undefined)}
|
||||
>
|
||||
<SecondsInput bind:seconds={deleteAfterSecs} disabled={!$enterpriseLicense} />
|
||||
</SettingToggle>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
visibleToRunnerOnly: boolean | undefined
|
||||
}
|
||||
|
||||
let { kind, visibleToRunnerOnly = $bindable() }: Props = $props()
|
||||
</script>
|
||||
|
||||
<SettingToggle
|
||||
setting="invisible-runs"
|
||||
label="Make runs invisible to others"
|
||||
tooltip={`Manual runs of this ${kind} are visible only to the user who ran them, owners included. Whoever runs it can override this from the advanced menu of the run form.`}
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs#invisible-runs"
|
||||
checked={!!visibleToRunnerOnly}
|
||||
onchange={(on) => (visibleToRunnerOnly = on || undefined)}
|
||||
/>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { toggleWorkspaceErrorHandler } from '$lib/components/details/errorHandlerToggle'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
path: string
|
||||
/** Also sent with the next deploy. */
|
||||
muted: boolean | undefined
|
||||
}
|
||||
|
||||
let { kind, path, muted = $bindable() }: Props = $props()
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
// The toggle shows the click until the request settles, then the muted state it left.
|
||||
let pending: boolean | undefined = $state(undefined)
|
||||
|
||||
async function toggle() {
|
||||
const next = await toggleWorkspaceErrorHandler($operatingWorkspace, kind, path, muted)
|
||||
if (next !== undefined) muted = next
|
||||
pending = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet eeHint()}
|
||||
Enterprise Edition only.
|
||||
{/snippet}
|
||||
|
||||
<SettingToggle
|
||||
setting="mute-error-handler"
|
||||
label="Mute workspace error handler"
|
||||
tooltip={`Failures of this ${kind} do not trigger the workspace error handler. Applies to the deployed ${kind} right away.`}
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/error_handling#workspace-error-handler"
|
||||
checked={pending ?? !!muted}
|
||||
onchange={(on) => {
|
||||
pending = on
|
||||
toggle()
|
||||
}}
|
||||
hint={$enterpriseLicense ? undefined : eeHint}
|
||||
/>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
priority: number | undefined
|
||||
}
|
||||
|
||||
let { kind, priority = $bindable() }: Props = $props()
|
||||
</script>
|
||||
|
||||
<!-- Not available on the cloud, where the toggle could only ever be disabled. -->
|
||||
{#if !isCloudHosted()}
|
||||
<SettingToggle
|
||||
setting="priority"
|
||||
label="High priority"
|
||||
tooltip={kind === 'flow'
|
||||
? 'The jobs of this flow and of its steps go ahead of the other jobs in the queue.'
|
||||
: 'The jobs of this script go ahead of the other jobs in the queue.'}
|
||||
documentationLink={kind === 'flow'
|
||||
? 'https://www.windmill.dev/docs/flows/priority'
|
||||
: 'https://www.windmill.dev/docs/core_concepts/jobs#high-priority-jobs'}
|
||||
eeOnly
|
||||
disabled={!$enterpriseLicense}
|
||||
checked={priority !== undefined && priority > 0}
|
||||
onchange={(on) => (priority = on ? 100 : undefined)}
|
||||
>
|
||||
<Label label="Priority number">
|
||||
{#snippet header()}
|
||||
<Tooltip>From 0 to 100. Higher numbers run first.</Tooltip>
|
||||
{/snippet}
|
||||
<TextInput
|
||||
class="w-24"
|
||||
bind:value={priority}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
disabled: !$enterpriseLicense,
|
||||
onchange: () => {
|
||||
if (priority != undefined && priority > 100) priority = 100
|
||||
else if (priority != undefined && priority < 0) priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</SettingToggle>
|
||||
{/if}
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import OnBehalfOfSelector, {
|
||||
type OnBehalfOfChoice
|
||||
} from '$lib/components/OnBehalfOfSelector.svelte'
|
||||
import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
import { canPreserveOnBehalfOf, deployedPermissionedAs, WM_DEPLOYERS_GROUP } from './onBehalfOf'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
/** Email of the user whose access the runs use; undefined uses whoever starts each run. */
|
||||
email: string | undefined
|
||||
/** `u/username` of that user. */
|
||||
permissionedAs: string | undefined
|
||||
/** Sent as `preserve_on_behalf_of`: keep the picked user rather than the deployer. */
|
||||
preserve: boolean
|
||||
/** The selector's choice. Owned by the host, so it outlives this row's remounts: while it is
|
||||
* undefined the selector picks the deployed user, which would override a later pick. */
|
||||
choice: OnBehalfOfChoice
|
||||
/** The deployed identity, which the "target" choice keeps. */
|
||||
savedEmail: string | undefined
|
||||
savedPermissionedAs: string | undefined
|
||||
targetWorkspace: string
|
||||
/** Never deployed, so no deployed identity can change. */
|
||||
isNew?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
kind,
|
||||
email = $bindable(),
|
||||
permissionedAs = $bindable(),
|
||||
preserve = $bindable(),
|
||||
choice = $bindable(),
|
||||
savedEmail,
|
||||
savedPermissionedAs,
|
||||
targetWorkspace,
|
||||
isNew = false
|
||||
}: Props = $props()
|
||||
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
let canPreserve = $derived(canPreserveOnBehalfOf(actingUser))
|
||||
let myPermissionedAs = $derived($userStore?.username ? `u/${$userStore.username}` : undefined)
|
||||
|
||||
let enabled = $derived(!!email)
|
||||
let deployPermissionedAs = $derived(
|
||||
enabled ? deployedPermissionedAs(actingUser, permissionedAs) : undefined
|
||||
)
|
||||
// Whoever cannot keep another user's identity deploys the item as themselves.
|
||||
let chosenUser = $derived(
|
||||
(canPreserve ? (permissionedAs ?? email) : deployPermissionedAs) ?? 'the chosen user'
|
||||
)
|
||||
let willChange = $derived(
|
||||
!isNew &&
|
||||
!!savedPermissionedAs &&
|
||||
!!deployPermissionedAs &&
|
||||
deployPermissionedAs !== savedPermissionedAs
|
||||
)
|
||||
</script>
|
||||
|
||||
{#snippet valueHint()}
|
||||
Lets people run this {kind} with resources and variables they can't access themselves.
|
||||
{/snippet}
|
||||
|
||||
<SettingToggle
|
||||
setting="on-behalf-of"
|
||||
label="Run on behalf of"
|
||||
tooltip={`Use it to share a ${kind} that touches secrets: people run it without ever seeing the credentials. Or pick a dedicated user, like billing-bot, so the ${kind} can only reach that user's folders and resources.`}
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/roles_and_permissions#run-on-behalf-of"
|
||||
hint={valueHint}
|
||||
checked={enabled}
|
||||
onchange={(on) => {
|
||||
if (on) {
|
||||
email = $userStore?.email
|
||||
permissionedAs = myPermissionedAs
|
||||
} else {
|
||||
email = undefined
|
||||
permissionedAs = undefined
|
||||
preserve = false
|
||||
choice = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<OnBehalfOfSelector
|
||||
{targetWorkspace}
|
||||
targetValue={savedEmail}
|
||||
selected={choice}
|
||||
onSelect={(c, details) => {
|
||||
choice = c
|
||||
if (c === 'me') {
|
||||
email = $userStore?.email
|
||||
permissionedAs = myPermissionedAs
|
||||
preserve = false
|
||||
} else if (c === 'target') {
|
||||
// Keep the saved pair. An item that has no recorded principal yet sends the
|
||||
// email alone and the backend derives one from it.
|
||||
email = savedEmail
|
||||
permissionedAs = savedPermissionedAs
|
||||
preserve = true
|
||||
} else if (c === 'custom' && details) {
|
||||
email = details.email
|
||||
permissionedAs = details.permissionedAs
|
||||
preserve = true
|
||||
}
|
||||
}}
|
||||
{kind}
|
||||
{canPreserve}
|
||||
customValue={choice === 'custom' ? email : undefined}
|
||||
isDeployment={false}
|
||||
readOnlyWithoutPreserve
|
||||
/>
|
||||
{#if willChange}
|
||||
<AlertTriangle size={14} class="text-yellow-500" />
|
||||
<span class="text-2xs text-yellow-600 dark:text-yellow-400"
|
||||
>Changes from <strong>{savedPermissionedAs}</strong> on deploy</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !canPreserve}
|
||||
<div class="text-2xs text-secondary">
|
||||
Only workspace admins and members of {WM_DEPLOYERS_GROUP} can pick another user.
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-2xs text-secondary">
|
||||
Every run uses the access of {chosenUser}. Who started it is still recorded, and the code reads
|
||||
their email from <code>WM_END_USER_EMAIL</code>.
|
||||
</div>
|
||||
</SettingToggle>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Button, SecondsInput } from '$lib/components/common'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import DebounceLimit from '$lib/components/flows/DebounceLimit.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { ScriptAdvancedSettingsFields } from '$lib/components/scriptSettings'
|
||||
import { Plus, X } from 'lucide-svelte'
|
||||
import SettingGroup from '$lib/components/itemSettings/SettingGroup.svelte'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
import WorkerTagSetting from './WorkerTagSetting.svelte'
|
||||
import ConcurrencyLimitSetting from './ConcurrencyLimitSetting.svelte'
|
||||
import CacheSetting from './CacheSetting.svelte'
|
||||
import PrioritySetting from './PrioritySetting.svelte'
|
||||
import DedicatedWorkersSetting from './DedicatedWorkersSetting.svelte'
|
||||
import DeleteAfterUseSetting from './DeleteAfterUseSetting.svelte'
|
||||
|
||||
interface Props {
|
||||
script: ScriptAdvancedSettingsFields
|
||||
/** Workspace to read worker tags from; the picker defaults to the operating one. */
|
||||
workspaceId?: string
|
||||
tagPlaceholder?: string
|
||||
/** Seeded when the custom timeout is turned on. */
|
||||
defaultTimeout?: number
|
||||
}
|
||||
|
||||
let { script = $bindable(), workspaceId, tagPlaceholder, defaultTimeout = 300 }: Props = $props()
|
||||
|
||||
const DEDICATED_WORKER_LANGUAGES = ['bun', 'bunnative', 'python3', 'deno']
|
||||
|
||||
// The list opens before a variable is typed, so the toggle cannot read only `envs`.
|
||||
let addingEnvs = $state(false)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<SettingGroup label="Execution policy" class="flex flex-col gap-6">
|
||||
<SettingToggle
|
||||
setting="timeout"
|
||||
label="Custom timeout"
|
||||
tooltip="Used instead of the instance timeout, which it cannot exceed."
|
||||
documentationLink="https://www.windmill.dev/docs/script_editor/settings#timeout"
|
||||
checked={!!script.timeout}
|
||||
onchange={(on) => (script.timeout = on ? defaultTimeout : undefined)}
|
||||
>
|
||||
<SecondsInput bind:seconds={script.timeout} />
|
||||
</SettingToggle>
|
||||
<ConcurrencyLimitSetting
|
||||
kind="script"
|
||||
bind:limit={script.concurrent_limit}
|
||||
bind:timeWindowS={script.concurrency_time_window_s}
|
||||
bind:concurrencyKey={script.concurrency_key}
|
||||
keyPlaceholder={`$workspace/script/${script.path ?? ''}-$args[foo]`}
|
||||
/>
|
||||
<DebounceLimit
|
||||
bind:debounce_delay_s={script.debounce_delay_s}
|
||||
bind:debounce_key={script.debounce_key}
|
||||
bind:debounce_args_to_accumulate={script.debounce_args_to_accumulate}
|
||||
bind:max_total_debouncing_time={script.max_total_debouncing_time}
|
||||
bind:max_total_debounces_amount={script.max_total_debounces_amount}
|
||||
schema={script.schema as Schema}
|
||||
placeholder={`$workspace/script/${script.path ?? ''}-$args[foo]`}
|
||||
/>
|
||||
<CacheSetting
|
||||
kind="script"
|
||||
bind:ttl={script.cache_ttl}
|
||||
bind:ignoreS3Path={script.cache_ignore_s3_path}
|
||||
/>
|
||||
<PrioritySetting kind="script" bind:priority={script.priority} />
|
||||
<SettingToggle
|
||||
setting="perpetual"
|
||||
label="Perpetual script"
|
||||
tooltip="The script restarts as soon as it ends, unless it was cancelled."
|
||||
documentationLink="https://www.windmill.dev/docs/script_editor/perpetual_scripts"
|
||||
checked={!!script.restart_unless_cancelled}
|
||||
onchange={(on) => (script.restart_unless_cancelled = on || undefined)}
|
||||
/>
|
||||
<DeleteAfterUseSetting kind="script" bind:deleteAfterSecs={script.delete_after_secs} />
|
||||
</SettingGroup>
|
||||
|
||||
<SettingGroup label="Workers" class="flex flex-col gap-6">
|
||||
<WorkerTagSetting
|
||||
kind="script"
|
||||
bind:tag={script.tag}
|
||||
placeholder={tagPlaceholder}
|
||||
{workspaceId}
|
||||
/>
|
||||
<DedicatedWorkersSetting
|
||||
kind="script"
|
||||
bind:dedicatedWorker={script.dedicated_worker}
|
||||
unsupportedReason={script.language && !DEDICATED_WORKER_LANGUAGES.includes(script.language)
|
||||
? 'Only for Python, Bun and Deno scripts. Other languages start no full runtime, so they already run about as fast.'
|
||||
: undefined}
|
||||
/>
|
||||
{#if !isCloudHosted()}
|
||||
<SettingToggle
|
||||
setting="envs"
|
||||
label="Custom env variables"
|
||||
tooltip="Static environment variables passed to the deployed script."
|
||||
documentationLink="https://www.windmill.dev/docs/script_editor/custom_environment_variables"
|
||||
checked={addingEnvs || (script.envs?.length ?? 0) > 0}
|
||||
onchange={(on) => {
|
||||
addingEnvs = on
|
||||
script.envs = on ? [''] : []
|
||||
}}
|
||||
>
|
||||
<Alert type="info" title="Not passed in previews" size="xs">
|
||||
Only the deployed script gets these variables.
|
||||
</Alert>
|
||||
{#each script.envs ?? [] as _, i (i)}
|
||||
<div class="flex flex-row items-center gap-1 max-w-md">
|
||||
{#if script.envs}
|
||||
<TextInput
|
||||
bind:value={script.envs[i]}
|
||||
inputProps={{ type: 'text', placeholder: 'KEY=VALUE' }}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="md"
|
||||
iconOnly
|
||||
startIcon={{ icon: X }}
|
||||
title="Remove variable"
|
||||
onclick={() => (script.envs = script.envs?.filter((_, j) => j !== i))}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex">
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: Plus }}
|
||||
onclick={() => (script.envs = [...(script.envs ?? []), ''])}
|
||||
>
|
||||
Add variable
|
||||
</Button>
|
||||
</div>
|
||||
</SettingToggle>
|
||||
{/if}
|
||||
</SettingGroup>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
/** Applied to the wrapper of the group's settings. */
|
||||
class?: string
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
let { label, class: clazz = '', children }: Props = $props()
|
||||
</script>
|
||||
|
||||
<!-- A group of settings whose rows explain themselves: the label only names the group, so it stays
|
||||
small and quiet instead of a section heading. -->
|
||||
<section class="flex flex-col gap-3">
|
||||
<div class="text-2xs font-medium uppercase tracking-wide text-secondary">{label}</div>
|
||||
<div class={clazz}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import type { ScriptSettingsBadge } from '$lib/components/scriptSettings'
|
||||
|
||||
interface Props {
|
||||
badges: ScriptSettingsBadge[]
|
||||
}
|
||||
|
||||
let { badges }: Props = $props()
|
||||
</script>
|
||||
|
||||
<!-- The configured settings of a tab, drawn on the tab the way the step "Run settings" tab
|
||||
draws its own. -->
|
||||
{#if badges.length > 0}
|
||||
<div class="flex flex-row items-center gap-1 text-secondary">
|
||||
{#each badges as badge (badge.key)}
|
||||
{@const Icon = badge.icon}
|
||||
<Popover notClickable>
|
||||
<Icon size={12} aria-hidden="true" />
|
||||
<span class="sr-only">{badge.label}: {badge.detail}</span>
|
||||
{#snippet text()}
|
||||
<span class="font-semibold">{badge.label}</span> · {badge.detail}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { slideDynamic } from '$lib/transitions'
|
||||
|
||||
// One optional setting of a flow, script, app or step: a toggle, and the setting's
|
||||
// parameters under it while it is on. The settings surfaces build their rows from this so
|
||||
// they keep one spacing and indent.
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
tooltip?: string
|
||||
documentationLink?: string
|
||||
/** Whether the setting is configured; its parameters show while it is. */
|
||||
checked: boolean | undefined
|
||||
/** The new state, for the host to write or clear the setting's value. */
|
||||
onchange?: (checked: boolean) => void
|
||||
disabled?: boolean
|
||||
/** Badges the toggle "EE" while it is disabled. */
|
||||
eeOnly?: boolean
|
||||
/** Lets a host scroll to the row: `[data-setting="<key>"]`. */
|
||||
setting?: string
|
||||
aiId?: string
|
||||
aiDescription?: string
|
||||
/** Shown under the row whatever its state, e.g. why it is disabled. */
|
||||
hint?: Snippet
|
||||
/** The setting's parameters. */
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
tooltip,
|
||||
documentationLink,
|
||||
checked,
|
||||
onchange,
|
||||
disabled = false,
|
||||
eeOnly = false,
|
||||
setting,
|
||||
aiId,
|
||||
aiDescription,
|
||||
hint,
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
// Hosts read `checked` from the value, and a value being typed passes through states that read
|
||||
// as off (an emptied field, a 0, an empty expression). Once its parameters have focus, they
|
||||
// stay open until the toggle is clicked; the toggle itself shows whether the value is set.
|
||||
let editing = $state(false)
|
||||
// Bumped each time the toggle turns the row on, so its parameters remount on the value the host
|
||||
// just wrote even when the row was already open: an editor reads its code once, at mount.
|
||||
let generation = $state(0)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2" data-setting={setting}>
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={
|
||||
() => checked,
|
||||
(v) => {
|
||||
editing = false
|
||||
// The host writes the value here, before the row opens. A click runs microtasks between
|
||||
// the input's listeners, so a `change` listener would come after the parameters mounted
|
||||
// on the old value, and an editor that reads its code once at mount would open empty.
|
||||
onchange?.(!!v)
|
||||
if (v) generation++
|
||||
}
|
||||
}
|
||||
{disabled}
|
||||
{eeOnly}
|
||||
{aiId}
|
||||
{aiDescription}
|
||||
options={{ right: label, rightTooltip: tooltip, rightDocumentationLink: documentationLink }}
|
||||
/>
|
||||
{#if hint}
|
||||
<!-- pl-9 lines everything under the label: an xs toggle is 28px wide plus its 8px gap. -->
|
||||
<div class="pl-9 text-2xs text-secondary">{@render hint()}</div>
|
||||
{/if}
|
||||
{#if (checked || editing) && children}
|
||||
<!-- The slide clamps this box's height. A flex column would shrink an `overflow` child (an
|
||||
editor) to fit the clamp, so the slide would measure it short and jump at the end. -->
|
||||
<div transition:slideDynamic onfocusin={() => (editing = true)}>
|
||||
{#key generation}
|
||||
<div class="flex flex-col gap-2 pl-9">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte'
|
||||
import SettingToggle from './SettingToggle.svelte'
|
||||
|
||||
interface Props {
|
||||
kind: 'flow' | 'script'
|
||||
tag: string | undefined
|
||||
/** Flows only: let steps that declare their own tag keep it. */
|
||||
preserveStepTags?: boolean | undefined
|
||||
placeholder?: string
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
let {
|
||||
kind,
|
||||
tag = $bindable(),
|
||||
preserveStepTags = $bindable(),
|
||||
placeholder,
|
||||
workspaceId
|
||||
}: Props = $props()
|
||||
|
||||
// The picker opens before a tag is picked, so the toggle cannot read only `tag`.
|
||||
let picking = $state(false)
|
||||
let checked = $derived(picking || !!tag)
|
||||
</script>
|
||||
|
||||
<SettingToggle
|
||||
setting="worker-group"
|
||||
label="Worker group tag"
|
||||
tooltip={kind === 'flow'
|
||||
? "Every step of the flow runs on the workers listening to this tag (queue), whatever the step's own tag. Without one, the flow's control steps run with the default 'flow' tag and each step with its own."
|
||||
: 'The script runs on the workers listening to this tag (queue), for instance a highmem or gpu worker group.'}
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
|
||||
{checked}
|
||||
onchange={(on) => {
|
||||
picking = on
|
||||
if (!on) {
|
||||
tag = undefined
|
||||
preserveStepTags = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<WorkerTagPicker bind:tag {placeholder} {workspaceId} />
|
||||
{#if kind === 'flow' && tag}
|
||||
<Toggle
|
||||
size="2xs"
|
||||
bind:checked={preserveStepTags}
|
||||
options={{
|
||||
right: 'Preserve step worker tags',
|
||||
rightTooltip:
|
||||
'Steps that declare their own worker tag run on it instead of the flow tag. Steps without one still inherit the flow tag.',
|
||||
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/worker_groups'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</SettingToggle>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { deployedPermissionedAs, keepsDeployedOnBehalfOf } from './onBehalfOf'
|
||||
|
||||
// Who a deploy leaves the item running on behalf of: only admins and wm_deployers keep another user.
|
||||
describe('onBehalfOf', () => {
|
||||
const admin = { username: 'admin', is_admin: true }
|
||||
const deployer = { username: 'dep', groups: ['wm_deployers'] }
|
||||
const member = { username: 'alice' }
|
||||
|
||||
it('keeps the held user only for a deployer who may preserve', () => {
|
||||
expect(deployedPermissionedAs(admin, 'u/bot')).toBe('u/bot')
|
||||
expect(deployedPermissionedAs(deployer, 'u/bot')).toBe('u/bot')
|
||||
expect(deployedPermissionedAs(admin, undefined)).toBe('u/admin')
|
||||
expect(deployedPermissionedAs(member, 'u/bot')).toBe('u/alice')
|
||||
})
|
||||
|
||||
it('picks the deployed user only when nothing is picked and run-as is on', () => {
|
||||
expect(keepsDeployedOnBehalfOf(admin, undefined, 'bot@x.dev', 'bot@x.dev')).toBe(true)
|
||||
expect(keepsDeployedOnBehalfOf(deployer, undefined, 'me@x.dev', 'bot@x.dev')).toBe(true)
|
||||
expect(keepsDeployedOnBehalfOf(admin, 'me', 'me@x.dev', 'bot@x.dev')).toBe(false)
|
||||
expect(keepsDeployedOnBehalfOf(admin, undefined, undefined, 'bot@x.dev')).toBe(false)
|
||||
expect(keepsDeployedOnBehalfOf(admin, undefined, 'me@x.dev', undefined)).toBe(false)
|
||||
expect(keepsDeployedOnBehalfOf(member, undefined, 'bot@x.dev', 'bot@x.dev')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
export const WM_DEPLOYERS_GROUP = 'wm_deployers'
|
||||
|
||||
type DeployingUser =
|
||||
| {
|
||||
username?: string
|
||||
email?: string
|
||||
is_admin?: boolean
|
||||
is_super_admin?: boolean
|
||||
groups?: string[]
|
||||
}
|
||||
| undefined
|
||||
|
||||
/** Who may keep, or pick, another user's identity on deploy. For anyone else the backend deploys
|
||||
* the item on behalf of the deployer, whatever the request says. */
|
||||
export function canPreserveOnBehalfOf(user: DeployingUser): boolean {
|
||||
return (
|
||||
!!user?.is_admin ||
|
||||
!!user?.is_super_admin ||
|
||||
(user?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
/** Whether the deployed user should be picked, as the run-as selector does once it shows with
|
||||
* nothing picked. The Runtime tab applies it too, so a deploy that only changes a runtime
|
||||
* setting keeps sending `preserve_on_behalf_of`. */
|
||||
export function keepsDeployedOnBehalfOf(
|
||||
user: DeployingUser,
|
||||
choice: string | undefined,
|
||||
email: string | undefined,
|
||||
savedEmail: string | undefined
|
||||
): boolean {
|
||||
return choice === undefined && !!email && !!savedEmail && canPreserveOnBehalfOf(user)
|
||||
}
|
||||
|
||||
/** The `u/username` a deploy leaves the item running on behalf of: the held user for a deployer
|
||||
* who may preserve, the deployer for anyone else. */
|
||||
export function deployedPermissionedAs(
|
||||
user: DeployingUser,
|
||||
permissionedAs: string | undefined
|
||||
): string | undefined {
|
||||
if (permissionedAs && canPreserveOnBehalfOf(user)) return permissionedAs
|
||||
return user?.username ? `u/${user.username}` : undefined
|
||||
}
|
||||
@@ -16,7 +16,7 @@ describe('getActiveScriptSettingsBadges', () => {
|
||||
priority: 50,
|
||||
tag: 'gpu'
|
||||
}).map((b) => b.key)
|
||||
expect(keys).toEqual(['concurrency', 'cache', 'timeout', 'priority', 'tag'])
|
||||
expect(keys).toEqual(['timeout', 'concurrency', 'cache', 'priority', 'tag'])
|
||||
})
|
||||
|
||||
it('treats a zero/absent priority and non-positive debounce as inactive', () => {
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import {
|
||||
Gauge,
|
||||
Database,
|
||||
Timer,
|
||||
Hourglass,
|
||||
Repeat,
|
||||
Cpu,
|
||||
Trash2,
|
||||
ChevronsUp,
|
||||
Tag
|
||||
CircleStop,
|
||||
Combine,
|
||||
CornerDownLeft,
|
||||
Cpu,
|
||||
Database,
|
||||
EyeOff,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
KeyRound,
|
||||
Repeat,
|
||||
Tag,
|
||||
Timer,
|
||||
Trash2,
|
||||
UserCog
|
||||
} from 'lucide-svelte'
|
||||
import type { ScriptLang } from '$lib/gen'
|
||||
import type { FlowValue, ScriptLang } from '$lib/gen'
|
||||
|
||||
// Subset of Script/NewScript fields that make up the "advanced runtime settings"
|
||||
// surfaced both in the standalone script editor and, via the mini settings drawer,
|
||||
// from within the flow editor for workspace-script steps.
|
||||
// Subset of Script/NewScript fields that make up the runtime settings surfaced both in the
|
||||
// standalone script editor and, via the mini settings drawer, from within the flow editor for
|
||||
// workspace-script steps.
|
||||
export type ScriptAdvancedSettingsFields = {
|
||||
path?: string
|
||||
language?: ScriptLang
|
||||
@@ -34,27 +40,94 @@ export type ScriptAdvancedSettingsFields = {
|
||||
dedicated_worker?: boolean
|
||||
delete_after_secs?: number
|
||||
priority?: number
|
||||
envs?: string[]
|
||||
}
|
||||
|
||||
export type ScriptSettingsBadge = {
|
||||
key: string
|
||||
label: string
|
||||
/** The label in a word or two, for the chips in the settings header. */
|
||||
short: string
|
||||
/** The `data-setting` of the setting's row in its tab. */
|
||||
setting: string
|
||||
icon: any
|
||||
detail: string
|
||||
}
|
||||
|
||||
// Compute the list of active advanced settings for a script, used to render
|
||||
// at-a-glance badges in the editor top bar and in the flow drawers.
|
||||
/** Runtime settings only a flow has. */
|
||||
export type FlowOnlyRuntimeFields = {
|
||||
skip_expr?: string
|
||||
early_return?: string
|
||||
same_worker?: boolean
|
||||
}
|
||||
|
||||
/** A flow's runtime settings, which it splits between itself and its value, in the shape the
|
||||
* badges read. */
|
||||
export function flowRuntimeFields(flow: {
|
||||
tag?: string
|
||||
dedicated_worker?: boolean
|
||||
value: FlowValue
|
||||
}): ScriptAdvancedSettingsFields & FlowOnlyRuntimeFields {
|
||||
const v = flow.value
|
||||
return {
|
||||
tag: flow.tag,
|
||||
dedicated_worker: flow.dedicated_worker,
|
||||
concurrent_limit: v.concurrent_limit,
|
||||
concurrency_time_window_s: v.concurrency_time_window_s,
|
||||
cache_ttl: v.cache_ttl,
|
||||
debounce_delay_s: v.debounce_delay_s,
|
||||
delete_after_secs: v.delete_after_secs,
|
||||
priority: v.priority,
|
||||
skip_expr: v.skip_expr,
|
||||
early_return: v.early_return,
|
||||
same_worker: v.same_worker
|
||||
}
|
||||
}
|
||||
|
||||
// The active runtime settings of a script or flow, in the order their Runtime tab lists them,
|
||||
// for the at-a-glance badges of the editor top bar, the settings header and tabs, and the flow drawers.
|
||||
export function getActiveScriptSettingsBadges(
|
||||
settings: ScriptAdvancedSettingsFields | undefined
|
||||
settings: (ScriptAdvancedSettingsFields & FlowOnlyRuntimeFields) | undefined
|
||||
): ScriptSettingsBadge[] {
|
||||
if (!settings) return []
|
||||
const badges: ScriptSettingsBadge[] = []
|
||||
if (settings.skip_expr) {
|
||||
badges.push({
|
||||
key: 'early_stop',
|
||||
short: 'Stop if',
|
||||
setting: 'early-stop',
|
||||
label: 'Stop flow if',
|
||||
icon: CircleStop,
|
||||
detail: 'Stops early when the inputs match the expression'
|
||||
})
|
||||
}
|
||||
if (settings.early_return) {
|
||||
badges.push({
|
||||
key: 'early_return',
|
||||
short: 'Early return',
|
||||
setting: 'early-return',
|
||||
label: 'Early return',
|
||||
icon: CornerDownLeft,
|
||||
detail: `Sync webhooks return the result of step ${settings.early_return}`
|
||||
})
|
||||
}
|
||||
// Non-positive concurrent_limit / timeout are treated as unset by the runtime
|
||||
// (legacy zero rows), so don't surface them as active settings.
|
||||
if (settings.timeout != undefined && settings.timeout > 0) {
|
||||
badges.push({
|
||||
key: 'timeout',
|
||||
short: 'Timeout',
|
||||
setting: 'timeout',
|
||||
label: 'Timeout',
|
||||
icon: Timer,
|
||||
detail: `${settings.timeout}s`
|
||||
})
|
||||
}
|
||||
if (settings.concurrent_limit != undefined && settings.concurrent_limit > 0) {
|
||||
badges.push({
|
||||
key: 'concurrency',
|
||||
short: 'Concurrency',
|
||||
setting: 'concurrency',
|
||||
label: 'Concurrency',
|
||||
icon: Gauge,
|
||||
detail: `Max ${settings.concurrent_limit} execution${
|
||||
@@ -66,49 +139,51 @@ export function getActiveScriptSettingsBadges(
|
||||
}`
|
||||
})
|
||||
}
|
||||
if (settings.debounce_delay_s != undefined && settings.debounce_delay_s > 0) {
|
||||
badges.push({
|
||||
key: 'debounce',
|
||||
short: 'Debounce',
|
||||
setting: 'debounce',
|
||||
label: 'Debounce',
|
||||
icon: Combine,
|
||||
detail: `Debounced by ${settings.debounce_delay_s}s`
|
||||
})
|
||||
}
|
||||
if (settings.cache_ttl != undefined && settings.cache_ttl > 0) {
|
||||
badges.push({
|
||||
key: 'cache',
|
||||
short: 'Cache',
|
||||
setting: 'cache',
|
||||
label: 'Cache',
|
||||
icon: Database,
|
||||
detail: `Cached for ${settings.cache_ttl}s`
|
||||
})
|
||||
}
|
||||
if (settings.timeout != undefined && settings.timeout > 0) {
|
||||
if (settings.priority != undefined && settings.priority > 0) {
|
||||
badges.push({
|
||||
key: 'timeout',
|
||||
label: 'Timeout',
|
||||
icon: Timer,
|
||||
detail: `${settings.timeout}s`
|
||||
})
|
||||
}
|
||||
if (settings.debounce_delay_s != undefined && settings.debounce_delay_s > 0) {
|
||||
badges.push({
|
||||
key: 'debounce',
|
||||
label: 'Debounce',
|
||||
icon: Hourglass,
|
||||
detail: `Debounced by ${settings.debounce_delay_s}s`
|
||||
key: 'priority',
|
||||
short: 'Priority',
|
||||
setting: 'priority',
|
||||
label: 'High priority',
|
||||
icon: ChevronsUp,
|
||||
detail: `Priority ${settings.priority}`
|
||||
})
|
||||
}
|
||||
if (settings.restart_unless_cancelled) {
|
||||
badges.push({
|
||||
key: 'perpetual',
|
||||
short: 'Perpetual',
|
||||
setting: 'perpetual',
|
||||
label: 'Perpetual',
|
||||
icon: Repeat,
|
||||
detail: 'Restarts unless cancelled'
|
||||
})
|
||||
}
|
||||
if (settings.dedicated_worker) {
|
||||
badges.push({
|
||||
key: 'dedicated',
|
||||
label: 'Dedicated',
|
||||
icon: Cpu,
|
||||
detail: 'Runs on dedicated workers'
|
||||
})
|
||||
}
|
||||
if (settings.delete_after_secs != undefined) {
|
||||
badges.push({
|
||||
key: 'delete_after_use',
|
||||
short: 'Auto-delete',
|
||||
setting: 'lifetime',
|
||||
label: 'Delete after use',
|
||||
icon: Trash2,
|
||||
detail:
|
||||
@@ -117,21 +192,78 @@ export function getActiveScriptSettingsBadges(
|
||||
: `Deleted ${settings.delete_after_secs}s after completion`
|
||||
})
|
||||
}
|
||||
if (settings.priority != undefined && settings.priority > 0) {
|
||||
badges.push({
|
||||
key: 'priority',
|
||||
label: 'High priority',
|
||||
icon: ChevronsUp,
|
||||
detail: `Priority ${settings.priority}`
|
||||
})
|
||||
}
|
||||
if (settings.tag) {
|
||||
badges.push({
|
||||
key: 'tag',
|
||||
short: settings.tag,
|
||||
setting: 'worker-group',
|
||||
label: settings.tag,
|
||||
icon: Tag,
|
||||
detail: `Worker tag: ${settings.tag}`
|
||||
})
|
||||
}
|
||||
if (settings.dedicated_worker) {
|
||||
badges.push({
|
||||
key: 'dedicated',
|
||||
short: 'Dedicated',
|
||||
setting: 'dedicated-worker',
|
||||
label: 'Dedicated',
|
||||
icon: Cpu,
|
||||
detail: 'Runs on dedicated workers'
|
||||
})
|
||||
}
|
||||
if (settings.same_worker) {
|
||||
badges.push({
|
||||
key: 'same_worker',
|
||||
short: 'Shared dir',
|
||||
setting: 'same-worker',
|
||||
label: 'Shared directory',
|
||||
icon: FolderOpen,
|
||||
detail: 'Steps run on the same worker and share ./shared'
|
||||
})
|
||||
}
|
||||
return badges
|
||||
}
|
||||
|
||||
// The active settings of a script's or flow's Permissions tab, in the order the tab lists them.
|
||||
export function getActivePermissionBadges(settings: {
|
||||
on_behalf_of_email?: string
|
||||
job_token_scopes?: string[] | null
|
||||
visible_to_runner_only?: boolean
|
||||
}): ScriptSettingsBadge[] {
|
||||
const badges: ScriptSettingsBadge[] = []
|
||||
if (settings.on_behalf_of_email) {
|
||||
badges.push({
|
||||
key: 'on_behalf_of',
|
||||
short: 'On behalf',
|
||||
setting: 'on-behalf-of',
|
||||
label: 'Run on behalf of',
|
||||
icon: UserCog,
|
||||
detail: settings.on_behalf_of_email
|
||||
})
|
||||
}
|
||||
if (settings.visible_to_runner_only) {
|
||||
badges.push({
|
||||
key: 'invisible_runs',
|
||||
short: 'Invisible',
|
||||
setting: 'invisible-runs',
|
||||
label: 'Invisible runs',
|
||||
icon: EyeOff,
|
||||
detail: 'Manual runs are visible only to whoever ran them'
|
||||
})
|
||||
}
|
||||
if (settings.job_token_scopes != null) {
|
||||
badges.push({
|
||||
key: 'job_token',
|
||||
short: 'Job token',
|
||||
setting: 'job-token',
|
||||
label: 'Restricted job token',
|
||||
icon: KeyRound,
|
||||
detail:
|
||||
settings.job_token_scopes.length === 0
|
||||
? 'No API access'
|
||||
: settings.job_token_scopes.join(', ')
|
||||
})
|
||||
}
|
||||
return badges
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user