mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 16:02:36 +00:00
* fix(frontend): scope session pipeline trigger editors to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): hoist triggerWorkspace decl above GCP init-time getBaseUrl call Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope nested trigger pickers to the session workspace The triggerWorkspace resolver scoped direct trigger CRUD calls to the session's (forked) workspace, but nested pickers still defaulted to the nav `$workspaceStore`: in a fork session, resource lists/creation, variable creation, and path-existence checks ran against the parent workspace while save/delete targeted the fork — misleading options and false path-validation failures. Thread `wsId` into the nested controls of the 9 pipeline-canvas kinds: - `<Path workspaceOverride={wsId}>` (8 editors) — path + folder checks - `<ResourcePicker workspace={wsId}>` (6 config sections; add the resolver to MqttEditorConfigSection, which lacked `wsId`) - SQS `<VariableEditor workspace={wsId}>` — variable creation Also drop the per-site `wsId` rationale comment repeated across ~20 files; the invariant is documented once in triggerWorkspace.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope trigger runnable picker to the session workspace The runnable picker (`ScriptPicker`, reached via `TriggerRunnablePicker` and directly in the schedule editor) listed scripts/flows/apps from the nav `$workspaceStore` with no override, so a forked session offered the parent workspace's runnables when attaching a script/flow to a trigger. Add an optional `workspace` prop to `ScriptPicker` (defaults to `$workspaceStore` → no change for existing callers), pass it through `TriggerRunnablePicker`, and wire `wsId` from the 7 trigger editors that use it plus the schedule editor's 3 direct pickers. Completes the nested-picker workspace scoping; the excluded kinds (azure/http/websocket/native) keep their own ScriptPickers unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope trigger error-handler, folder default, and runnable actions Follow-up to the trigger-workspace scoping: three subtrees still read the nav `$workspaceStore` in a forked session. - ErrorOrRecoveryHandler (via TriggerRetriesAndErrorHandler in the 7 in-scope editors): add a `workspace` prop (defaults to `$workspaceStore`) and route handler lookup/schema, Slack/Teams settings, test jobs, and run links through it, so the error handler is resolved/tested/saved in the session workspace instead of A while the trigger lives in B. - useFolderDefaultPermissionedAs: accept an optional workspace getter so a `f/...` trigger's default permissioned-as is read from the session workspace, not the nav one (PermissionedAsLine passes `() => wsId`). - ScriptPicker actions: scope the View drawer (`getScriptByPath`) and `FlowPathViewer` to `effectiveWorkspace`, and carry `?workspace=` onto the Edit/View routes when an explicit override is set (the layout consumes the param, same mechanism as editInFork). The param is only appended when a workspace override is passed, so existing callers' links are unchanged. Also consolidate the repeated workspace-scoping comment in PipelineTriggerEditors (the invariant lives in triggerWorkspace.ts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope error-handler/schedule nested consumers to the session workspace Address CI Codex review on #10032 — the error-handler and schedule subtrees still had nested consumers reading the nav workspace: - ErrorOrRecoveryHandler: pass the resolved workspace to its own nested `ScriptPicker` (custom-handler list + View/Edit) and add a `workspace` prop to `ChannelSelector` (Teams channel listing); carry the acting workspace onto the "create from template" link. - ScheduleEditorInner: pass `workspace={wsId}` to the error/recovery/ success `ErrorOrRecoveryHandler` panels, `workspaceId={wsId}` to `WorkerTagPicker`, and the workspace query param onto the dynamic-skip template link. Template-link and picker overrides only diverge from `$workspaceStore` when a session override is set, so non-session callers are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
178 lines
4.8 KiB
Svelte
178 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import Select from './select/Select.svelte'
|
|
import { WorkspaceService } from '$lib/gen'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { RefreshCcw } from 'lucide-svelte'
|
|
import { Button } from './common'
|
|
|
|
interface ChannelItem {
|
|
channel_id?: string
|
|
channel_name?: string
|
|
}
|
|
|
|
interface Props {
|
|
disabled?: boolean
|
|
placeholder?: string
|
|
selectedChannel?: ChannelItem | undefined
|
|
containerClass?: string
|
|
minWidth?: string
|
|
channels?: ChannelItem[]
|
|
teamId?: string
|
|
showRefreshButton?: boolean
|
|
onError?: (error: Error) => void
|
|
onSelectedChannelChange?: (channel: ChannelItem | undefined) => void
|
|
/** Workspace to list Teams channels from; defaults to the nav
|
|
* `$workspaceStore`. A forked session passes its acting workspace. */
|
|
workspace?: string
|
|
}
|
|
|
|
let {
|
|
disabled = false,
|
|
placeholder = 'Select channel',
|
|
selectedChannel = $bindable(),
|
|
containerClass = 'w-64',
|
|
minWidth = '160px',
|
|
channels = undefined,
|
|
teamId,
|
|
showRefreshButton = true,
|
|
onError,
|
|
onSelectedChannelChange,
|
|
workspace = undefined
|
|
}: Props = $props()
|
|
|
|
let effectiveWorkspace = $derived(workspace ?? $workspaceStore)
|
|
|
|
let isFetching = $state(false)
|
|
let loadedChannels = $state<ChannelItem[]>([])
|
|
let loadedForTeamId = $state<string | undefined>(undefined)
|
|
|
|
const searchMode = $derived(!channels && !!teamId)
|
|
|
|
let displayChannels = $derived.by(() => {
|
|
const baseChannels = channels || loadedChannels
|
|
if (
|
|
selectedChannel &&
|
|
!baseChannels.find((c) => c.channel_id === selectedChannel?.channel_id)
|
|
) {
|
|
return [selectedChannel, ...baseChannels]
|
|
}
|
|
return baseChannels
|
|
})
|
|
|
|
// Single setter to bridge Select's string value -> selectedChannel object.
|
|
function setSelectedChannelById(newId: string | undefined) {
|
|
if (newId) {
|
|
const channel = displayChannels.find((c) => c.channel_id === newId)
|
|
if (channel && channel.channel_id !== selectedChannel?.channel_id) {
|
|
selectedChannel = channel
|
|
}
|
|
} else if (selectedChannel !== undefined) {
|
|
selectedChannel = undefined
|
|
}
|
|
}
|
|
|
|
let previousChannelId = $state<string | undefined>(undefined)
|
|
|
|
$effect(() => {
|
|
if (selectedChannel?.channel_id !== previousChannelId) {
|
|
previousChannelId = selectedChannel?.channel_id
|
|
onSelectedChannelChange?.(selectedChannel)
|
|
}
|
|
})
|
|
|
|
// Fetch channels when teamId is set or changes
|
|
$effect(() => {
|
|
if (searchMode && teamId && teamId !== loadedForTeamId) {
|
|
loadedForTeamId = teamId
|
|
fetchChannels()
|
|
}
|
|
})
|
|
|
|
async function fetchChannels() {
|
|
if (!teamId) return
|
|
|
|
isFetching = true
|
|
try {
|
|
const response = await WorkspaceService.listAvailableTeamsChannels({
|
|
workspace: effectiveWorkspace!,
|
|
teamId: teamId
|
|
})
|
|
|
|
loadedChannels =
|
|
response.channels?.map((c) => ({
|
|
channel_id: c.channel_id || '',
|
|
channel_name: c.channel_name || ''
|
|
})) || []
|
|
} catch (error) {
|
|
onError?.(error as Error)
|
|
console.error('Error fetching channels:', error)
|
|
loadedChannels = []
|
|
} finally {
|
|
isFetching = false
|
|
}
|
|
}
|
|
|
|
async function refreshChannels() {
|
|
if (searchMode) {
|
|
await fetchChannels()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class={containerClass}>
|
|
<div class="flex flex-col gap-1">
|
|
<div class="flex items-center gap-1">
|
|
<div class="flex-grow" style="min-width: {minWidth};">
|
|
{#if searchMode}
|
|
<Select
|
|
containerStyle={'min-width: ' + minWidth}
|
|
items={displayChannels
|
|
.filter((channel) => channel.channel_id && channel.channel_name)
|
|
.map((channel) => ({
|
|
label: channel.channel_name ?? 'Unknown Channel',
|
|
value: channel.channel_id ?? ''
|
|
}))}
|
|
placeholder={isFetching ? 'Loading...' : teamId ? placeholder : 'Select a team first'}
|
|
clearable
|
|
disabled={disabled || !teamId}
|
|
loading={isFetching}
|
|
bind:value={() => selectedChannel?.channel_id, (newId) => setSelectedChannelById(newId)}
|
|
/>
|
|
{:else}
|
|
<Select
|
|
containerStyle={'min-width: ' + minWidth}
|
|
items={displayChannels
|
|
.filter((channel) => channel.channel_id && channel.channel_name)
|
|
.map((channel) => ({
|
|
label: channel.channel_name ?? 'Unknown Channel',
|
|
value: channel.channel_id ?? ''
|
|
}))}
|
|
{placeholder}
|
|
clearable
|
|
disabled={disabled || displayChannels.length === 0}
|
|
bind:value={() => selectedChannel?.channel_id, (newId) => setSelectedChannelById(newId)}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if showRefreshButton && searchMode}
|
|
<Button
|
|
onclick={refreshChannels}
|
|
disabled={isFetching || disabled || !teamId}
|
|
title="Refresh channels"
|
|
startIcon={{ icon: RefreshCcw, props: { class: isFetching ? 'animate-spin' : '' } }}
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
iconOnly
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if searchMode && loadedChannels.length > 0 && !isFetching}
|
|
<span class="text-2xs text-tertiary pl-1">
|
|
{loadedChannels.length} channel{loadedChannels.length === 1 ? '' : 's'}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|