fix: make every trigger and schedule editor control respect read-only mode (#11569)

* fix: make every trigger and schedule editor control respect read-only mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: grey out read-only option buttons in gcp, mqtt and sqs editors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexRV12
2026-10-07 12:24:00 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent c71ecad6f9
commit 52a99c3047
20 changed files with 125 additions and 46 deletions
+10 -6
View File
@@ -17,6 +17,7 @@
* button needs it: without it that text is invisible to the editor's dirty state,
* so it is silently dropped on close and cannot even enable Save on its own. */
onPendingChange?: (pending: string) => void
disabled?: boolean
}
let {
@@ -24,7 +25,8 @@
onchange,
class: clazz = '',
workspace,
onPendingChange
onPendingChange,
disabled = false
}: Props = $props()
let adding = $state(false)
@@ -140,12 +142,14 @@
{#each labels ?? [] as label (label)}
<Badge color="blue" small>
{label}
<button class="ml-0.5 hover:text-red-500" onclick={() => removeLabel(label)}>
<X size={10} />
</button>
{#if !disabled}
<button class="ml-0.5 hover:text-red-500" onclick={() => removeLabel(label)}>
<X size={10} />
</button>
{/if}
</Badge>
{/each}
{#if adding}
{#if adding && !disabled}
<div class="relative">
<input
bind:this={inputEl}
@@ -189,7 +193,7 @@
</div>
{/if}
</div>
{:else}
{:else if !disabled}
<Button
variant="subtle"
unifiedSize="xs"
@@ -159,7 +159,7 @@
>
<DraggableTags
items={valueEntry}
{allowClear}
allowClear={allowClear && !disabled}
onRemove={onRemoveValue}
onReorder={reorderable
? (oldIdx, newIdx) => (value = reorder(currentValue, oldIdx, newIdx))
@@ -167,7 +167,7 @@
/>
</ul>
{/if}
{#if allowClear && !hideMainClearBtn && !!value?.length}
{#if allowClear && !disabled && !hideMainClearBtn && !!value?.length}
<CloseButton
noBg
class="ml-2 remove-all bg-transparent text-hint"
@@ -37,8 +37,12 @@
import { FlaskConical } from 'lucide-svelte'
import Alert from '../common/alert/Alert.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import { useTriggerLock } from '$lib/operatorWriteRights'
const operatingWorkspace = useOperatingWorkspace()
// Saving a capture config is a trigger write the server refuses to a withdrawn operator, so
// every capture kind locks here rather than each editor passing it down.
const triggerLock = useTriggerLock()
interface Props {
disabled?: boolean | undefined
@@ -289,7 +293,7 @@
<Button
size="xs"
on:click={() => dispatch('captureToggle', {})}
{disabled}
disabled={disabled || !!$triggerLock}
variant={captureInfo.active ? 'default' : 'accent-secondary'}
selected={captureInfo.active}
startIcon={captureInfo.active
@@ -327,12 +331,14 @@
<div
class={twMerge(
'absolute top-0 left-0 w-full text-center',
disabled === true ? 'text-red-600 dark:text-red-400' : ''
disabled === true && !$triggerLock ? 'text-red-600 dark:text-red-400' : ''
)}
in:fade={{ duration: 100, delay: 50 }}
out:fade={{ duration: 50 }}
>
{#if disabled === true}
{#if $triggerLock}
{$triggerLock}.
{:else if disabled === true}
Enter a valid configuration to start capturing.
{:else}
{@render description?.()}
@@ -22,9 +22,16 @@
* `default_permissioned_as` rules and preselects the matching default.
*/
path?: string | undefined
/** Read-only editor: show the stored value, offer no choice and announce no change. */
disabled?: boolean
}
let { permissionedAs, onPermissionedAsChange, path = undefined }: Props = $props()
let {
permissionedAs,
onPermissionedAsChange,
path = undefined,
disabled = false
}: Props = $props()
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
@@ -77,13 +84,20 @@
permissionedAs !== effectivePermissionedAs
)
const shouldRender = $derived(!!wsId && (permissionedAs !== undefined || canPreserve))
const shouldRender = $derived(
!!wsId && (permissionedAs !== undefined || (canPreserve && !disabled))
)
// For non-admin users editing a trigger owned by someone else: signal that
// permissioned_as will change to the current user (backend ignores preserve for non-admins).
// Only fires when there's an actual change (DB value differs from current user).
$effect(() => {
if (!canPreserve && permissionedAs !== undefined && permissionedAs !== myPermissionedAs) {
if (
!disabled &&
!canPreserve &&
permissionedAs !== undefined &&
permissionedAs !== myPermissionedAs
) {
onPermissionedAsChange(undefined, false)
}
})
@@ -107,7 +121,7 @@
{#if shouldRender && wsId}
<div class="flex items-center gap-1.5 text-2xs text-tertiary mb-4">
<span>Permissioned as</span>
{#if canPreserve}
{#if canPreserve && !disabled}
<OnBehalfOfSelector
targetWorkspace={wsId}
targetValue={permissionedAs}
@@ -127,7 +141,7 @@
{/if}
{:else if permissionedAs}
<strong class="text-secondary">{permissionedAs}</strong>
{#if willChange}
{#if willChange && !disabled}
<AlertTriangle class="w-3.5 h-3.5 text-yellow-500" />
<span class="text-yellow-600 dark:text-yellow-400"
>will change to <strong>{effectivePermissionedAs}</strong> on save</span
@@ -136,8 +136,9 @@
/>
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Clear"
disabled={!can_write}
onclick={() => {
if (exchange) {
exchange.routing_keys = (exchange.routing_keys ?? []).filter(
@@ -506,6 +506,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -465,6 +465,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -390,6 +390,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -229,20 +229,22 @@
<div class="flex flex-col gap-3 mt-2">
<ToggleButtonGroup
selected={usesDefaultCredentials ? 'default' : 'resource'}
disabled={!can_write}
on:selected={(e) => onCredentialsModeChange(e.detail === 'default')}
>
{#snippet children({ item })}
{#snippet children({ item, disabled })}
<ToggleButton
label="Service account"
value="resource"
tooltip="Authenticate with a service account key held in a GCP resource."
showTooltipIcon
{item}
{disabled}
/>
<ToggleButton
label="Application default credentials"
value="default"
disabled={!canUseDefaultCredentials}
disabled={disabled || !canUseDefaultCredentials}
tooltip={canUseDefaultCredentials
? 'Authenticate as the Windmill server itself, using the credentials of its environment (workload identity, the metadata server, or GOOGLE_APPLICATION_CREDENTIALS).'
: 'Workspace admins can authenticate as the Windmill server itself. Ask one to set this up.'}
@@ -256,6 +258,7 @@
<ResourcePicker
workspace={wsId}
resourceType="gcloud"
disabled={!can_write}
bind:value={
() => gcp_resource_path,
(v) => {
@@ -313,6 +316,7 @@
<div class="flex flex-row gap-1 mt-2">
<Select
loading={loadingTopic}
disabled={!can_write}
disablePortal
clearable
class="grow shrink"
@@ -347,19 +351,21 @@
<div class="flex flex-col gap-3">
<ToggleButtonGroup
bind:selected={subscription_mode}
disabled={!can_write}
on:selected={(e) => {
if (e.detail === 'existing' && subscription_items.length === 0) {
loadAllSubscriptionFromGooglePubSubTopic()
}
}}
>
{#snippet children({ item })}
{#snippet children({ item, disabled })}
<ToggleButton
label="Create/Update"
value="create_update"
tooltip="Create a new subscription or update an existing one with custom settings"
showTooltipIcon
{item}
{disabled}
/>
<ToggleButton
label="Existing subscription"
@@ -367,6 +373,7 @@
tooltip="Select an existing subscription from GCP Pub/Sub"
showTooltipIcon
{item}
{disabled}
/>
{/snippet}
</ToggleButtonGroup>
@@ -382,6 +389,7 @@
autocomplete="off"
placeholder="Enter subscription ID (leave empty to auto-generate)"
bind:value={create_update_subscription_id}
disabled={!can_write}
oninput={(event) => {
subscription_id = event?.currentTarget.value
}}
@@ -394,14 +402,15 @@
tooltip="Select the delivery type for the Pub/Sub subscription. If the subscription already exists and you want to keep it as-is, choose the same delivery type as in Google Cloud. You can switch the type here if the API allows it — otherwise, make the change directly in Google Cloud."
>
<div class="flex flex-col gap-2 mt-2">
<ToggleButtonGroup bind:selected={delivery_type}>
{#snippet children({ item })}
<ToggleButtonGroup bind:selected={delivery_type} disabled={!can_write}>
{#snippet children({ item, disabled })}
<ToggleButton
label="Pull"
tooltip="Create a subscription where your service will pull messages from the queue. Suitable for services that periodically check for new messages."
value="pull"
showTooltipIcon
{item}
{disabled}
/>
<ToggleButton
label="Push"
@@ -409,6 +418,7 @@
showTooltipIcon
value="push"
{item}
{disabled}
/>
{/snippet}
</ToggleButtonGroup>
@@ -425,7 +435,7 @@
required={true}
/>
</p>
<Toggle bind:checked={delivery_config.authenticate} />
<Toggle bind:checked={delivery_config.authenticate} disabled={!can_write} />
</Subsection>
{#if delivery_config.authenticate}
<Subsection
@@ -450,6 +460,7 @@
<div class="flex gap-1">
<Select
loading={loadingSubscription}
disabled={!can_write}
disablePortal
clearable
class="grow shrink"
@@ -517,6 +517,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -633,7 +634,7 @@
tooltip="When enabled (recommended), Windmill automatically acknowledges Pub/Sub messages after successful processing. When disabled, your script/flow must explicitly acknowledge each message."
>
<div class="mt-2">
<Toggle bind:checked={auto_acknowledge_msg} />
<Toggle bind:checked={auto_acknowledge_msg} disabled={!can_write} />
</div>
{#if !auto_acknowledge_msg}
<div class="mt-3">
@@ -592,6 +592,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -727,6 +728,7 @@
{#key is_static_website}
<FileUpload
folderOnly={is_static_website}
disabled={!can_write}
allowMultiple={false}
randomFileKey={true}
on:addition={(evt) => {
@@ -528,6 +528,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -128,11 +128,26 @@
>
</span>
</div>
<ToggleButtonGroup bind:selected={v.qos}>
{#snippet children({ item })}
<ToggleButton value={'qos0'} label="At most once (QoS 0)" {item} />
<ToggleButton value={'qos1'} label="At least once (QoS 1)" {item} />
<ToggleButton value={'qos2'} label="Exactly once (QoS 2)" {item} />
<ToggleButtonGroup bind:selected={v.qos} disabled={!can_write}>
{#snippet children({ item, disabled })}
<ToggleButton
value={'qos0'}
label="At most once (QoS 0)"
{item}
{disabled}
/>
<ToggleButton
value={'qos1'}
label="At least once (QoS 1)"
{item}
{disabled}
/>
<ToggleButton
value={'qos2'}
label="Exactly once (QoS 2)"
{item}
{disabled}
/>
{/snippet}
</ToggleButtonGroup>
</label>
@@ -159,8 +174,9 @@
</div>
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Clear"
disabled={!can_write}
onclick={() => {
subscribe_topics = subscribe_topics.filter((_, index) => index !== i)
}}
@@ -175,6 +191,7 @@
variant="default"
size="xs"
btnClasses="mt-1"
disabled={!can_write}
on:click={() => {
subscribe_topics = [
...subscribe_topics,
@@ -502,6 +502,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -605,10 +606,10 @@
<div class="mt-4">
{#if optionTabSelected === 'connection_options'}
<div class="flex p-2 flex-col gap-2 mt-3">
<ToggleButtonGroup bind:selected={client_version}>
{#snippet children({ item })}
<ToggleButton value="v5" label="Version 5" {item} />
<ToggleButton value="v3" label="Version 3" {item} />
<ToggleButtonGroup bind:selected={client_version} disabled={!can_write}>
{#snippet children({ item, disabled })}
<ToggleButton value="v5" label="Version 5" {item} {disabled} />
<ToggleButton value="v3" label="Version 3" {item} {disabled} />
{/snippet}
</ToggleButtonGroup>
@@ -625,6 +626,7 @@
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!can_write}
bind:checked={v5_config.clean_start}
options={{
right: 'Clean start',
@@ -641,6 +643,7 @@
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!can_write}
bind:checked={activateV5Options.session_expiry_interval}
on:change={(ev) => {
if (!ev.detail) {
@@ -672,6 +675,7 @@
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!can_write}
bind:checked={activateV5Options.topic_alias_maximum}
on:change={(ev) => {
if (!ev.detail) {
@@ -703,6 +707,7 @@
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!can_write}
checked={v3_config.clean_session}
on:change={() => {
v3_config.clean_session = !v3_config.clean_session
@@ -499,6 +499,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -684,6 +684,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -837,6 +837,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -870,7 +871,7 @@
bind:value={summary}
/>
</label>
<LabelsInput bind:labels class="-mt-4" />
<LabelsInput bind:labels class="-mt-4" disabled={!can_write} />
<div class="flex flex-col gap-1">
<label for="path" class="text-xs font-semibold text-emphasis">Path</label>
@@ -980,7 +981,7 @@
disabled={!can_write}
/>
{#if showPauseUntil}
<DateTimeInput bind:value={paused_until} />
<DateTimeInput bind:value={paused_until} disabled={!can_write} />
{/if}
</div>
</div>
@@ -1030,6 +1031,7 @@
<Toggle
options={{ right: 'no overlap of flows' }}
bind:checked={no_flow_overlap}
disabled={!can_write}
class="mt-2"
/>
{/if}
@@ -1197,7 +1199,7 @@
<select
class="!w-14"
bind:value={failedExact}
disabled={!$enterpriseLicense || emptyString(errorHandlerPath)}
disabled={!can_write || !$enterpriseLicense || emptyString(errorHandlerPath)}
>
<option value={false}>&gt;=</option>
<option value={true}>==</option>
@@ -1206,7 +1208,7 @@
type="number"
class="!w-14 text-center {emptyString(errorHandlerPath) ? 'text-primary' : ''}"
bind:value={failedTimes}
disabled={!$enterpriseLicense}
disabled={!can_write || !$enterpriseLicense}
min="1"
/>
<p class={emptyString(errorHandlerPath) ? 'text-primary' : ''}
@@ -73,23 +73,30 @@
<ToggleButtonGroup
bind:selected={aws_auth_resource_type}
disabled={!can_write}
on:selected={() => {
aws_resource_path = ''
}}
>
{#snippet children({ item })}
<ToggleButton label="Credentials" value="credentials" {item} />
<ToggleButton label="Oidc" value="oidc" {item} />
{#snippet children({ item, disabled })}
<ToggleButton label="Credentials" value="credentials" {item} {disabled} />
<ToggleButton label="Oidc" value="oidc" {item} {disabled} />
{/snippet}
</ToggleButtonGroup>
{#if aws_auth_resource_type === 'credentials'}
<ResourcePicker workspace={wsId} resourceType="aws" bind:value={aws_resource_path} />
<ResourcePicker
workspace={wsId}
resourceType="aws"
bind:value={aws_resource_path}
disabled={!can_write}
/>
{:else if aws_auth_resource_type === 'oidc'}
<ResourcePicker
workspace={wsId}
resourceType="aws_oidc"
bind:value={aws_resource_path}
disabled={!can_write}
/>
{/if}
{#if isValid}
@@ -130,6 +137,7 @@
<div class="mt-2">
<ToggleButtonGroup
selected={tab}
disabled={!can_write}
on:selected={({ detail }) => {
if (detail === 'all') {
cached = message_attributes
@@ -140,9 +148,9 @@
tab = detail
}}
>
{#snippet children({ item })}
<ToggleButton value="all" label="All attributes" {item} />
<ToggleButton value="specific" label="Specific attributes" {item} />
{#snippet children({ item, disabled })}
<ToggleButton value="all" label="All attributes" {item} {disabled} />
<ToggleButton value="specific" label="Specific attributes" {item} {disabled} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -153,7 +161,7 @@
onCreateItem={(x) => message_attributes.push(x)}
placeholder="Set message attributes"
noItemsMsg="Add message attributes to filter on"
disabled={tab === 'all'}
disabled={tab === 'all' || !can_write}
/>
</div>
</Subsection>
@@ -481,6 +481,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa
@@ -572,6 +572,7 @@
{:else}
<PermissionedAsLine
{permissionedAs}
disabled={!can_write}
{path}
onPermissionedAsChange={(pa, preserve) => {
selectedPermissionedAs = pa