Files
windmill/frontend/src/lib/components/triggers/CaptureWrapper.svelte
T
6186a0645d feat: edit variables, resources and triggers in their own session tab (#11206)
* feat: edit variables, resources and triggers in their own session tab

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: take a trigger's flow kind from its config and name the written trigger

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: one operating-workspace context for editors acting on a session's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: reload page item tabs only in sessions acting on the tool's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: resolve open in workspace for every preview tab kind in one place

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: guard every component under a session editor against the navigation workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: show a page item tab whose item is gone instead of mounting its editor

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: follow a saved page item in the tab's own workspace and restore in-frame rows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: close a page item tab from its editor header

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge trigger permissions by the user acting in the session workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge every session-editor permission by the user acting in that workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep session permissions reactive to the acting role and restore tabs as last seen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: default Path's owner controls to the user acting in its workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-19 23:29:14 +02:00

390 lines
8.9 KiB
Svelte

<script lang="ts">
import { CaptureService, type CaptureConfig, type CaptureTriggerKind } from '$lib/gen'
import { onDestroy, untrack } from 'svelte'
import { sendUserToast, sleep } from '$lib/utils'
import RouteCapture from './http/RouteCapture.svelte'
import type { ConnectionInfo } from '../common/alert/ConnectionIndicator.svelte'
import type { CaptureInfo } from './CaptureSection.svelte'
import WebhooksCapture from './webhook/WebhooksCapture.svelte'
import DefaultEmailCapture from './email/DefaultEmailCapture.svelte'
import WebsocketCapture from './websocket/WebsocketCapture.svelte'
import PostgresCapture from './postgres/PostgresCapture.svelte'
import KafkaCapture from './kafka/KafkaCapture.svelte'
import NatsCapture from './nats/NatsCapture.svelte'
import MqttCapture from './mqtt/MqttCapture.svelte'
import AmqpCapture from './amqp/AmqpCapture.svelte'
import SqsCapture from './sqs/SqsCapture.svelte'
import GcpCapture from './gcp/GcpCapture.svelte'
import AzureCapture from './azure/AzureCapture.svelte'
import EmailCapture from './email/EmailCapture.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
isFlow: boolean
path: string
hasPreprocessor: boolean
canHavePreprocessor: boolean
captureType?: CaptureTriggerKind
data?: any
connectionInfo?: ConnectionInfo | undefined
args?: Record<string, any>
isValid?: boolean
triggerDeployed?: boolean
}
let {
isFlow,
path,
hasPreprocessor,
canHavePreprocessor,
captureType = 'webhook',
data = {},
connectionInfo = $bindable(undefined),
args = {},
isValid = false,
triggerDeployed = false
}: Props = $props()
let captureLoading = $state(false)
let captureActive = $state(false)
let captureConfigs: {
[key: string]: CaptureConfig
} = $state({})
let ready = $state(false)
const config: CaptureConfig | undefined = $derived(captureConfigs[captureType])
export async function setConfig(): Promise<boolean> {
try {
await CaptureService.setCaptureConfig({
requestBody: {
trigger_kind: captureType,
path,
is_flow: isFlow,
trigger_config: args && Object.keys(args).length > 0 ? args : undefined
},
workspace: $operatingWorkspace!
})
return true
} catch (error) {
sendUserToast(error.body, true)
return false
}
}
function isStreamingCapture() {
if (captureType === 'gcp' && args.delivery_type === 'push') {
return false
}
if (captureType === 'azure' && args.azure_mode !== 'namespace_pull') {
return false
}
return [
'mqtt',
'amqp',
'sqs',
'websocket',
'postgres',
'kafka',
'nats',
'gcp',
'azure'
].includes(captureType)
}
async function getCaptureConfigs() {
const captureConfigsList = await CaptureService.getCaptureConfigs({
workspace: $operatingWorkspace!,
runnableKind: isFlow ? 'flow' : 'script',
path
})
captureConfigs = captureConfigsList.reduce((acc, c) => {
acc[c.trigger_kind] = c
return acc
}, {})
if (isStreamingCapture() && captureActive) {
const config = captureConfigs[captureType]
if (config && config.error) {
const serverEnabled = getServerEnabled(config)
if (!serverEnabled) {
sendUserToast('Capture was stopped because of error: ' + config.error, true)
captureActive = false
}
}
}
return captureConfigs
}
async function capture() {
let i = 0
captureActive = true
while (captureActive) {
if (i % 3 === 0) {
await CaptureService.pingCaptureConfig({
workspace: $operatingWorkspace!,
triggerKind: captureType,
runnableKind: isFlow ? 'flow' : 'script',
path
})
await getCaptureConfigs()
}
i++
await sleep(1000)
}
}
onDestroy(() => {
captureActive = false
})
function getServerEnabled(config: CaptureConfig) {
return (
!!config.last_server_ping &&
new Date(config.last_server_ping).getTime() > new Date().getTime() - 15 * 1000
)
}
export async function handleCapture(e: CustomEvent<{ disableOnly?: boolean }>) {
if (captureActive || e.detail.disableOnly) {
captureActive = false
} else {
try {
captureLoading = true
const configSet = await setConfig()
if (configSet) {
capture()
}
} finally {
captureLoading = false
}
}
}
function updateConnectionInfo(config: CaptureConfig | undefined, captureActive: boolean) {
if (isStreamingCapture() && config && captureActive) {
const serverEnabled = getServerEnabled(config)
const connected = serverEnabled && !config.error
const message = connected
? `Connected`
: `Not connected${config.error ? ': ' + config.error : ''}`
connectionInfo = {
connected,
message
}
} else {
connectionInfo = undefined
}
}
$effect(() => {
const args = [config, captureActive] as const
untrack(() => updateConnectionInfo(...args))
})
let captureInfo: CaptureInfo = $derived({
active: captureActive,
hasPreprocessor,
canHavePreprocessor,
isFlow,
path,
connectionInfo
})
$effect(() => {
args && (captureActive = false)
})
</script>
{#key ready}
<div class="flex flex-col gap-4 w-full h-full">
{#if captureType === 'websocket'}
<WebsocketCapture
{isValid}
{captureInfo}
{captureLoading}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'postgres'}
<PostgresCapture
{captureInfo}
{captureLoading}
{isValid}
{hasPreprocessor}
{isFlow}
{triggerDeployed}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'webhook'}
<WebhooksCapture
{hasPreprocessor}
{isFlow}
{path}
runnableArgs={data?.args}
{captureInfo}
{captureLoading}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'http'}
<RouteCapture
runnableArgs={data?.args}
route_path={args.route_path}
http_method={args.http_method}
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'default_email'}
<DefaultEmailCapture
{path}
{isFlow}
emailDomain={data?.emailDomain}
{captureInfo}
{hasPreprocessor}
{captureLoading}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'kafka'}
<KafkaCapture
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
groupId={args?.group_id}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'nats'}
<NatsCapture
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
{triggerDeployed}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'mqtt'}
<MqttCapture
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
{triggerDeployed}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'amqp'}
<AmqpCapture
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
{triggerDeployed}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'sqs'}
<SqsCapture
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
{triggerDeployed}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'gcp'}
<GcpCapture
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{triggerDeployed}
deliveryType={args.delivery_type}
{captureLoading}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'azure'}
<AzureCapture
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
subscriptionName={args.subscription_name}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'email'}
<EmailCapture
local_part={args.local_part}
emailDomain={data?.emailDomain}
{isValid}
{captureInfo}
{hasPreprocessor}
{isFlow}
{captureLoading}
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{/if}
</div>
{/key}