mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
refactor(frontend): capture refactor UI (#5591)
* migrate postgresEditorInner to svelte 5 syntaxe
* Add postgress
* add postgres capture section
* migrate kafka trigger config to svelte 5
* use snippets in kafka config
* feat: Update KafkaTriggerEditorInner with editMode, hideTarget and other props from WebsocketTriggerEditorInner
* feat: Create KafkaTriggerPanelV2 following WebsocketTriggersPanelV2 model
* feat: Integrate KafkaTriggerPanelV2 into TriggersEditorV2
* feat: add kafka capture panel
* use svelte 5 syntaxe for nats config
* use snippet in nats config
* feat: add editMode for nats trigger config
* fix import
* create NatsTriggerPanelV2.svelte for NATS trigger
* integrate NatsTriggerPanelV2 into TriggersEditorV2.svelte
* Create NatsCapture.svelte and update CaptureWrapperV2.svelte to use it
* migrate MqttTriggerEditorInner to svelte 5
* add mqtt trigger to new trigger panel
* migrate SqsTriggerEditorInner to svelte 5 syntaxe
* feat: add sqs triggers panel
* Add badge to MQTT config section
* remove unnecessary binding
* add gcp to the trigger list
* add prop size to GCP icon
* add gcp trigger utils
* migrate GcpTriggerEditorInner to svelte5 syntaxe
* add gcp trigger panel
* Revert "add gcp trigger utils"
This reverts commit 58f0df9985.
* fix type error
* add gcp to trigger bar
* add capture gcp panel
* add schedule poll panel
* feat: add trigger table horizontal bar
* add http trigger label
* make triggerbar vertical
* nit
* add trigger delete button for drafts
* enable edit from table for all triggers
* fix automatic edit mode for draft trigger
* fix proxy dispatching issue
* fix primary star positionning
* fix apply args to preprocessor
* automatically load bigpayloads
* clean capture wrapper code
* nit
* fix capture config not passed
* use Url component
* remove unused prop captureTable
* use new schedule panel for scheduled poll
* update triggersWrapperV2
* improve webhook capture panel
* remove debug log
* save draft with flow draft
* add path in draft label when available
* remove flowcard title
* use trigger label for trigger badge dropdown
* break from table to sidebar at 1000px
* remove triggers badge allways show sidebar behavior
* fix multiple primary addition
* improve primary schedule deploy logic
* remove debug logs
* gather imports
* add path for draft primary schedule
* handle redirection
* Add confirmation modal to deploy triggers
* remove debug log
* Save draft route trigger on deploy
* Allow draft for deployed triggers
* add component trigger editor toolbar
* improve update trigger utility function
* Add reset button for deployed draft triggers
* allow draft trigger for kafka triggers
* allow draft trigger for websocket triggers
* allow draft trigger for postgres triggers
* Add toggle to trigger toolbar
* allow draft for nats triggers
* allow draft for mqtt triggers
* allow draft sqs triggers
* allow draft for gcp triggers
* revert changes on the schedule editor
* update all triggers to use the same toolbar
* remove debug logs
* remove PrimarySchedule Panel and merge primary schedule logic with all triggers
* load schema from draft when editing a draft schedule
* prevent overflow in trigger table
* fix path saving for draft schedule
* fix trigger badge dropdown display
* get draft status from trigger
* remove unused event
* nit
* Add deploy badge to trigger
* replace checkbox with toggle for the modal
* fix selection of triggers with draft
* fix nit
* fix nit
* add missing config for loading schedule
* save schedule from config
* save http route from cfg
* save websocket trigger from cfg
* save postgres trigger from cfg
* save kafka trigger from cfg
* save sqs trigger from cfg
* save nats trigger from cfg
* save mqtt triggers from cfg
* save gcp trigger from config
* nit
* fix path update when editing a trigger
* remove unused saveCb
* fix trigger node navigation
* remove debug log
* add min size for trigger editor capture panel
* Integrate new triggers Editor to script builder
* remove previous triggersEdior page
* Remove former triggers Editor
* Migrate detail page to new triggers editor UI
* Remove unused dev TriggersBadge
* migrate triggers to V2
* Migrate capture components to V2
* improve triggers badge UI
* fix trigger panel with scheduled poll
* fix auto select all draft on deploy
* simplify permission check
* Add custom label to every trigger editor
* fix draft not deleting when saving triggers and add option to deploy now
* remove unused utility function
* fix triggers loading from draft
* make advanced route section collapsible
* handle subflow triggers display
* center triggers badge
* fix panel redirection when adding a new triggers from node
* Remove unnecessary alerts
* Fix proxy issue in schedule editor inner
* Remove unused schedule pages
* nit
* nit cli panel
* Migrate selectedTrigger store to V2
* handle extra header button for trigger editor panel
* prevent reload on toggle schedule enable
* remove unused utility function
* remove primaryScheduleStore
* handle triggerCount with drafts
* fix editing in Schedule Poll Mode
* Remove triggers default value context store
* fix triggers label max width
* Remove dbg logs
* improve no trigger selected message
* nit
* Do not show enable toggle for http triggers
* Adapt ui to dark mode
* nit
* remove captureOn context value
* improve dark mode
* fix nit
* fix merge issue
* nit
* nit
* fix schedule permission initialisation
* nit
* Disable deploy now if needed
* Improve utilitu function for handling with triggers
* improve trigger badge look
* fix kafka
* nit
* fix schedule display
* fix toggling edit mode
* fix edit/cancel edit
* nit
* prevent capture config update in editor
* migrate webhook panel to svelte 5
* fix postgres trigger creation
* fix bind issue in nats triggers
* fix modal triggers message flow/script
* fix apply args for script
* fix gcp triggers update when creating
* fix gcp capture
* nits
* add loading state for deploying all triggers
* remove default mqtt
* update base_endpoint for gcp
* nit
* fix
* fix
* remove base endpoint
* add catch in case of error while creating a trigger
* Add animation is capture panels
* show preprocessor extra when there is a preprocessor
* Add loading state for captures
* add missing fields for capture
* migrate URL to svelte 5
* show icon by default in confirmation modal
* do not show draft triggers in detail badge list
* migrate to new captures
* revert unwanted change
* migrate to preprocesor v2
* nit
* fix email capture
* nits and fix
* nits
* update repo ref
* Feat: auto save draft instad of save button
* use triggerIndex as selection
* Add modified tag to triggers
* Update trigger modal
* fix trigger node select
* fix schedule page
* migrate TriggersEditor to svelte 5
* remove unused props
* fix schedule editor init
* fix store reactivity issue
* use class instead of store for triggers context
* nit
* nit
* fix schedule init
* Add postgres error message
* nit
* fix deploy new script
* load schema in shedule editor
* improve transitions
* Improve capture indication
* revert unwanted changes
* improve update
* nit
* migrate captures section to svelte 5
* only show trash on hover triggers row
* Add token count
* improve trigger display
* Add relevent naming for triggers
* Use wrapper div insted of width binding in flow card
* add trigger sorting
* nit
* handle reload from URL for draft triggers
* dirty script and flow draft with draft triggers
* handle cloud hosted triggers
* nit
* nit
* fix nats reactivity
* remove debug log
* handle deployed draft triggers in the flow editor
* Add warning for event stream capture
---------
Co-authored-by: dieriba <dieriba.pro@gmail.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
committed by
GitHub
co-authored by
dieriba
Ruben Fiszel
parent
b7812bfd09
commit
ea74f584b1
@@ -5,6 +5,9 @@
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use axum::{body::Body, response::Response};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
@@ -35,6 +38,16 @@ pub enum RunnableKind {
|
||||
Flow,
|
||||
}
|
||||
|
||||
impl Display for RunnableKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let runnable_kind = match self {
|
||||
RunnableKind::Script => "script",
|
||||
RunnableKind::Flow => "flow"
|
||||
};
|
||||
write!(f, "{}", runnable_kind)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
|
||||
let is_admin = is_super_admin_email(db, email).await?;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let link: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="text-xs text-tertiary font-normal">
|
||||
<div class={twMerge('text-xs text-tertiary font-normal opacity-80', $$props.class)}>
|
||||
<slot />
|
||||
{#if link}
|
||||
<a href={link} target="_blank">Learn more</a>
|
||||
|
||||
@@ -47,10 +47,11 @@
|
||||
parseTypescriptDeps
|
||||
} from '$lib/relative_imports'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
import type { TriggerContext } from './triggers'
|
||||
import { workspaceAIClients } from './copilot/lib'
|
||||
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
$: token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
$: themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
|
||||
@@ -493,20 +494,13 @@
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
primarySchedule: primaryScheduleStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false),
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined),
|
||||
showCaptureHint: writable(undefined)
|
||||
showCaptureHint: writable(undefined),
|
||||
triggersState: new Triggers()
|
||||
})
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import PdfViewer from './display/PdfViewer.svelte'
|
||||
import type { DisplayResultUi } from './custom_ui'
|
||||
import { getContext, hasContext, createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { toJsonStr } from '$lib/utils'
|
||||
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
|
||||
|
||||
export let result: any
|
||||
@@ -269,15 +270,6 @@
|
||||
let jsonViewer: Drawer
|
||||
let s3FileViewer: S3FilePicker
|
||||
|
||||
function toJsonStr(result: any) {
|
||||
try {
|
||||
// console.log(result)
|
||||
return JSON.stringify(result ?? null, null, 4) ?? 'null'
|
||||
} catch (e) {
|
||||
return 'error stringifying object: ' + e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfHasBigInt(result: any) {
|
||||
if (typeof result === 'number' && Number.isInteger(result) && !Number.isSafeInteger(result)) {
|
||||
return true
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={twMerge('flex flex-row gap-2.5 z-10 text-tertiary -mt-1')}>
|
||||
<div class={twMerge('flex flex-row gap-2.5 z-10 text-tertiary -mt-1 items-center')}>
|
||||
{#if customUi?.disableDownload !== true}
|
||||
<a
|
||||
download="{filename ?? 'result'}.json"
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
export let fixedHeight = true
|
||||
export let hidePopup = false
|
||||
export let open = false
|
||||
export let customWidth: number | undefined = undefined
|
||||
export let customMenu = false
|
||||
|
||||
const {
|
||||
elements: { menu, item, trigger },
|
||||
@@ -106,11 +108,16 @@
|
||||
</button>
|
||||
|
||||
{#if open && !hidePopup}
|
||||
<div use:melt={$menu} data-menu class="z-[6000]">
|
||||
<div
|
||||
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
|
||||
>
|
||||
<DropdownV2Inner items={computeItems} meltItem={item} />
|
||||
</div>
|
||||
<div use:melt={$menu} data-menu class="z-[6000] transition-all duration-100">
|
||||
{#if customMenu}
|
||||
<slot name="menu" />
|
||||
{:else}
|
||||
<div
|
||||
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
|
||||
style={customWidth ? `width: ${customWidth}px` : ''}
|
||||
>
|
||||
<DropdownV2Inner items={computeItems} meltItem={item} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { MenubarMenuElements } from '@melt-ui/svelte'
|
||||
import type { Item } from '$lib/utils'
|
||||
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
|
||||
export let meltItem: MenubarMenuElements['item']
|
||||
|
||||
let computedItems: Item[] | undefined = undefined
|
||||
interface Props {
|
||||
items?: Item[] | (() => Item[]) | (() => Promise<Item[]>)
|
||||
meltItem: MenubarMenuElements['item']
|
||||
}
|
||||
|
||||
let { items = [], meltItem }: Props = $props()
|
||||
|
||||
let computedItems: Item[] | undefined = $state(undefined)
|
||||
async function computeItems() {
|
||||
if (typeof items === 'function') {
|
||||
computedItems = ((await items()) ?? []).filter((item) => !item.hide)
|
||||
@@ -38,9 +43,12 @@
|
||||
item={meltItem}
|
||||
>
|
||||
{#if item.icon}
|
||||
<svelte:component this={item.icon} size={14} color={item.iconColor} />
|
||||
<item.icon size={14} color={item.iconColor} />
|
||||
{/if}
|
||||
{item.displayName}
|
||||
<p title={item.displayName} class="truncate grow min-w-0 whitespace-nowrap text-left">
|
||||
{item.displayName}
|
||||
</p>
|
||||
{@render item.extra?.()}
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
FlowService,
|
||||
ScheduleService,
|
||||
type Flow,
|
||||
type FlowModule,
|
||||
DraftService,
|
||||
@@ -19,12 +18,12 @@
|
||||
enterpriseLicense,
|
||||
tutorialsToDo,
|
||||
userStore,
|
||||
workspaceStore
|
||||
workspaceStore,
|
||||
usedTriggerKinds
|
||||
} from '$lib/stores'
|
||||
import {
|
||||
cleanValueProperties,
|
||||
encodeState,
|
||||
formatCron,
|
||||
generateRandomString,
|
||||
orderedJsonStringify,
|
||||
replaceFalseWithUndefined,
|
||||
@@ -76,6 +75,14 @@
|
||||
import { type TriggerContext, type ScheduleTrigger } from './triggers'
|
||||
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
|
||||
import DeployButton from './DeployButton.svelte'
|
||||
import type { FlowWithDraftAndDraftTriggers, Trigger } from './triggers/utils'
|
||||
import {
|
||||
deployTriggers,
|
||||
filterDraftTriggers,
|
||||
handleSelectTriggerFromKind
|
||||
} from './triggers/utils'
|
||||
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
|
||||
export let initialPath: string = ''
|
||||
export let pathStoreInit: string | undefined = undefined
|
||||
@@ -85,18 +92,16 @@
|
||||
export let loading = false
|
||||
export let flowStore: Writable<OpenFlow>
|
||||
export let flowStateStore: Writable<FlowState>
|
||||
export let savedFlow:
|
||||
| (Flow & {
|
||||
draft?: Flow | undefined
|
||||
})
|
||||
| undefined = undefined
|
||||
export let savedFlow: FlowWithDraftAndDraftTriggers | undefined = undefined
|
||||
export let diffDrawer: DiffDrawer | undefined = undefined
|
||||
export let customUi: FlowBuilderWhitelabelCustomUi = {}
|
||||
export let disableAi: boolean = false
|
||||
export let disabledFlowInputs = false
|
||||
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
|
||||
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined // used to set the primary schedule in the legacy primaryScheduleStore
|
||||
export let version: number | undefined = undefined
|
||||
export let setSavedraftCb: ((cb: () => void) => void) | undefined = undefined
|
||||
export let draftTriggersFromUrl: Trigger[] | undefined = undefined
|
||||
export let selectedTriggerIndexFromUrl: number | undefined = undefined
|
||||
|
||||
let initialPathStore = writable(initialPath)
|
||||
$: initialPathStore.set(initialPath)
|
||||
@@ -116,12 +121,26 @@
|
||||
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
|
||||
let open: boolean = false // Is confirmation modal open
|
||||
|
||||
// Draft triggers confirmation modal
|
||||
let draftTriggersModalOpen = false
|
||||
let confirmDeploymentCallback: (triggersToDeploy: Trigger[]) => void = () => {}
|
||||
|
||||
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
|
||||
const { selectedTriggers } = event.detail
|
||||
// Continue with saving the flow
|
||||
draftTriggersModalOpen = false
|
||||
confirmDeploymentCallback(selectedTriggers)
|
||||
}
|
||||
|
||||
$: setContext('customUi', customUi)
|
||||
|
||||
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
|
||||
return {
|
||||
savedValue: savedFlow,
|
||||
modifiedValue: $flowStore
|
||||
modifiedValue: {
|
||||
...$flowStore,
|
||||
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
}
|
||||
}
|
||||
}
|
||||
let onLatest = true
|
||||
@@ -148,42 +167,25 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
|
||||
const triggersCount = writable<TriggersCount | undefined>(
|
||||
savedPrimarySchedule
|
||||
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
|
||||
: undefined
|
||||
)
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // kept for legacy reasons
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const simplifiedPoll = writable(false)
|
||||
|
||||
// used to set the primary schedule in the legacy primaryScheduleStore
|
||||
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
|
||||
primaryScheduleStore.set(schedule)
|
||||
}
|
||||
|
||||
export function setDraftTriggers(triggers: Trigger[] | undefined) {
|
||||
triggersState.setTriggers([
|
||||
...(triggers ?? []),
|
||||
...triggersState.triggers.filter((t) => !t.draftConfig)
|
||||
])
|
||||
loadTriggers()
|
||||
}
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
if ($primaryScheduleStore) {
|
||||
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
|
||||
|
||||
try {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: path,
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
script_path: path,
|
||||
is_flow: true,
|
||||
args,
|
||||
enabled,
|
||||
summary
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
sendUserToast(`The primary schedule could not be created: ${err}`, true)
|
||||
}
|
||||
} else {
|
||||
sendUserToast('The primary schedule could not be created: no schedule data', true)
|
||||
}
|
||||
export function setSelectedTriggerIndex(index: number | undefined) {
|
||||
triggersState.selectedTriggerIndex = index
|
||||
}
|
||||
|
||||
let loadingSave = false
|
||||
@@ -195,7 +197,12 @@
|
||||
}
|
||||
if (savedFlow) {
|
||||
const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow)
|
||||
const current = cleanValueProperties({ ...$flowStore, path: $pathStore })
|
||||
const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
const current = cleanValueProperties({
|
||||
...$flowStore,
|
||||
path: $pathStore,
|
||||
draft_triggers: currentDraftTriggers
|
||||
})
|
||||
if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
|
||||
sendUserToast('No changes detected, ignoring', false, [
|
||||
{
|
||||
@@ -259,7 +266,7 @@
|
||||
value: {
|
||||
...flow,
|
||||
path: $pathStore,
|
||||
primary_schedule: $primaryScheduleStore
|
||||
draft_triggers: triggersState.getDraftTriggersSnapshot()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -270,15 +277,14 @@
|
||||
...structuredClone($flowStore),
|
||||
path: $pathStore,
|
||||
draft_only: true
|
||||
}
|
||||
}
|
||||
: savedFlow),
|
||||
draft: {
|
||||
...structuredClone($flowStore),
|
||||
path: $pathStore
|
||||
path: $pathStore,
|
||||
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
}
|
||||
} as Flow & {
|
||||
draft?: Flow
|
||||
}
|
||||
} as FlowWithDraftAndDraftTriggers
|
||||
|
||||
let savedAtNewPath = false
|
||||
if (newFlow) {
|
||||
@@ -351,7 +357,19 @@
|
||||
deployedBy = flow.edited_by
|
||||
}
|
||||
|
||||
async function saveFlow(deploymentMsg?: string): Promise<void> {
|
||||
async function saveFlow(deploymentMsg?: string, triggersToDeploy?: Trigger[]): Promise<void> {
|
||||
if (!triggersToDeploy) {
|
||||
// Check if there are draft triggers that need confirmation
|
||||
const draftTriggers = triggersState.triggers.filter((trigger) => trigger.draftConfig)
|
||||
if (draftTriggers.length > 0) {
|
||||
draftTriggersModalOpen = true
|
||||
confirmDeploymentCallback = async (triggersToDeploy: Trigger[]) => {
|
||||
await saveFlow(deploymentMsg, triggersToDeploy)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loadingSave = true
|
||||
try {
|
||||
const flow = cleanInputs($flowStore)
|
||||
@@ -390,8 +408,15 @@
|
||||
},
|
||||
runnableKind: 'flow'
|
||||
})
|
||||
if ($primaryScheduleStore && $primaryScheduleStore.enabled) {
|
||||
await createSchedule($pathStore)
|
||||
if (triggersToDeploy) {
|
||||
await deployTriggers(
|
||||
triggersToDeploy,
|
||||
$workspaceStore,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
usedTriggerKinds,
|
||||
$pathStore,
|
||||
true
|
||||
)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -400,51 +425,14 @@
|
||||
console.error('error interacting with local storage', e)
|
||||
}
|
||||
|
||||
const scheduleExists = await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
})
|
||||
|
||||
if (scheduleExists) {
|
||||
const schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
})
|
||||
if ($primaryScheduleStore) {
|
||||
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
|
||||
|
||||
if (
|
||||
JSON.stringify(schedule.args) != JSON.stringify(args) ||
|
||||
schedule.schedule != cron ||
|
||||
schedule.timezone != timezone ||
|
||||
schedule.summary != summary
|
||||
) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
args,
|
||||
summary
|
||||
}
|
||||
})
|
||||
}
|
||||
if (enabled != schedule.enabled) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
}
|
||||
} else if (scheduleExists && !$triggersCount?.primary_schedule) {
|
||||
await ScheduleService.deleteSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: $pathStore
|
||||
})
|
||||
}
|
||||
} else if ($primaryScheduleStore && $primaryScheduleStore.enabled) {
|
||||
await createSchedule(initialPath)
|
||||
if (triggersToDeploy) {
|
||||
await deployTriggers(
|
||||
triggersToDeploy,
|
||||
$workspaceStore,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
usedTriggerKinds,
|
||||
initialPath
|
||||
)
|
||||
}
|
||||
|
||||
await FlowService.updateFlow({
|
||||
@@ -465,10 +453,15 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const { draft_triggers: _, ...newSavedFlow } = $flowStore as OpenFlow & {
|
||||
draft_triggers: Trigger[]
|
||||
}
|
||||
savedFlow = {
|
||||
...structuredClone($flowStore),
|
||||
...structuredClone(newSavedFlow),
|
||||
path: $pathStore
|
||||
} as Flow
|
||||
triggersState.setTriggers([])
|
||||
loadingSave = false
|
||||
dispatch('deploy', $pathStore)
|
||||
} catch (err) {
|
||||
@@ -496,7 +489,8 @@
|
||||
flow: $flowStore,
|
||||
path: $pathStore,
|
||||
selectedId: $selectedIdStore,
|
||||
primarySchedule: $primaryScheduleStore
|
||||
draft_triggers: triggersState.getDraftTriggersSnapshot(),
|
||||
selected_trigger: triggersState.getSelectedTriggerSnapshot()
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
@@ -506,16 +500,6 @@
|
||||
}
|
||||
|
||||
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
|
||||
const selectedTriggerStore = writable<
|
||||
| 'webhooks'
|
||||
| 'emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets'
|
||||
| 'postgres'
|
||||
| 'scheduledPoll'
|
||||
>('webhooks')
|
||||
|
||||
export function getSelectedId() {
|
||||
return $selectedIdStore
|
||||
@@ -541,20 +525,6 @@
|
||||
selectedIdStore.set(selectedId)
|
||||
}
|
||||
|
||||
function selectTrigger(
|
||||
selectedTrigger:
|
||||
| 'webhooks'
|
||||
| 'emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets'
|
||||
| 'postgres'
|
||||
| 'scheduledPoll'
|
||||
) {
|
||||
selectedTriggerStore.set(selectedTrigger)
|
||||
}
|
||||
|
||||
let insertButtonOpen = writable<boolean>(false)
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
@@ -577,29 +547,42 @@
|
||||
flowInputEditorState: flowInputEditorStateStore
|
||||
})
|
||||
|
||||
// Add triggers context store
|
||||
const triggersState = new Triggers(
|
||||
[
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'email', path: '', isDraft: false },
|
||||
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
|
||||
],
|
||||
selectedTriggerIndexFromUrl,
|
||||
saveSessionDraft
|
||||
)
|
||||
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount,
|
||||
simplifiedPoll,
|
||||
defaultValues: writable(undefined),
|
||||
captureOn,
|
||||
showCaptureHint
|
||||
showCaptureHint,
|
||||
triggersState
|
||||
})
|
||||
|
||||
async function loadTriggers() {
|
||||
export async function loadTriggers() {
|
||||
$triggersCount = await FlowService.getTriggersCountOfFlow({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
if ($primaryScheduleStore && $triggersCount.primary_schedule == undefined) {
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount.schedule_count ?? 0) + 1,
|
||||
primary_schedule: {
|
||||
schedule: $primaryScheduleStore.cron
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize triggers using utility function
|
||||
await triggersState.fetchTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
initialPath,
|
||||
true,
|
||||
$primaryScheduleStore,
|
||||
$userStore
|
||||
)
|
||||
|
||||
if (savedFlow && savedFlow.draft) {
|
||||
savedFlow = filterDraftTriggers(savedFlow, triggersState) as FlowWithDraftAndDraftTriggers
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,16 +854,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (module.type === 'trigger') {
|
||||
$primaryScheduleStore = {
|
||||
summary: 'Scheduled poll of flow',
|
||||
args: {},
|
||||
cron: '0 */15 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
const flowModule: FlowModule & {
|
||||
value: RawScript | PathScript
|
||||
} = {
|
||||
@@ -890,7 +863,7 @@
|
||||
? {
|
||||
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
|
||||
skip_if_stopped: true
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
value: {
|
||||
input_transforms: {},
|
||||
@@ -974,7 +947,7 @@
|
||||
pastModule?.value.type === 'rawscript' || pastModule?.value.type === 'script'
|
||||
? (pastModule as FlowModule & {
|
||||
value: RawScript | PathScript
|
||||
})
|
||||
})
|
||||
: undefined,
|
||||
isFirstInLoop,
|
||||
abortController
|
||||
@@ -1094,8 +1067,8 @@
|
||||
? isFirstInLoop
|
||||
? 'flow_input.iter.value'
|
||||
: pastModule
|
||||
? 'results.' + pastModule.id
|
||||
: 'flow_input.' + snakeKey
|
||||
? 'results.' + pastModule.id
|
||||
: 'flow_input.' + snakeKey
|
||||
: 'flow_input.' + snakeKey
|
||||
}
|
||||
$shouldUpdatePropertyType[key] = 'javascript'
|
||||
@@ -1228,7 +1201,7 @@
|
||||
},
|
||||
disabled: newFlow
|
||||
}
|
||||
]
|
||||
]
|
||||
: []),
|
||||
...(customUi?.topBar?.history != false
|
||||
? [
|
||||
@@ -1242,11 +1215,23 @@
|
||||
icon: FileJson,
|
||||
action: () => yamlEditorDrawer?.openDrawer()
|
||||
}
|
||||
]
|
||||
]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
|
||||
function handleDeployTrigger(trigger: Trigger) {
|
||||
const { id, path, type } = trigger
|
||||
//Update the saved flow to remove the draft trigger that is deployed
|
||||
if (savedFlow && savedFlow.draft && savedFlow.draft.draft_triggers) {
|
||||
const newSavedDraftTrigers = savedFlow.draft.draft_triggers.filter(
|
||||
(t) => t.id !== id || t.path !== path || t.type !== type
|
||||
)
|
||||
savedFlow.draft.draft_triggers =
|
||||
newSavedDraftTrigers.length > 0 ? newSavedDraftTrigers : undefined
|
||||
}
|
||||
}
|
||||
|
||||
let flowPreviewButtons: FlowPreviewButtons
|
||||
</script>
|
||||
|
||||
@@ -1263,6 +1248,16 @@
|
||||
currentValue={$flowStore}
|
||||
/>
|
||||
|
||||
<DraftTriggersConfirmationModal
|
||||
bind:open={draftTriggersModalOpen}
|
||||
draftTriggers={triggersState.triggers.filter((t) => t.draftConfig)}
|
||||
isFlow={true}
|
||||
on:canceled={() => {
|
||||
draftTriggersModalOpen = false
|
||||
}}
|
||||
on:confirmed={handleDraftTriggersConfirmed}
|
||||
/>
|
||||
|
||||
{#key renderCount}
|
||||
{#if !$userStore?.operator}
|
||||
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
|
||||
@@ -1332,7 +1327,9 @@
|
||||
</div>
|
||||
|
||||
<div class="gap-4 flex-row hidden md:flex w-full max-w-md">
|
||||
{#if $primaryScheduleStore != undefined ? $primaryScheduleStore && $primaryScheduleStore?.enabled : $triggersCount?.primary_schedule}
|
||||
{#if triggersState.triggers?.some((t) => t.type === 'schedule')}
|
||||
{@const primaryScheduleIndex = triggersState.triggers.findIndex((t) => t.isPrimary)}
|
||||
{@const scheduleIndex = triggersState.triggers.findIndex((t) => t.type === 'schedule')}
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
@@ -1341,14 +1338,15 @@
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('triggers')
|
||||
selectTrigger('schedules')
|
||||
const selected = primaryScheduleIndex ?? scheduleIndex
|
||||
if (selected) {
|
||||
triggersState.selectedTriggerIndex = selected
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$primaryScheduleStore != undefined
|
||||
? $primaryScheduleStore
|
||||
? $primaryScheduleStore?.cron
|
||||
: ''
|
||||
: $triggersCount?.primary_schedule?.schedule}
|
||||
{triggersState.triggers[primaryScheduleIndex]?.draftConfig?.schedule ??
|
||||
triggersState.triggers[primaryScheduleIndex]?.lightConfig?.schedule ??
|
||||
''}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -1409,12 +1407,16 @@
|
||||
|
||||
await syncWithDeployed()
|
||||
|
||||
const currentDraftTriggers = structuredClone(
|
||||
triggersState.getDraftTriggersSnapshot()
|
||||
)
|
||||
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'normal',
|
||||
deployed: deployedValue ?? savedFlow,
|
||||
draft: savedFlow['draft'],
|
||||
current: { ...$flowStore, path: $pathStore }
|
||||
draft: savedFlow?.draft,
|
||||
current: { ...$flowStore, path: $pathStore, draft_triggers: currentDraftTriggers }
|
||||
})
|
||||
}}
|
||||
disabled={!savedFlow}
|
||||
@@ -1437,7 +1439,7 @@
|
||||
<FlowPreviewButtons
|
||||
on:openTriggers={(e) => {
|
||||
select('triggers')
|
||||
selectTrigger(e.detail.kind)
|
||||
handleSelectTriggerFromKind(triggersState, triggersCount, initialPath, e.detail.kind)
|
||||
captureOn.set(true)
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
@@ -1489,6 +1491,7 @@
|
||||
flowPreviewButtons?.openPreview(true)
|
||||
}}
|
||||
{savedFlow}
|
||||
onDeployTrigger={handleDeployTrigger}
|
||||
/>
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
|
||||
@@ -7,13 +7,15 @@
|
||||
export let disabled = false
|
||||
export let headless = false
|
||||
export let required = false
|
||||
export let headerClass = ''
|
||||
</script>
|
||||
|
||||
<div class={twMerge(disabled ? 'opacity-60 pointer-events-none' : '', $$props.class)}>
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
{#if !headless}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span class="{primary ? 'text-primary' : 'text-secondary'} text-sm leading-6"
|
||||
<div class={twMerge('flex flex-row items-center gap-2', headerClass)}>
|
||||
<span
|
||||
class="{primary ? 'text-primary' : 'text-secondary'} text-sm leading-6 whitespace-nowrap"
|
||||
>{label}
|
||||
{#if required}
|
||||
<Required required={true} />
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import { random_adj } from './random_positive_adjetive'
|
||||
import { Eye, Folder, Loader2, Plus, SearchCode, User } from 'lucide-svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { tick } from 'svelte'
|
||||
|
||||
type PathKind =
|
||||
| 'resource'
|
||||
@@ -64,6 +65,7 @@
|
||||
export let dirty = false
|
||||
export let kind: PathKind
|
||||
export let hideUser: boolean = false
|
||||
export let disableEditing = false
|
||||
|
||||
let inputP: HTMLInputElement | undefined = undefined
|
||||
|
||||
@@ -300,7 +302,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
function initPath() {
|
||||
async function initPath() {
|
||||
await tick()
|
||||
if (path != undefined && path != '') {
|
||||
meta = pathToMeta(path, hideUser)
|
||||
onMetaChange()
|
||||
@@ -425,11 +428,12 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={disabled || disableEditing}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
icon={User}
|
||||
{disabled}
|
||||
disabled={disabled || disableEditing}
|
||||
light
|
||||
size="xs"
|
||||
value="user"
|
||||
@@ -440,7 +444,7 @@
|
||||
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
|
||||
<ToggleButton
|
||||
icon={Folder}
|
||||
{disabled}
|
||||
disabled={disabled || disableEditing}
|
||||
light
|
||||
size="xs"
|
||||
value="folder"
|
||||
@@ -462,14 +466,20 @@
|
||||
type="text"
|
||||
bind:value={meta.owner}
|
||||
placeholder={$userStore?.username ?? ''}
|
||||
disabled={disabled || !($superadmin || ($userStore?.is_admin ?? false))}
|
||||
disabled={disabled ||
|
||||
!($superadmin || ($userStore?.is_admin ?? false)) ||
|
||||
disableEditing}
|
||||
on:keydown={setDirty}
|
||||
/>
|
||||
</label>
|
||||
{:else if meta.ownerKind === 'folder'}
|
||||
<label class="block grow w-48">
|
||||
<div class="flex flex-row items-center gap-1 w-full">
|
||||
<select class="grow w-full" {disabled} bind:value={meta.owner}>
|
||||
<select
|
||||
class="grow w-full"
|
||||
disabled={disabled || disableEditing}
|
||||
bind:value={meta.owner}
|
||||
>
|
||||
{#if folders?.length == 0}
|
||||
<option disabled>No folders</option>
|
||||
{/if}
|
||||
@@ -488,17 +498,19 @@
|
||||
iconOnly
|
||||
startIcon={{ icon: Eye }}
|
||||
/>
|
||||
<Button
|
||||
title="New folder"
|
||||
btnClasses="!p-1.5"
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
{disabled}
|
||||
on:click={newFolder.openDrawer}
|
||||
iconOnly
|
||||
startIcon={{ icon: Plus }}
|
||||
/>
|
||||
{#if !disableEditing}
|
||||
<Button
|
||||
title="New folder"
|
||||
btnClasses="!p-1.5"
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
{disabled}
|
||||
on:click={newFolder.openDrawer}
|
||||
iconOnly
|
||||
startIcon={{ icon: Plus }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
@@ -507,7 +519,7 @@
|
||||
<label class="block grow w-full max-w-md">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
{disabled}
|
||||
disabled={disabled || disableEditing}
|
||||
type="text"
|
||||
id="path"
|
||||
{autofocus}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Button } from './common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { base } from '$lib/base'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { ListOrdered, PenBox } from 'lucide-svelte'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte'
|
||||
import { type Schedule } from '$lib/gen'
|
||||
|
||||
export let schedule: any
|
||||
export let can_write: boolean
|
||||
export let path: string
|
||||
export let isFlow: boolean
|
||||
export let scheduleEditor: ScheduleEditor
|
||||
export let setScheduleEnabled: (path: string, enabled: boolean) => void
|
||||
|
||||
$: schedule = typeof schedule === 'boolean' ? undefined : (schedule as Schedule | undefined)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 grow w-full">
|
||||
<div class="grid grid-cols-3 w-full">
|
||||
<div class="flex justify-start">
|
||||
<Badge color="indigo" small>
|
||||
Primary
|
||||
<Tooltip light>
|
||||
Share the same path as the script or flow it is attached to and its path get renamed
|
||||
whenever the source path is renamed
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
size="9"
|
||||
class="!text-xs !h-6 !text-primary"
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
placeholder="*/30 * * * *"
|
||||
value={schedule?.schedule ?? ''}
|
||||
disabled={true}
|
||||
/>
|
||||
<Toggle
|
||||
checked={schedule?.enabled ?? false}
|
||||
on:change={(e) => {
|
||||
if (can_write) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'On'
|
||||
}}
|
||||
size="xs"
|
||||
textClass="text-primary font-normal text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button size={'xs'} variant="border" color="light" href={`${base}/runs/${path}`}>
|
||||
<span>Runs</span>
|
||||
<ListOrdered size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
|
||||
>
|
||||
<PenBox size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if Object.keys(schedule?.args ?? {}).length > 0}
|
||||
<div class="">
|
||||
<JobArgs args={schedule?.args ?? {}} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-tertiary"> No arguments </div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,245 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from './common'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { ScheduleService, type Schedule } from '$lib/gen'
|
||||
import { Calendar, Trash, Save } from 'lucide-svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
import CronInput from './CronInput.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { loadSchedules, saveSchedule } from './flows/scheduleUtils'
|
||||
import { type Writable, writable } from 'svelte/store'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte'
|
||||
|
||||
export let schema: any
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let can_write: boolean
|
||||
export let newItem: boolean = false
|
||||
|
||||
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
let schedules: Writable<Schedule[] | undefined> = writable(undefined)
|
||||
let initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined> = writable(undefined)
|
||||
|
||||
async function updateSchedules(forceRefresh: boolean) {
|
||||
const loadPrimarySchedule = true
|
||||
loadSchedules(
|
||||
forceRefresh,
|
||||
path,
|
||||
isFlow,
|
||||
schedules,
|
||||
primarySchedule,
|
||||
initialPrimarySchedule,
|
||||
$workspaceStore ?? '',
|
||||
triggersCount,
|
||||
loadPrimarySchedule
|
||||
)
|
||||
}
|
||||
|
||||
$: updateSchedules(false) || path
|
||||
|
||||
async function save() {
|
||||
await saveSchedule(path, newItem, $workspaceStore ?? '', primarySchedule, isFlow)
|
||||
updateSchedules(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/scheduling">
|
||||
Run scripts and flows automatically on a recurring basis using cron expressions. Each script or
|
||||
flow can have multiple schedules, with one designated as primary.
|
||||
</Description>
|
||||
<ScheduleEditor
|
||||
on:update={() => {
|
||||
updateSchedules(true)
|
||||
}}
|
||||
bind:this={scheduleEditor}
|
||||
/>
|
||||
|
||||
{#if $primarySchedule == undefined}
|
||||
<Skeleton layout={[[12]]} />
|
||||
{:else if $primarySchedule}
|
||||
<div class="w-full flex flex-col mb-4">
|
||||
{#if can_write}
|
||||
<div class="w-full flex-row-reverse flex mb-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<Button
|
||||
on:click={() => {
|
||||
$primarySchedule = false
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount?.schedule_count ?? 1) - 1,
|
||||
primary_schedule: undefined
|
||||
}
|
||||
}}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Trash }}
|
||||
/>
|
||||
{#if initialPrimarySchedule && !newItem}
|
||||
<Toggle
|
||||
disabled={emptyString($primarySchedule.cron)}
|
||||
bind:checked={$primarySchedule.enabled}
|
||||
options={{
|
||||
right: 'Enabled'
|
||||
}}
|
||||
on:change={async (e) => {
|
||||
if (!newItem && $initialPrimarySchedule != false) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
path: path,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${path}`)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !newItem}
|
||||
<Button
|
||||
on:click={save}
|
||||
color="dark"
|
||||
size="sm"
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={JSON.stringify({ ...$primarySchedule, enabled: true }) ==
|
||||
JSON.stringify({ ...initialPrimarySchedule, enabled: true })}
|
||||
>Apply changes now</Button
|
||||
>
|
||||
{:else}
|
||||
<div class="text-sm text-secondary mt-1 text-center"
|
||||
>Deployed automatically with {isFlow ? 'flow' : 'script'}</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<div class="mt-5">
|
||||
<Label label="Summary" class="font-semibold" primary>
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
class="text-sm w-full"
|
||||
bind:value={$primarySchedule.summary}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<CronInput bind:schedule={$primarySchedule.cron} bind:timezone={$primarySchedule.timezone} />
|
||||
<SchemaForm onlyMaskPassword {schema} bind:args={$primarySchedule.args} />
|
||||
{#if emptyString($primarySchedule.cron)}
|
||||
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
|
||||
{/if}
|
||||
|
||||
{#if $initialPrimarySchedule != false && !newItem}
|
||||
<div class="flex">
|
||||
<Button size="xs" color="light" on:click={() => scheduleEditor?.openEdit(path, isFlow)}
|
||||
>Advanced</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row gap-4 mt-2">
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
on:click={() => {
|
||||
$primarySchedule = {
|
||||
summary: '',
|
||||
args: {},
|
||||
cron: '0 0 */1 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
}
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount?.schedule_count ?? 0) + 1,
|
||||
primary_schedule: { schedule: $primarySchedule.cron }
|
||||
}
|
||||
}}
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size="sm"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
Set primary schedule
|
||||
</Button>
|
||||
</div>
|
||||
{#if $initialPrimarySchedule != undefined && $initialPrimarySchedule != false && !newItem}
|
||||
<Button on:click={save} color="dark" size="md" startIcon={{ icon: Save }}>
|
||||
Apply changes now
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="text-sm text-center text-secondary mt-2"
|
||||
>Deployed automatically with {isFlow ? 'flow' : 'script'}</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Label label="Summary" class="font-semibold" primary>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
class="text-sm w-full"
|
||||
/>
|
||||
</Label>
|
||||
<CronInput schedule={''} disabled timezone={Intl.DateTimeFormat().resolvedOptions().timeZone} />
|
||||
|
||||
<SchemaForm disabled {schema} />
|
||||
{/if}
|
||||
|
||||
{#if !newItem}
|
||||
<div class="mt-10"></div>
|
||||
{#if $primarySchedule}
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(isFlow, path)}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
New schedule
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Label label="Other schedules">
|
||||
{#if $schedules}
|
||||
{#if $schedules.length == 0 || $schedules == undefined}
|
||||
<div class="text-xs text-tertiary"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y">
|
||||
{#each $schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-xs items-center py-2">
|
||||
<div class="col-span-3 truncate">{schedule.path}</div>
|
||||
<div class="col-span-2 flex flex-row gap-4 flex-nowrap">
|
||||
<div>{schedule.schedule}</div>
|
||||
<div>{schedule.enabled ? 'on' : 'off'}</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => scheduleEditor?.openEdit(schedule.path, isFlow)}
|
||||
class="px-2">Edit</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import ScheduleEditorInner from '$lib/components/triggers/schedules/ScheduleEditorInner.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
let scheduleEditor = $state<ScheduleEditorInner | null>(null)
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
defaultValues = undefined,
|
||||
schema,
|
||||
customLabel = undefined,
|
||||
...restProps
|
||||
} = $props()
|
||||
|
||||
function openScheduleEditor(isFlow: boolean, isDraft: boolean) {
|
||||
if (isDraft) {
|
||||
scheduleEditor?.openNew(isFlow, path, defaultValues)
|
||||
} else {
|
||||
scheduleEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
selectedTrigger?.type === 'schedule' &&
|
||||
scheduleEditor &&
|
||||
openScheduleEditor(isFlow, selectedTrigger.isDraft ?? false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<ScheduleEditorInner
|
||||
useDrawer={false}
|
||||
bind:this={scheduleEditor}
|
||||
hideTarget
|
||||
allowDraft
|
||||
hasDraft={!!selectedTrigger.draftConfig}
|
||||
isDraftOnly={selectedTrigger.isDraft}
|
||||
primary={selectedTrigger.isPrimary}
|
||||
draftSchema={schema}
|
||||
{customLabel}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet docDescription()}
|
||||
<div class="flex flex-col gap-2 pb-4">
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/scheduling">
|
||||
Run scripts and flows automatically on a recurring basis using cron expressions.
|
||||
</Description>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ScheduleEditorInner>
|
||||
<!-- hideTarget
|
||||
hidePath
|
||||
{header} -->
|
||||
@@ -4,7 +4,6 @@
|
||||
type NewScript,
|
||||
ScriptService,
|
||||
type NewScriptWithDraft,
|
||||
ScheduleService,
|
||||
type Script,
|
||||
type TriggersCount,
|
||||
PostgresTriggerService,
|
||||
@@ -12,13 +11,18 @@
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { defaultScripts, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
defaultScripts,
|
||||
enterpriseLicense,
|
||||
usedTriggerKinds,
|
||||
userStore,
|
||||
workspaceStore
|
||||
} from '$lib/stores'
|
||||
import {
|
||||
cleanValueProperties,
|
||||
emptySchema,
|
||||
emptyString,
|
||||
encodeState,
|
||||
formatCron,
|
||||
generateRandomString,
|
||||
orderedJsonStringify,
|
||||
replaceFalseWithUndefined,
|
||||
@@ -68,7 +72,7 @@
|
||||
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
|
||||
import TriggersEditor from './triggers/TriggersEditor.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext, TriggerKind } from './triggers'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
import {
|
||||
TS_PREPROCESSOR_MODULE_CODE,
|
||||
TS_PREPROCESSOR_SCRIPT_INTRO,
|
||||
@@ -79,8 +83,17 @@
|
||||
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
|
||||
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
|
||||
import DeployButton from './DeployButton.svelte'
|
||||
import {
|
||||
type NewScriptWithDraftAndDraftTriggers,
|
||||
type Trigger,
|
||||
deployTriggers,
|
||||
filterDraftTriggers,
|
||||
handleSelectTriggerFromKind
|
||||
} from './triggers/utils'
|
||||
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
|
||||
export let script: NewScript
|
||||
export let script: NewScript & { draft_triggers?: Trigger[] }
|
||||
export let fullyLoaded: boolean = true
|
||||
export let initialPath: string = ''
|
||||
export let template: 'docker' | 'bunnative' | 'script' = 'script'
|
||||
@@ -89,7 +102,7 @@
|
||||
export let showMeta: boolean = false
|
||||
export let neverShowMeta: boolean = false
|
||||
export let diffDrawer: DiffDrawer | undefined = undefined
|
||||
export let savedScript: NewScriptWithDraft | undefined = undefined
|
||||
export let savedScript: NewScriptWithDraftAndDraftTriggers | undefined = undefined
|
||||
export let searchParams: URLSearchParams = new URLSearchParams()
|
||||
export let disableHistoryChange = false
|
||||
export let replaceStateFn: (url: string) => void = (url) =>
|
||||
@@ -102,7 +115,10 @@
|
||||
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
|
||||
return {
|
||||
savedValue: savedScript,
|
||||
modifiedValue: script
|
||||
modifiedValue: {
|
||||
...script,
|
||||
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,20 +150,38 @@
|
||||
let scriptEditor: ScriptEditor | undefined = undefined
|
||||
let captureTable: CaptureTable | undefined = undefined
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
|
||||
// Draft triggers confirmation modal
|
||||
let draftTriggersModalOpen = false
|
||||
let confirmDeploymentCallback: (triggersToDeploy: Trigger[]) => void = () => {}
|
||||
|
||||
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
|
||||
const { selectedTriggers } = event.detail
|
||||
// Continue with saving the flow
|
||||
draftTriggersModalOpen = false
|
||||
confirmDeploymentCallback(selectedTriggers)
|
||||
}
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // keep for legacy
|
||||
const triggersCount = writable<TriggersCount | undefined>(
|
||||
savedPrimarySchedule
|
||||
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
|
||||
: undefined
|
||||
)
|
||||
const simplifiedPoll = writable(false)
|
||||
const selectedTriggerStore = writable<TriggerKind>('webhooks')
|
||||
|
||||
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
|
||||
primaryScheduleStore.set(schedule)
|
||||
loadTriggers()
|
||||
}
|
||||
|
||||
export function setDraftTriggers(triggers: Trigger[] | undefined) {
|
||||
triggersState.setTriggers([
|
||||
...(triggers ?? []),
|
||||
...triggersState.triggers.filter((t) => !t.draftConfig)
|
||||
])
|
||||
loadTriggers()
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: initialPath != '' && loadTriggers()
|
||||
@@ -176,29 +210,42 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
if ($primaryScheduleStore && $triggersCount.primary_schedule == undefined) {
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount.schedule_count ?? 0) + 1,
|
||||
primary_schedule: {
|
||||
schedule: $primaryScheduleStore.cron
|
||||
}
|
||||
}
|
||||
|
||||
await triggersState.fetchTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
initialPath,
|
||||
false,
|
||||
$primaryScheduleStore,
|
||||
$userStore
|
||||
)
|
||||
|
||||
if (savedScript && savedScript.draft && savedScript.draft.draft_triggers) {
|
||||
savedScript = filterDraftTriggers(
|
||||
savedScript,
|
||||
triggersState
|
||||
) as NewScriptWithDraftAndDraftTriggers
|
||||
}
|
||||
}
|
||||
|
||||
const triggerDefaultValuesStore = writable<Record<string, any> | undefined>(undefined)
|
||||
// Add triggers context store
|
||||
const triggersState = new Triggers(
|
||||
[
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'email', path: '', isDraft: false },
|
||||
...(script.draft_triggers ?? [])
|
||||
],
|
||||
undefined,
|
||||
saveSessionDraft
|
||||
)
|
||||
|
||||
const captureOn = writable<boolean | undefined>(undefined)
|
||||
const showCaptureHint = writable<boolean | undefined>(undefined)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount,
|
||||
simplifiedPoll,
|
||||
defaultValues: triggerDefaultValuesStore,
|
||||
captureOn: captureOn,
|
||||
showCaptureHint: showCaptureHint
|
||||
showCaptureHint: showCaptureHint,
|
||||
triggersState
|
||||
})
|
||||
|
||||
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql', 'oracledb']
|
||||
@@ -274,8 +321,26 @@
|
||||
})
|
||||
}
|
||||
|
||||
$: !disableHistoryChange &&
|
||||
replaceStateFn('#' + encodeState({ ...script, primarySchedule: $primaryScheduleStore }))
|
||||
$: !disableHistoryChange && encodeScriptState(script)
|
||||
|
||||
function encodeScriptState(script: NewScript) {
|
||||
replaceStateFn(
|
||||
'#' +
|
||||
encodeState({
|
||||
...script,
|
||||
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
function saveSessionDraft() {
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
encodeScriptState(script)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
if (script.content == '') {
|
||||
initContent(script.language, script.kind, template)
|
||||
}
|
||||
@@ -317,31 +382,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
if (!$primaryScheduleStore) {
|
||||
return
|
||||
}
|
||||
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
|
||||
|
||||
try {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: path,
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
script_path: path,
|
||||
is_flow: false,
|
||||
args,
|
||||
enabled,
|
||||
summary
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
sendUserToast(`The primary schedule could not be created: ${err}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditScript(stay: boolean, deployMsg?: string): Promise<void> {
|
||||
// Fetch latest version and fetch entire script after if needed
|
||||
let actual_parent_hash: string | undefined = undefined
|
||||
@@ -419,8 +459,21 @@
|
||||
async function editScript(
|
||||
stay: boolean,
|
||||
parentHash: string,
|
||||
deploymentMsg?: string
|
||||
deploymentMsg?: string,
|
||||
triggersToDeploy?: Trigger[]
|
||||
): Promise<void> {
|
||||
if (!triggersToDeploy) {
|
||||
// Check if there are draft triggers that need confirmation
|
||||
const draftTriggers = triggersState.triggers.filter((trigger) => trigger.draftConfig)
|
||||
if (draftTriggers.length > 0) {
|
||||
draftTriggersModalOpen = true
|
||||
confirmDeploymentCallback = async (triggersToDeploy: Trigger[]) => {
|
||||
await editScript(stay, parentHash, deploymentMsg, triggersToDeploy)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loadingSave = true
|
||||
try {
|
||||
try {
|
||||
@@ -490,55 +543,21 @@
|
||||
})
|
||||
}
|
||||
|
||||
const scheduleExists =
|
||||
initialPath != '' &&
|
||||
(await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path
|
||||
}))
|
||||
if ($primaryScheduleStore) {
|
||||
const { enabled, timezone, args, cron, summary } = $primaryScheduleStore
|
||||
|
||||
if (scheduleExists) {
|
||||
const schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path
|
||||
})
|
||||
if (
|
||||
JSON.stringify(schedule.args) != JSON.stringify(args) ||
|
||||
schedule.schedule != cron ||
|
||||
schedule.timezone != timezone ||
|
||||
schedule.summary != summary
|
||||
) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path,
|
||||
requestBody: {
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
args,
|
||||
summary
|
||||
}
|
||||
})
|
||||
}
|
||||
if (enabled != schedule.enabled) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
}
|
||||
} else if (enabled) {
|
||||
await createSchedule(script.path)
|
||||
}
|
||||
} else if (scheduleExists && !$triggersCount?.primary_schedule) {
|
||||
await ScheduleService.deleteSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path
|
||||
})
|
||||
if (triggersToDeploy) {
|
||||
await deployTriggers(
|
||||
triggersToDeploy,
|
||||
$workspaceStore,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
usedTriggerKinds,
|
||||
script.path,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
savedScript = structuredClone(script) as NewScriptWithDraft
|
||||
const { draft_triggers: _, ...newScript } = structuredClone(script)
|
||||
savedScript = structuredClone(newScript) as NewScriptWithDraft
|
||||
triggersState.setTriggers([])
|
||||
|
||||
if (!disableHistoryChange) {
|
||||
history.replaceState(history.state, '', `/scripts/edit/${script.path}`)
|
||||
}
|
||||
@@ -562,7 +581,8 @@
|
||||
|
||||
if (savedScript) {
|
||||
const draftOrDeployed = cleanValueProperties(savedScript.draft || savedScript)
|
||||
const current = cleanValueProperties(script)
|
||||
const currentTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
const current = cleanValueProperties({ ...script, draft_triggers: currentTriggers })
|
||||
if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
|
||||
sendUserToast('No changes detected, ignoring', false, [
|
||||
{
|
||||
@@ -653,12 +673,16 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
const draftTriggers = triggersState.getDraftTriggersSnapshot()
|
||||
await DraftService.createDraft({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: initialPath == '' || savedScript?.draft_only ? script.path : initialPath,
|
||||
typ: 'script',
|
||||
value: { ...script, primary_schedule: $primaryScheduleStore }
|
||||
value: {
|
||||
...script,
|
||||
draft_triggers: draftTriggers
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -666,8 +690,11 @@
|
||||
...(initialPath == '' || savedScript?.draft_only
|
||||
? { ...structuredClone(script), draft_only: true }
|
||||
: savedScript),
|
||||
draft: structuredClone(script)
|
||||
} as NewScriptWithDraft
|
||||
draft: {
|
||||
...structuredClone(script),
|
||||
draft_triggers: draftTriggers
|
||||
}
|
||||
} as NewScriptWithDraftAndDraftTriggers
|
||||
|
||||
let savedAtNewPath = false
|
||||
if (initialPath == '' || (savedScript?.draft_only && script.path !== initialPath)) {
|
||||
@@ -690,7 +717,7 @@
|
||||
|
||||
function computeDropdownItems(
|
||||
initialPath: string,
|
||||
savedScript: NewScriptWithDraft | undefined,
|
||||
savedScript: NewScriptWithDraftAndDraftTriggers | undefined,
|
||||
diffDrawer: DiffDrawer | undefined
|
||||
) {
|
||||
let dropdownItems: { label: string; onClick: () => void }[] =
|
||||
@@ -718,12 +745,19 @@
|
||||
}
|
||||
await syncWithDeployed()
|
||||
|
||||
const currentDraftTriggers = structuredClone(
|
||||
triggersState.getDraftTriggersSnapshot()
|
||||
)
|
||||
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'normal',
|
||||
deployed: deployedValue ?? savedScript,
|
||||
draft: savedScript['draft'],
|
||||
current: script
|
||||
current: {
|
||||
...script,
|
||||
draft_triggers: currentDraftTriggers
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -799,8 +833,7 @@
|
||||
function openTriggers(ev) {
|
||||
metadataOpen = true
|
||||
selectedTab = 'triggers'
|
||||
selectedTriggerStore.set(ev.detail.kind)
|
||||
triggerDefaultValuesStore.set(ev.detail.config)
|
||||
handleSelectTriggerFromKind(triggersState, triggersCount, initialPath, ev.detail.kind)
|
||||
captureOn.set(true)
|
||||
}
|
||||
|
||||
@@ -824,6 +857,18 @@
|
||||
}
|
||||
selectedInputTab = 'preprocessor'
|
||||
}
|
||||
|
||||
function handleDeployTrigger(trigger: Trigger) {
|
||||
const { id, path, type } = trigger
|
||||
//Update the saved script to remove the draft trigger that is deployed
|
||||
if (savedScript && savedScript.draft && savedScript.draft.draft_triggers) {
|
||||
const newSavedDraftTrigers = savedScript.draft.draft_triggers.filter(
|
||||
(t) => t.id !== id || t.path !== path || t.type !== type
|
||||
)
|
||||
savedScript.draft.draft_triggers =
|
||||
newSavedDraftTrigers.length > 0 ? newSavedDraftTrigers : undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
@@ -837,11 +882,21 @@
|
||||
bind:deployedValue
|
||||
currentValue={script}
|
||||
/>
|
||||
|
||||
<DraftTriggersConfirmationModal
|
||||
bind:open={draftTriggersModalOpen}
|
||||
draftTriggers={triggersState.triggers.filter((t) => t.draftConfig)}
|
||||
on:canceled={() => {
|
||||
draftTriggersModalOpen = false
|
||||
}}
|
||||
on:confirmed={handleDraftTriggersConfirmed}
|
||||
/>
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<Drawer
|
||||
placement="right"
|
||||
bind:open={metadataOpen}
|
||||
size={selectedTab === 'ui' ? '1200px' : '800px'}
|
||||
size={selectedTab === 'ui' || selectedTab === 'triggers' ? '1200px' : '800px'}
|
||||
>
|
||||
<DrawerContent noPadding title="Settings" on:close={() => (metadataOpen = false)}>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
@@ -1459,27 +1514,30 @@
|
||||
customUi={customUi?.settingsPanel?.metadata?.editableSchemaForm}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="triggers">
|
||||
<TabContent value="triggers" class="h-full">
|
||||
<TriggersEditor
|
||||
on:applyArgs={applyArgs}
|
||||
on:addPreprocessor={addPreprocessor}
|
||||
on:exitTriggers={() => {
|
||||
captureTable?.loadCaptures(true)
|
||||
}}
|
||||
args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args}
|
||||
currentPath={script.path}
|
||||
{initialPath}
|
||||
{fakeInitialPath}
|
||||
schema={script.schema}
|
||||
noEditor={true}
|
||||
isFlow={false}
|
||||
currentPath={script.path}
|
||||
hash={script.parent_hash}
|
||||
newItem={initialPath == ''}
|
||||
isFlow={false}
|
||||
{hasPreprocessor}
|
||||
canHavePreprocessor={script.language === 'bun' ||
|
||||
script.language === 'deno' ||
|
||||
script.language === 'python3'}
|
||||
{hasPreprocessor}
|
||||
args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args}
|
||||
isDeployed={savedScript && !savedScript?.draft_only}
|
||||
schema={script.schema}
|
||||
hash={script.parent_hash}
|
||||
onDeployTrigger={handleDeployTrigger}
|
||||
/>
|
||||
|
||||
<!-- <ScriptSchedules {initialPath} schema={script.schema} schedule={scheduleStore} /> -->
|
||||
</TabContent>
|
||||
</div>
|
||||
@@ -1510,7 +1568,10 @@
|
||||
</div>
|
||||
|
||||
<div class="gap-4 flex">
|
||||
{#if $primaryScheduleStore != undefined ? $primaryScheduleStore && $primaryScheduleStore?.enabled : $triggersCount?.primary_schedule}
|
||||
{#if triggersState.triggers?.some((t) => t.type === 'schedule')}
|
||||
{@const primarySchedule = triggersState.triggers.findIndex((t) => t.isPrimary)}
|
||||
{@const schedule = triggersState.triggers.findIndex((t) => t.type === 'schedule')}
|
||||
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
@@ -1520,14 +1581,12 @@
|
||||
on:click={async () => {
|
||||
metadataOpen = true
|
||||
selectedTab = 'triggers'
|
||||
$selectedTriggerStore = 'schedules'
|
||||
triggersState.selectedTriggerIndex = primarySchedule ?? schedule
|
||||
}}
|
||||
>
|
||||
{$primaryScheduleStore != undefined
|
||||
? $primaryScheduleStore
|
||||
? $primaryScheduleStore?.cron
|
||||
: ''
|
||||
: $triggersCount?.primary_schedule?.schedule}
|
||||
{triggersState.triggers[primarySchedule]?.draftConfig?.schedule ??
|
||||
triggersState.triggers[primarySchedule]?.lightConfig?.schedule ??
|
||||
''}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if customUi?.topBar?.path != false}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
{/if}
|
||||
|
||||
{#if disabled}
|
||||
<input type="text" value={scriptPath ?? ''} disabled />
|
||||
<input type="text" value={scriptPath ?? initialPath ?? ''} disabled />
|
||||
{:else}
|
||||
<Select
|
||||
value={items?.find((x) => x.value == initialPath)}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let eeOnly = false
|
||||
export let small: boolean = false
|
||||
export let wrapperClass: string = ''
|
||||
export let headerClass: string = ''
|
||||
|
||||
export let collapsable: boolean = false
|
||||
export let collapsed: boolean = true
|
||||
@@ -26,7 +27,8 @@
|
||||
class={twMerge(
|
||||
'font-semibold flex flex-row items-center gap-1',
|
||||
breakAll ? 'break-all' : '',
|
||||
small ? 'text-sm' : 'text-base'
|
||||
small ? 'text-sm' : 'text-base',
|
||||
headerClass
|
||||
)}
|
||||
>
|
||||
{#if collapsable}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
export let lightMode: boolean = false
|
||||
export let eeOnly: boolean = false
|
||||
|
||||
export let size: 'sm' | 'xs' | '2xs' = 'sm'
|
||||
export let size: 'sm' | 'xs' | '2xs' | '2sm' = 'sm'
|
||||
|
||||
const dispatch = createEventDispatcher<{ change: boolean }>()
|
||||
const bothOptions = Boolean(options.left) && Boolean(options.right)
|
||||
@@ -40,7 +40,7 @@
|
||||
class={twMerge(
|
||||
'mr-2 font-medium duration-50 select-none',
|
||||
bothOptions || textDisabled ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
|
||||
size === 'xs' ? 'text-xs' : size === '2xs' ? 'text-[0.5rem]' : 'text-sm',
|
||||
size === 'xs' || size === '2sm' ? 'text-xs' : size === '2xs' ? 'text-[0.5rem]' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
style={textStyle}
|
||||
@@ -73,13 +73,15 @@
|
||||
color == 'red'
|
||||
? 'peer-checked:bg-red-600'
|
||||
: color == 'blue'
|
||||
? 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500'
|
||||
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-400',
|
||||
? 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500'
|
||||
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-400',
|
||||
size === 'sm'
|
||||
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
|
||||
: size === '2xs'
|
||||
? 'w-5 h-3 after:top-0.5 after:left-[2px] after:h-2 after:w-2'
|
||||
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
|
||||
: size === '2sm'
|
||||
? 'w-9 h-5 after:top-0.5 after:left-[2px] after:h-4 after:w-4'
|
||||
: size === '2xs'
|
||||
? 'w-5 h-3 after:top-0.5 after:left-[2px] after:h-2 after:w-2'
|
||||
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
@@ -88,7 +90,7 @@
|
||||
class={twMerge(
|
||||
'ml-2 font-medium duration-50 select-none',
|
||||
bothOptions || textDisabled ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
size === 'xs' || size === '2sm' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
style={textStyle}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
export let documentationLink: string | undefined = undefined
|
||||
export let markdownTooltip: string | undefined = undefined
|
||||
const plugins = [gfmPlugin()]
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="shadow max-w-sm break-words py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800 whitespace-normal text-left"
|
||||
>
|
||||
{#if markdownTooltip}
|
||||
<div class="prose-sm">
|
||||
<Markdown md={markdownTooltip} {plugins} />
|
||||
</div>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
{#if documentationLink}
|
||||
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
|
||||
<div class="flex flex-row gap-2 mt-4">
|
||||
See documentation
|
||||
<ExternalLink size="16" />
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/common'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
|
||||
interface Props {
|
||||
url?: string
|
||||
disabled?: boolean
|
||||
label?: string
|
||||
}
|
||||
|
||||
let { url = '', disabled = false, label = '' }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex justify-start w-full">
|
||||
<Badge color="gray" class="rounded-r-none h-[27px]">{label}</Badge>
|
||||
|
||||
<ClipboardPanel
|
||||
content={url}
|
||||
class="rounded-l-none bg-surface border-none outline outline-2 outline-surface-secondary outline-offset-[-2px]"
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
@@ -198,6 +198,7 @@
|
||||
this={startIcon.icon}
|
||||
class={twMerge(startIcon?.classes, iconOnlyPadding[size])}
|
||||
size={lucideIconSize}
|
||||
{...startIcon.props}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -246,6 +247,7 @@
|
||||
this={startIcon.icon}
|
||||
class={twMerge(startIcon?.classes, iconOnlyPadding[size])}
|
||||
size={lucideIconSize}
|
||||
{...startIcon.props}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -288,12 +290,13 @@
|
||||
<div
|
||||
class={twMerge(
|
||||
buttonClass,
|
||||
'rounded-md m-0 p-0 !w-10 center-center h-full',
|
||||
'rounded-md m-0 p-0 center-center h-full',
|
||||
variant === 'border' ? 'border-0 border-r border-y ' : 'border-0',
|
||||
'rounded-r-md !rounded-l-none'
|
||||
'rounded-r-md !rounded-l-none',
|
||||
size === 'xs2' ? '!w-8' : '!w-10'
|
||||
)}
|
||||
>
|
||||
<ChevronDown class="w-5 h-5" />
|
||||
<ChevronDown size={lucideIconSize} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Dropdown>
|
||||
|
||||
@@ -10,6 +10,7 @@ export namespace ButtonType {
|
||||
icon?: any | undefined
|
||||
classes?: string
|
||||
faIcon?: any | undefined
|
||||
props?: any
|
||||
}
|
||||
|
||||
export const FontSizeClasses: Record<ButtonType.Size, string> = {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import Button from '../button/Button.svelte'
|
||||
import { AlertTriangle, CornerDownLeft, Loader2, RefreshCcw } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
@@ -12,6 +13,7 @@
|
||||
loading?: boolean
|
||||
open?: boolean
|
||||
type?: 'danger' | 'reload'
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -20,7 +22,8 @@
|
||||
keyListen = true,
|
||||
loading = false,
|
||||
open = false,
|
||||
type: _type
|
||||
type: _type,
|
||||
showIcon = true
|
||||
}: Props = $props()
|
||||
const type = $derived(_type ?? 'danger')
|
||||
|
||||
@@ -92,12 +95,14 @@
|
||||
)}
|
||||
>
|
||||
<div class="flex">
|
||||
<div
|
||||
class={`flex h-12 w-12 items-center justify-center rounded-full ${theme[type].classes.iconWrapper}`}
|
||||
>
|
||||
<Icon class={theme[type].classes.icon} />
|
||||
</div>
|
||||
<div class="ml-4 text-left flex-1">
|
||||
{#if showIcon}
|
||||
<div
|
||||
class={`flex h-12 w-12 items-center justify-center rounded-full ${theme[type].classes.iconWrapper}`}
|
||||
>
|
||||
<Icon class={theme[type].classes.icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class={twMerge('ml-0 text-left flex-1 ', showIcon ? 'ml-4' : '')}>
|
||||
<h3 class="text-lg font-medium text-primary">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
<script lang="ts">
|
||||
import ConfirmationModal from './ConfirmationModal.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { Trigger } from '$lib/components/triggers/utils'
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import TriggerLabel from '$lib/components/triggers/TriggerLabel.svelte'
|
||||
import { triggerIconMap } from '$lib/components/triggers/utils'
|
||||
import { Star } from 'lucide-svelte'
|
||||
import ToggleButtonGroup from '../toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../toggleButton-v2/ToggleButton.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
interface Props {
|
||||
open?: boolean
|
||||
draftTriggers?: Trigger[]
|
||||
isFlow?: boolean
|
||||
}
|
||||
|
||||
let { open = $bindable(false), draftTriggers = [], isFlow = false }: Props = $props()
|
||||
|
||||
let selectedTriggers: Trigger[] = $state(draftTriggers)
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
canceled: void
|
||||
confirmed: { selectedTriggers: Trigger[] }
|
||||
}>()
|
||||
|
||||
function toggleTrigger(trigger: Trigger, selected: 'discard' | 'deploy') {
|
||||
if (selected === 'discard') {
|
||||
if (trigger.isDraft) {
|
||||
selectedTriggers = selectedTriggers.filter((t) => !t.isDraft || t.id !== trigger.id)
|
||||
} else {
|
||||
selectedTriggers = selectedTriggers.filter(
|
||||
(t) => t.isDraft || t.type !== trigger.type || t.path !== trigger.path
|
||||
)
|
||||
}
|
||||
} else if (!isSelected(selectedTriggers, trigger)) {
|
||||
selectedTriggers = [...selectedTriggers, trigger]
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(triggers: Trigger[], trigger: Trigger): boolean {
|
||||
if (trigger.isDraft) {
|
||||
return triggers.some((t) => t.id === trigger.id)
|
||||
} else {
|
||||
return triggers.some((t) => t.path === trigger.path && t.type === trigger.type)
|
||||
}
|
||||
}
|
||||
|
||||
function checkSavePermissions(trigger: Trigger) {
|
||||
// Creating http trigger is forbidden for non-admin users
|
||||
const adminOnly =
|
||||
trigger.type === 'http' &&
|
||||
!$userStore?.is_admin &&
|
||||
!$userStore?.is_super_admin &&
|
||||
trigger.isDraft
|
||||
|
||||
const invalidConfig = !trigger.draftConfig?.canSave
|
||||
|
||||
return invalidConfig ? 'invalid-config' : adminOnly ? 'admin-only' : 'deploy'
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
open &&
|
||||
(selectedTriggers = [...draftTriggers].filter((t) => checkSavePermissions(t) === 'deploy'))
|
||||
})
|
||||
</script>
|
||||
|
||||
<ConfirmationModal
|
||||
{open}
|
||||
title="Draft triggers detected !"
|
||||
confirmationText={isFlow ? 'Deploy Flow' : 'Deploy Script'}
|
||||
type="reload"
|
||||
showIcon={false}
|
||||
on:canceled={() => dispatch('canceled')}
|
||||
on:confirmed={() => dispatch('confirmed', { selectedTriggers })}
|
||||
>
|
||||
<div class="flex flex-col w-full gap-8 pb-4">
|
||||
<div class="text-secondary text-sm">
|
||||
{`${isFlow ? 'Your flow' : 'Your script'} has draft triggers. Select which draft triggers to deploy with the ${isFlow ? 'flow' : 'script'}. Undeployed
|
||||
draft triggers will be permanently deleted.`}
|
||||
</div>
|
||||
|
||||
<div class={draftTriggers.length > 5 ? 'h-[300px]' : ''}>
|
||||
<DataTable size="sm" tableFixed={true}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700 text-secondary dark:text-gray-300 text-xs">
|
||||
<th class="text-left py-2 px-4">Triggers to deploy</th>
|
||||
<th class="w-32 text-center py-2 px-1 justify-center"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each draftTriggers as trigger}
|
||||
{@const SvelteComponent = triggerIconMap[trigger.type]}
|
||||
{@const permission = checkSavePermissions(trigger)}
|
||||
{@const isSelectedTrigger = isSelected(selectedTriggers, trigger)}
|
||||
<tr
|
||||
class={twMerge(
|
||||
'transition-colors h-12 border-t border-gray-200 dark:border-gray-700 whitespace-nowrap',
|
||||
permission === 'deploy' ? 'hover:bg-surface-hover ' : ''
|
||||
)}
|
||||
>
|
||||
<td class={twMerge('text-center py-1 px-4')}>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="relative flex justify-center items-center">
|
||||
<SvelteComponent
|
||||
size={14}
|
||||
class={isSelectedTrigger
|
||||
? 'text-blue-500'
|
||||
: 'text-gray-500 dark:text-gray-400'}
|
||||
/>
|
||||
{#if trigger.isPrimary}
|
||||
<Star size={8} class="absolute -mt-3 ml-3 text-blue-400" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex grow min-w-0 items-center text-left">
|
||||
<TriggerLabel {trigger} discard={!isSelectedTrigger} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-left py-1">
|
||||
{#if permission === 'deploy'}
|
||||
<div class="flex justify-start">
|
||||
<ToggleButtonGroup
|
||||
let:item
|
||||
class="w-fit h-fit"
|
||||
selected={isSelectedTrigger ? 'deploy' : 'discard'}
|
||||
on:selected={(e) => toggleTrigger(trigger, e.detail)}
|
||||
>
|
||||
<ToggleButton
|
||||
label={!trigger.isDraft && trigger.draftConfig ? 'Reset' : 'Discard'}
|
||||
value={'discard'}
|
||||
{item}
|
||||
small
|
||||
class="data-[state=on]:text-white data-[state=on]:bg-red-400 w-[54px] justify-center"
|
||||
/>
|
||||
<ToggleButton
|
||||
label={!trigger.isDraft && trigger.draftConfig ? 'Update' : 'Deploy'}
|
||||
value={'deploy'}
|
||||
{item}
|
||||
small
|
||||
class="data-[state=on]:bg-marine-400 data-[state=on]:text-white data-[state=on]:dark:bg-marine-50 data-[state=on]:dark:text-primary-inverse w-[54px] justify-center"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{:else if permission === 'admin-only'}
|
||||
<div
|
||||
class="text-xs font-semibold px-1.5 py-1.5 bg-red-400 text-white rounded whitespace-nowrap w-[114px] text-center"
|
||||
title="Only admins can deploy http triggers"
|
||||
>
|
||||
Admin only
|
||||
</div>
|
||||
{:else if permission === 'invalid-config'}
|
||||
<div
|
||||
class="text-xs font-semibold px-1.5 py-1.5 bg-red-400 text-white rounded whitespace-nowrap w-[114px] text-center"
|
||||
title="Invalid config"
|
||||
>
|
||||
Invalid config
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
{#if draftTriggers.length === 0}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-6 text-gray-500 dark:text-gray-400 text-sm">
|
||||
No draft triggers found
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
@@ -17,6 +17,7 @@ export { default as Tabs } from './tabs/Tabs.svelte'
|
||||
export { default as Breadcrumb } from './breadcrumb/Breadcrumb.svelte'
|
||||
export { default as FileInput } from './fileInput/FileInput.svelte'
|
||||
export { default as Section } from '../Section.svelte'
|
||||
export { default as Url } from './Url.svelte'
|
||||
|
||||
export * from './alert/model'
|
||||
export * from './badge/model'
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</script>
|
||||
|
||||
{#if menuOpen}
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
<ScheduleEditor onUpdate={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
<FlowHistory bind:this={flowHistory} path={flow.path} />
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</script>
|
||||
|
||||
{#if menuOpen}
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
<ScheduleEditor onUpdate={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
{/if}
|
||||
|
||||
<Row
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
light ? 'font-medium' : '',
|
||||
'data-[state=on]:bg-surface data-[state=on]:shadow-md',
|
||||
'bg-surface-secondary hover:bg-surface-hover',
|
||||
disabled ? '!shadow-none' : '',
|
||||
$$props.class
|
||||
)}
|
||||
use:melt={$item(value)}
|
||||
|
||||
@@ -48,6 +48,6 @@
|
||||
{id}
|
||||
>
|
||||
<div class={twMerge('flex bg-surface-secondary rounded-md p-0.5 gap-1 h-full ', tabListClass)}>
|
||||
<slot {item} />
|
||||
<slot {item} {disabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="grow min-w-0 w-full px-2 py-1 border flex items-center bg-surface-secondary text-primary justify-between rounded-md"
|
||||
class={twMerge(
|
||||
'grow min-w-0 w-full px-2 py-1 border flex items-center bg-surface-secondary text-primary justify-between rounded-md',
|
||||
$$props.class
|
||||
)}
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:cursor-pointer={!disabled}
|
||||
on:click={(e) => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex flex-row flex-1 border p-2 rounded-md overflow-auto relative"
|
||||
class="flex flex-col flex-1 border rounded-md relative"
|
||||
class:cursor-not-allowed={disabled}
|
||||
on:click={(e) => {
|
||||
if (disabled) {
|
||||
@@ -22,6 +22,10 @@
|
||||
copyToClipboard(code)
|
||||
}}
|
||||
>
|
||||
<Highlight {language} {code} class="pointer-events-none" />
|
||||
<Clipboard size={14} class="w-8 top-2 right-2 absolute" />
|
||||
<div class="absolute top-2 right-1 z-10 pointer-events-none">
|
||||
<Clipboard size={14} class="w-8 cursor-pointer pointer-events-auto" />
|
||||
</div>
|
||||
<div class="p-2 overflow-auto w-full">
|
||||
<Highlight {language} {code} class="pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,24 +3,8 @@
|
||||
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import FlowViewerInner from '../FlowViewerInner.svelte'
|
||||
import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte'
|
||||
|
||||
export let triggerSelected:
|
||||
| 'webhooks'
|
||||
| 'emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets'
|
||||
| 'postgres'
|
||||
| 'scheduledPoll'
|
||||
| 'kafka'
|
||||
| 'mqtt'
|
||||
| 'sqs'
|
||||
| 'gcp'
|
||||
| 'nats' = 'webhooks'
|
||||
export let flow_json: any | undefined = undefined
|
||||
export let simplfiedPoll: boolean = false
|
||||
|
||||
export let isOperator: boolean = false
|
||||
|
||||
@@ -52,21 +36,8 @@
|
||||
<TabContent value="script" class="h-full">
|
||||
<slot name="script" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="h-full pt-2">
|
||||
<DetailPageTriggerPanel {simplfiedPoll} bind:triggerSelected>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
<slot slot="postgres" name="postgres" />
|
||||
<slot slot="kafka" name="kafka" />
|
||||
<slot slot="nats" name="nats" />
|
||||
<slot slot="mqtt" name="mqtt" />
|
||||
<slot slot="sqs" name="sqs" />
|
||||
<slot slot="gcp" name="gcp" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
</DetailPageTriggerPanel>
|
||||
<TabContent value="triggers" class="h-full">
|
||||
<slot name="triggers" />
|
||||
</TabContent>
|
||||
{#if flow_json}
|
||||
<TabContent value="raw" class="flex flex-col flex-1 h-full overflow-auto p-2">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import ErrorHandlerToggleButton from './ErrorHandlerToggleButton.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import { Calendar } from 'lucide-svelte'
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
color?: 'red'
|
||||
}
|
||||
|
||||
const { triggersCount, selectedTrigger } = getContext<TriggerContext>('TriggerContext')
|
||||
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
export let mainButtons: MainButton[] = []
|
||||
export let menuItems: MenuItemButton[] = []
|
||||
@@ -52,16 +52,20 @@
|
||||
<Badge>tag: {tag}</Badge>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if $triggersCount?.primary_schedule}
|
||||
{#if triggersState?.triggers?.some((t) => t.isPrimary && !t.isDraft)}
|
||||
{@const primarySchedule = triggersState.triggers.findIndex(
|
||||
(t) => t.isPrimary && !t.isDraft
|
||||
)}
|
||||
<Button
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'schedules'
|
||||
dispatch('triggerDetail')
|
||||
on:click={async () => {
|
||||
dispatch('seeTriggers')
|
||||
await tick()
|
||||
triggersState.selectedTriggerIndex = primarySchedule
|
||||
}}
|
||||
>
|
||||
{$triggersCount?.primary_schedule?.schedule ?? ''}
|
||||
|
||||
@@ -3,48 +3,14 @@
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import DetailPageDetailPanel from './DetailPageDetailPanel.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from '../triggers'
|
||||
import { setContext } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { TriggersCount } from '$lib/gen'
|
||||
import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte'
|
||||
|
||||
export let isOperator: boolean = false
|
||||
export let flow_json: any | undefined = undefined
|
||||
export let selected: string
|
||||
export let triggersCount: Writable<TriggersCount | undefined>
|
||||
|
||||
let mobileTab: 'form' | 'detail' = 'form'
|
||||
|
||||
let clientWidth = window.innerWidth
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const selectedTriggerStore = writable<
|
||||
| 'webhooks'
|
||||
| 'emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets'
|
||||
| 'postgres'
|
||||
| 'scheduledPoll'
|
||||
| 'kafka'
|
||||
| 'nats'
|
||||
| 'mqtt'
|
||||
| 'sqs'
|
||||
| 'gcp'
|
||||
>('webhooks')
|
||||
|
||||
const simplifiedPoll = writable(false)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount,
|
||||
simplifiedPoll,
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined),
|
||||
showCaptureHint: writable(undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<main class="h-screen w-full" bind:clientWidth>
|
||||
@@ -57,28 +23,11 @@
|
||||
<slot name="form" />
|
||||
</Pane>
|
||||
<Pane size={35} minSize={15}>
|
||||
<DetailPageDetailPanel
|
||||
simplfiedPoll={$simplifiedPoll}
|
||||
bind:triggerSelected={$selectedTriggerStore}
|
||||
bind:selected
|
||||
{isOperator}
|
||||
{flow_json}
|
||||
>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
<slot slot="postgres" name="postgres" />
|
||||
<slot slot="kafka" name="kafka" />
|
||||
<slot slot="nats" name="nats" />
|
||||
<slot slot="mqtt" name="mqtt" />
|
||||
<slot slot="sqs" name="sqs" />
|
||||
<slot slot="gcp" name="gcp" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
<DetailPageDetailPanel bind:selected {isOperator} {flow_json}>
|
||||
<slot slot="script" name="script" />
|
||||
<slot slot="save_inputs" name="save_inputs" />
|
||||
<slot slot="flow_step" name="flow_step" />
|
||||
<slot slot="triggers" name="triggers" />
|
||||
</DetailPageDetailPanel>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
@@ -108,25 +57,8 @@
|
||||
<TabContent value="saved_inputs" class="flex flex-col flex-1 h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full">
|
||||
<DetailPageTriggerPanel
|
||||
simplfiedPoll={$simplifiedPoll}
|
||||
bind:triggerSelected={$selectedTriggerStore}
|
||||
>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="script" name="script" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
<slot slot="postgres" name="postgres" />
|
||||
<slot slot="kafka" name="kafka" />
|
||||
<slot slot="nats" name="nats" />
|
||||
<slot slot="mqtt" name="mqtt" />
|
||||
<slot slot="sqs" name="sqs" />
|
||||
<slot slot="gcp" name="gcp" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
</DetailPageTriggerPanel>
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full mt-[-2px]">
|
||||
<slot name="triggers" />
|
||||
</TabContent>
|
||||
<TabContent value="script" class="flex flex-col flex-1 h-full">
|
||||
<slot name="script" />
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { base32 } from 'rfc4648'
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../triggers/CaptureSection.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
interface Props {
|
||||
isFlow?: boolean
|
||||
path: string
|
||||
emailDomain?: string | null
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
hasPreprocessor?: boolean
|
||||
captureLoading?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
isFlow = false,
|
||||
path,
|
||||
emailDomain = null,
|
||||
captureInfo = undefined,
|
||||
hasPreprocessor = false,
|
||||
captureLoading = false
|
||||
}: Props = $props()
|
||||
|
||||
function getCaptureEmail() {
|
||||
const cleanedPath = path.replaceAll('/', '.')
|
||||
const plainPrefix = `capture+${$workspaceStore}+${(isFlow ? 'flow.' : '') + cleanedPath}`
|
||||
const encodedPrefix = base32
|
||||
.stringify(new TextEncoder().encode(plainPrefix), {
|
||||
pad: false
|
||||
})
|
||||
.toLowerCase()
|
||||
return `${encodedPrefix}@${emailDomain}`
|
||||
}
|
||||
|
||||
let captureEmail = $derived(getCaptureEmail())
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="email"
|
||||
disabled={false}
|
||||
{captureInfo}
|
||||
{captureLoading}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Send an email to the test address below to simulate an email trigger.
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to email events on this test address.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
<Label label="Test email address" disabled={!captureInfo.active}>
|
||||
<ClipboardPanel content={captureEmail} disabled={!captureInfo.active} />
|
||||
</Label>
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -10,8 +10,6 @@
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import { base32 } from 'rfc4648'
|
||||
import CaptureSection, { type CaptureInfo } from '../triggers/CaptureSection.svelte'
|
||||
import CaptureTable from '../triggers/CaptureTable.svelte'
|
||||
|
||||
export let token: string = ''
|
||||
export let isFlow: boolean = false
|
||||
@@ -19,25 +17,9 @@
|
||||
export let path: string
|
||||
export let userSettings: any
|
||||
export let emailDomain: string | null = null
|
||||
export let showCapture: boolean = false
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
|
||||
let requestType: 'hash' | 'path' = 'path'
|
||||
|
||||
function getCaptureEmail() {
|
||||
const cleanedPath = path.replaceAll('/', '.')
|
||||
const plainPrefix = `capture+${$workspaceStore}+${(isFlow ? 'flow.' : '') + cleanedPath}`
|
||||
const encodedPrefix = base32
|
||||
.stringify(new TextEncoder().encode(plainPrefix), {
|
||||
pad: false
|
||||
})
|
||||
.toLowerCase()
|
||||
return `${encodedPrefix}@${emailDomain}`
|
||||
}
|
||||
|
||||
$: captureEmail = getCaptureEmail()
|
||||
|
||||
function emailAddress(
|
||||
requestType: 'hash' | 'path',
|
||||
path: string,
|
||||
@@ -63,23 +45,6 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if showCapture && captureInfo}
|
||||
<CaptureSection
|
||||
bind:captureTable
|
||||
captureType="email"
|
||||
disabled={false}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
>
|
||||
<Label label="Email address">
|
||||
<ClipboardPanel content={captureEmail} disabled={!captureInfo.active} />
|
||||
</Label>
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<Label label="Token">
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
import { SettingService } from '$lib/gen'
|
||||
import Skeleton from '../common/skeleton/Skeleton.svelte'
|
||||
import TriggerTokens from '../triggers/TriggerTokens.svelte'
|
||||
import TriggersEditorSection from '../triggers/TriggersEditorSection.svelte'
|
||||
import Description from '../Description.svelte'
|
||||
import Section from '../Section.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import EmailTriggerConfigSection from './EmailTriggerConfigSection.svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let token: string
|
||||
export let scopes: string[] = []
|
||||
export let isFlow: boolean = false
|
||||
export let hash: string | undefined = undefined
|
||||
export let path: string
|
||||
export let isEditor: boolean = false
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
export let newItem: boolean = false
|
||||
|
||||
let emailDomain: string | null = null
|
||||
let triggerTokens: TriggerTokens | undefined = undefined
|
||||
@@ -35,6 +35,8 @@
|
||||
}
|
||||
|
||||
getEmailDomain()
|
||||
|
||||
$: emailDomain && dispatch('email-domain', emailDomain)
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -50,7 +52,7 @@
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<Section label="Email trigger" class="flex flex-col gap-4">
|
||||
<Description link="https://www.windmill.dev/docs/advanced/email_triggers">
|
||||
Email triggers execute scripts and flows when emails are sent to specific addresses. Each
|
||||
trigger has its own unique email address that can be used to invoke the script or flow.
|
||||
@@ -59,23 +61,7 @@
|
||||
<Skeleton layout={[[18]]} />
|
||||
{:else}
|
||||
{#if emailDomain}
|
||||
<TriggersEditorSection
|
||||
cloudDisabled={false}
|
||||
triggerType="email"
|
||||
{isFlow}
|
||||
noSave
|
||||
data={{ emailDomain, userSettings, token, hash, path }}
|
||||
{isEditor}
|
||||
{path}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
alwaysOpened={true}
|
||||
/>
|
||||
<EmailTriggerConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
|
||||
{:else}
|
||||
<div>
|
||||
<Alert title="Email triggers are disabled" size="xs" type="warning">
|
||||
@@ -95,4 +81,4 @@
|
||||
|
||||
<TriggerTokens bind:this={triggerTokens} {isFlow} {path} labelPrefix="email" />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import type { PropPickerContext, FlowPropPickerConfig } from '$lib/components/prop_picker'
|
||||
import type { PickableProperties } from '$lib/components/flows/previousResults'
|
||||
import type { Flow } from '$lib/gen'
|
||||
import type { Trigger } from '$lib/components/triggers/utils'
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
export let loading: boolean
|
||||
@@ -28,6 +29,8 @@
|
||||
draft?: Flow | undefined
|
||||
})
|
||||
| undefined = undefined
|
||||
export let onDeployTrigger: (trigger: Trigger) => void = () => {}
|
||||
|
||||
let size = 50
|
||||
|
||||
const { currentStepStore: copilotCurrentStepStore } =
|
||||
@@ -84,6 +87,7 @@
|
||||
enableAi={!disableAi}
|
||||
on:applyArgs
|
||||
on:testWithArgs
|
||||
{onDeployTrigger}
|
||||
/>
|
||||
{/if}
|
||||
</Pane>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { FlowModuleValue } from '$lib/gen'
|
||||
import FlowCardHeader from './FlowCardHeader.svelte'
|
||||
|
||||
export let title: string | undefined = undefined
|
||||
export let summary: string | undefined = undefined
|
||||
export let noEditor: boolean
|
||||
export let noHeader = false
|
||||
export let flowModuleValue: FlowModuleValue | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
{#if !noEditor}
|
||||
{#if !noEditor && !noHeader}
|
||||
<div>
|
||||
<FlowCardHeader on:setHash on:reload {title} bind:summary {flowModuleValue}>
|
||||
<slot name="header" />
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
import FlowInput from './FlowInput.svelte'
|
||||
import FlowFailureModule from './FlowFailureModule.svelte'
|
||||
import FlowConstants from './FlowConstants.svelte'
|
||||
import TriggersEditor from '../../triggers/TriggersEditor.svelte'
|
||||
import type { FlowModule, Flow } from '$lib/gen'
|
||||
import { initFlowStepWarnings } from '../utils'
|
||||
import { dfs } from '../dfs'
|
||||
import FlowPreprocessorModule from './FlowPreprocessorModule.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { insertNewPreprocessorModule } from '../flowStateUtils'
|
||||
import TriggersEditor from '../../triggers/TriggersEditor.svelte'
|
||||
import { handleSelectTriggerFromKind, type Trigger } from '$lib/components/triggers/utils'
|
||||
|
||||
export let noEditor = false
|
||||
export let enableAi = false
|
||||
@@ -23,7 +24,7 @@
|
||||
draft?: Flow | undefined
|
||||
})
|
||||
| undefined = undefined
|
||||
|
||||
export let onDeployTrigger: (trigger: Trigger) => void = () => {}
|
||||
const {
|
||||
selectedId,
|
||||
flowStore,
|
||||
@@ -36,7 +37,7 @@
|
||||
flowInputEditorState
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { selectedTrigger, defaultValues, captureOn, showCaptureHint } =
|
||||
const { showCaptureHint, triggersState, triggersCount } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
function checkDup(modules: FlowModule[]): string | undefined {
|
||||
let seenModules: string[] = []
|
||||
@@ -82,9 +83,7 @@
|
||||
disabled={disabledFlowInputs}
|
||||
on:openTriggers={(ev) => {
|
||||
$selectedId = 'triggers'
|
||||
selectedTrigger.set(ev.detail.kind)
|
||||
defaultValues.set(ev.detail.config)
|
||||
captureOn.set(true)
|
||||
handleSelectTriggerFromKind(triggersState, triggersCount, savedFlow?.path, ev.detail.kind)
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
on:applyArgs
|
||||
@@ -118,16 +117,18 @@
|
||||
}
|
||||
}}
|
||||
on:testWithArgs
|
||||
args={$previewArgs}
|
||||
currentPath={$pathStore}
|
||||
initialPath={$initialPathStore}
|
||||
{fakeInitialPath}
|
||||
schema={$flowStore.schema}
|
||||
{noEditor}
|
||||
newItem={newFlow}
|
||||
isFlow={true}
|
||||
hasPreprocessor={!!$flowStore.value.preprocessor_module}
|
||||
canHavePreprocessor={true}
|
||||
args={$previewArgs}
|
||||
isDeployed={savedFlow && !savedFlow?.draft_only}
|
||||
schema={$flowStore.schema}
|
||||
{onDeployTrigger}
|
||||
/>
|
||||
{:else if $selectedId.startsWith('subflow:')}
|
||||
<div class="p-4"
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import { initFlowStepWarnings } from '../utils'
|
||||
import { dfs } from '../dfs'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { formatCron } from '$lib/utils'
|
||||
|
||||
export let flowModule: FlowModule
|
||||
export let noEditor: boolean = false
|
||||
@@ -30,7 +31,7 @@
|
||||
const { selectedId, flowStateStore, flowInputsStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { primarySchedule } = getContext<TriggerContext>('TriggerContext')
|
||||
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let scriptKind: 'script' | 'trigger' | 'approval' = 'script'
|
||||
let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = 'script'
|
||||
@@ -42,19 +43,33 @@
|
||||
export let previousModule: FlowModule | undefined = undefined
|
||||
|
||||
function initializePrimaryScheduleForTriggerScript(module: FlowModule) {
|
||||
if (!$primarySchedule) {
|
||||
$primarySchedule = {
|
||||
const primaryIndex = triggersState.triggers.findIndex((t) => t.isPrimary)
|
||||
if (primaryIndex === -1) {
|
||||
const primaryCfg = {
|
||||
summary: 'Scheduled poll of flow',
|
||||
args: {},
|
||||
cron: '0 */15 * * *',
|
||||
schedule: formatCron('0 */15 * * *'),
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
is_flow: true
|
||||
}
|
||||
triggersState.addDraftTrigger(triggersCount, 'schedule', undefined, primaryCfg)
|
||||
} else if (triggersState.triggers[primaryIndex].draftConfig) {
|
||||
//If there is a primary schedule draft update it
|
||||
const newCfg = { ...triggersState.triggers[primaryIndex].draftConfig }
|
||||
let updated = false
|
||||
if (!newCfg.schedule) {
|
||||
newCfg.schedule = formatCron('0 */15 * * *')
|
||||
updated = true
|
||||
}
|
||||
if (!newCfg.enabled) {
|
||||
newCfg.enabled = true
|
||||
updated = true
|
||||
}
|
||||
if (updated) {
|
||||
triggersState.triggers[primaryIndex].draftConfig = newCfg
|
||||
}
|
||||
}
|
||||
if (!$primarySchedule.cron) {
|
||||
$primarySchedule.cron = '0 */15 * * *'
|
||||
}
|
||||
$primarySchedule.enabled = true
|
||||
|
||||
module.stop_after_if = {
|
||||
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from '$lib/components/triggers'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { Triggers } from '$lib/components/triggers/triggers.svelte'
|
||||
import { FlowService, type Flow, type TriggersCount } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { setContext } from 'svelte'
|
||||
@@ -12,19 +13,12 @@
|
||||
|
||||
let flow: Flow | undefined = undefined
|
||||
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
primarySchedule: primaryScheduleStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false),
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined),
|
||||
showCaptureHint: writable(undefined)
|
||||
showCaptureHint: writable(undefined),
|
||||
triggersState: new Triggers()
|
||||
})
|
||||
|
||||
async function loadFlow(path: string) {
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
flowInputsStore,
|
||||
pathStore
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
const { flowPropPickerConfig } = getContext<PropPickerContext>('PropPickerContext')
|
||||
async function insertNewModuleAtIndex(
|
||||
@@ -433,7 +433,7 @@
|
||||
undefined
|
||||
)
|
||||
setExpr(detail.modules[index + 1], `results.${id}`)
|
||||
setScheduledPollSchedule(primarySchedule, triggersCount)
|
||||
setScheduledPollSchedule(triggersState, triggersCount)
|
||||
}
|
||||
|
||||
if (`flow` in detail) {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Cross } from 'lucide-svelte'
|
||||
import StepGenQuick from '$lib/components/copilot/StepGenQuick.svelte'
|
||||
import FlowInputsQuick from '../content/FlowInputsQuick.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
||||
import ToggleHubWorkspaceQuick from '$lib/components/ToggleHubWorkspaceQuick.svelte'
|
||||
import InsertModuleInner from './InsertModuleInner.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { ComputeConfig } from 'svelte-floating-ui'
|
||||
import TopLevelNode from '../pickers/TopLevelNode.svelte'
|
||||
|
||||
import PopupV2 from '$lib/components/common/popup/PopupV2.svelte'
|
||||
import { flip, offset } from 'svelte-floating-ui/dom'
|
||||
import { SchedulePollIcon } from '$lib/components/icons'
|
||||
@@ -16,13 +12,9 @@
|
||||
// import type { Writable } from 'svelte/store'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
export let stop = false
|
||||
export let index: number = 0
|
||||
export let funcDesc = ''
|
||||
export let modules: FlowModule[] = []
|
||||
export let disableAi = false
|
||||
export let kind: 'script' | 'trigger' | 'preprocessor' | 'failure' = 'script'
|
||||
export let allowTrigger = true
|
||||
export let iconSize = 12
|
||||
|
||||
type Alignment = 'start' | 'end' | 'center'
|
||||
@@ -39,19 +31,8 @@
|
||||
autoUpdate: true
|
||||
}
|
||||
$: !open && (funcDesc = '')
|
||||
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
|
||||
let selectedKind: 'script' | 'trigger' | 'preprocessor' | 'approval' | 'flow' | 'failure' = kind
|
||||
let preFilter: 'all' | 'workspace' | 'hub' = 'all'
|
||||
let loading = false
|
||||
let small = false
|
||||
|
||||
let open = false
|
||||
|
||||
let width = 0
|
||||
let height = 0
|
||||
|
||||
$: displayPath = width > 650 || height > 400
|
||||
|
||||
$: small = kind === 'preprocessor' || kind === 'failure'
|
||||
</script>
|
||||
|
||||
<!-- <Menu transitionDuration={0} pointerDown bind:show={open} noMinW {placement} let:close> -->
|
||||
@@ -93,129 +74,5 @@ shouldUsePortal={true} -->
|
||||
{/if}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
<div
|
||||
id="flow-editor-insert-module"
|
||||
class="flex flex-col h-[400px] {small
|
||||
? 'w-[450px]'
|
||||
: 'w-[650px]'} pt-1 pr-1 pl-1 gap-1.5 resize overflow-auto {small
|
||||
? 'min-w-[450px]'
|
||||
: 'min-w-[650px]'} min-h-[400px]"
|
||||
on:wheel={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
role="none"
|
||||
bind:clientWidth={width}
|
||||
bind:clientHeight={height}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<StepGenQuick
|
||||
on:escape={() => close(null)}
|
||||
{disableAi}
|
||||
on:insert
|
||||
bind:funcDesc
|
||||
{preFilter}
|
||||
{loading}
|
||||
/>
|
||||
{#if selectedKind != 'preprocessor' && selectedKind != 'flow'}
|
||||
<ToggleHubWorkspaceQuick bind:selected={preFilter} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row grow min-h-0">
|
||||
{#if kind === 'script'}
|
||||
<div class="flex-none flex flex-col text-xs text-primary">
|
||||
<TopLevelNode
|
||||
label="Action"
|
||||
selected={selectedKind === 'script'}
|
||||
on:select={() => {
|
||||
selectedKind = 'script'
|
||||
}}
|
||||
/>
|
||||
{#if customUi?.triggers != false && allowTrigger}
|
||||
<TopLevelNode
|
||||
label="Trigger"
|
||||
selected={selectedKind === 'trigger'}
|
||||
on:select={() => {
|
||||
selectedKind = 'trigger'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<TopLevelNode
|
||||
label="Approval/Prompt"
|
||||
selected={selectedKind === 'approval'}
|
||||
on:select={() => {
|
||||
selectedKind = 'approval'
|
||||
}}
|
||||
/>
|
||||
{#if customUi?.flowNode != false}
|
||||
<TopLevelNode
|
||||
label="Flow"
|
||||
selected={selectedKind === 'flow'}
|
||||
on:select={() => {
|
||||
selectedKind = 'flow'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if stop}
|
||||
<TopLevelNode
|
||||
label="End flow"
|
||||
selected={selectedKind === 'script'}
|
||||
on:select={() => {
|
||||
selectedKind = 'script'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<TopLevelNode
|
||||
label="For loop"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'forloop' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="While loop"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'whileloop' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="Branch to one"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'branchone' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="Branch to all"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'branchall' })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<FlowInputsQuick
|
||||
{selectedKind}
|
||||
bind:loading
|
||||
filter={funcDesc}
|
||||
{modules}
|
||||
{index}
|
||||
{disableAi}
|
||||
{funcDesc}
|
||||
{kind}
|
||||
on:close={() => {
|
||||
close(null)
|
||||
}}
|
||||
on:new
|
||||
on:pickScript
|
||||
on:pickFlow
|
||||
{preFilter}
|
||||
{small}
|
||||
{displayPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InsertModuleInner on:close={() => close(null)} on:insert on:new on:pickFlow on:pickScript />
|
||||
</PopupV2>
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import StepGenQuick from '$lib/components/copilot/StepGenQuick.svelte'
|
||||
import FlowInputsQuick from '../content/FlowInputsQuick.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
||||
import ToggleHubWorkspaceQuick from '$lib/components/ToggleHubWorkspaceQuick.svelte'
|
||||
import TopLevelNode from '../pickers/TopLevelNode.svelte'
|
||||
|
||||
// import type { Writable } from 'svelte/store'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
export let stop = false
|
||||
export let index: number = 0
|
||||
export let funcDesc = ''
|
||||
export let modules: FlowModule[] = []
|
||||
export let disableAi = false
|
||||
export let kind: 'script' | 'trigger' | 'preprocessor' | 'failure' = 'script'
|
||||
export let allowTrigger = true
|
||||
|
||||
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
|
||||
let selectedKind: 'script' | 'trigger' | 'preprocessor' | 'approval' | 'flow' | 'failure' = kind
|
||||
let preFilter: 'all' | 'workspace' | 'hub' = 'all'
|
||||
let loading = false
|
||||
let small = false
|
||||
|
||||
let width = 0
|
||||
let height = 0
|
||||
|
||||
$: displayPath = width > 650 || height > 400
|
||||
|
||||
$: small = kind === 'preprocessor' || kind === 'failure'
|
||||
</script>
|
||||
|
||||
<!-- <Menu transitionDuration={0} pointerDown bind:show={open} noMinW {placement} let:close> -->
|
||||
|
||||
<!-- {floatingConfig}
|
||||
floatingClasses="mt-2"
|
||||
containerClasses="border rounded-lg shadow-lg bg-surface"
|
||||
noTransition
|
||||
shouldUsePortal={true} -->
|
||||
|
||||
<div
|
||||
id="flow-editor-insert-module"
|
||||
class="flex flex-col h-[400px] {small
|
||||
? 'w-[450px]'
|
||||
: 'w-[650px]'} pt-1 pr-1 pl-1 gap-1.5 resize overflow-auto {small
|
||||
? 'min-w-[450px]'
|
||||
: 'min-w-[650px]'} min-h-[400px]"
|
||||
on:wheel={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
role="none"
|
||||
bind:clientWidth={width}
|
||||
bind:clientHeight={height}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<StepGenQuick
|
||||
on:escape={() => dispatch('close')}
|
||||
{disableAi}
|
||||
on:insert
|
||||
bind:funcDesc
|
||||
{preFilter}
|
||||
{loading}
|
||||
/>
|
||||
{#if selectedKind != 'preprocessor' && selectedKind != 'flow'}
|
||||
<ToggleHubWorkspaceQuick bind:selected={preFilter} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row grow min-h-0">
|
||||
{#if kind === 'script'}
|
||||
<div class="flex-none flex flex-col text-xs text-primary">
|
||||
<TopLevelNode
|
||||
label="Action"
|
||||
selected={selectedKind === 'script'}
|
||||
on:select={() => {
|
||||
selectedKind = 'script'
|
||||
}}
|
||||
/>
|
||||
{#if customUi?.triggers != false && allowTrigger}
|
||||
<TopLevelNode
|
||||
label="Trigger"
|
||||
selected={selectedKind === 'trigger'}
|
||||
on:select={() => {
|
||||
selectedKind = 'trigger'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<TopLevelNode
|
||||
label="Approval/Prompt"
|
||||
selected={selectedKind === 'approval'}
|
||||
on:select={() => {
|
||||
selectedKind = 'approval'
|
||||
}}
|
||||
/>
|
||||
{#if customUi?.flowNode != false}
|
||||
<TopLevelNode
|
||||
label="Flow"
|
||||
selected={selectedKind === 'flow'}
|
||||
on:select={() => {
|
||||
selectedKind = 'flow'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if stop}
|
||||
<TopLevelNode
|
||||
label="End flow"
|
||||
selected={selectedKind === 'script'}
|
||||
on:select={() => {
|
||||
selectedKind = 'script'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<TopLevelNode
|
||||
label="For loop"
|
||||
on:select={() => {
|
||||
dispatch('close')
|
||||
dispatch('new', { kind: 'forloop' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="While loop"
|
||||
on:select={() => {
|
||||
dispatch('close')
|
||||
dispatch('new', { kind: 'whileloop' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="Branch to one"
|
||||
on:select={() => {
|
||||
dispatch('close')
|
||||
dispatch('new', { kind: 'branchone' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="Branch to all"
|
||||
on:select={() => {
|
||||
dispatch('close')
|
||||
dispatch('new', { kind: 'branchall' })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<FlowInputsQuick
|
||||
{selectedKind}
|
||||
bind:loading
|
||||
filter={funcDesc}
|
||||
{modules}
|
||||
{index}
|
||||
{disableAi}
|
||||
{funcDesc}
|
||||
{kind}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:new
|
||||
on:pickScript
|
||||
on:pickFlow
|
||||
{preFilter}
|
||||
{small}
|
||||
{displayPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ScheduleService, type Schedule, type TriggersCount } from '$lib/gen'
|
||||
import type { ScheduleTrigger } from '../triggers'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store'
|
||||
import { get } from 'svelte/store'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
|
||||
@@ -38,7 +39,8 @@ export async function loadSchedules(
|
||||
initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined>,
|
||||
workspace: string,
|
||||
triggersCount: Writable<TriggersCount | undefined>,
|
||||
loadPrimarySchedule: boolean = false
|
||||
loadPrimarySchedule: boolean = false,
|
||||
isDeployed: Writable<boolean | undefined> = writable(undefined)
|
||||
) {
|
||||
if (!path || path == '') {
|
||||
schedules.set([])
|
||||
@@ -53,6 +55,9 @@ export async function loadSchedules(
|
||||
isFlow
|
||||
})
|
||||
const primary = allSchedules.find((s) => s.path == path)
|
||||
if (primary) {
|
||||
isDeployed.set(true)
|
||||
}
|
||||
let remotePrimarySchedule: ScheduleTrigger | false | undefined = undefined
|
||||
if (loadPrimarySchedule && primary) {
|
||||
remotePrimarySchedule = await loadSchedule(path, workspace)
|
||||
@@ -64,7 +69,7 @@ export async function loadSchedules(
|
||||
cron: primary.schedule,
|
||||
timezone: primary.timezone,
|
||||
enabled: primary.enabled
|
||||
}
|
||||
}
|
||||
: false
|
||||
}
|
||||
primarySchedule.update((ps) => (ps === undefined || forceRefresh ? remotePrimarySchedule : ps))
|
||||
@@ -137,3 +142,58 @@ export async function saveSchedule(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveScheduleFromCfg(
|
||||
scheduleCfg: Record<string, any>,
|
||||
edit: boolean,
|
||||
workspace: string
|
||||
): Promise<boolean> {
|
||||
const requestBody = {
|
||||
schedule: scheduleCfg.schedule,
|
||||
timezone: scheduleCfg.timezone,
|
||||
args: scheduleCfg.args,
|
||||
on_failure: scheduleCfg.on_failure,
|
||||
on_failure_times: scheduleCfg.on_failure_times,
|
||||
on_failure_exact: scheduleCfg.on_failure_exact,
|
||||
on_failure_extra_args: scheduleCfg.on_failure_extra_args,
|
||||
on_recovery: scheduleCfg.on_recovery,
|
||||
on_recovery_times: scheduleCfg.on_recovery_times,
|
||||
on_recovery_extra_args: scheduleCfg.on_recovery_extra_args,
|
||||
on_success: scheduleCfg.on_success,
|
||||
on_success_extra_args: scheduleCfg.on_success_extra_args,
|
||||
ws_error_handler_muted: scheduleCfg.ws_error_handler_muted,
|
||||
retry: scheduleCfg.retry,
|
||||
summary: scheduleCfg.summary,
|
||||
description: scheduleCfg.description,
|
||||
no_flow_overlap: scheduleCfg.no_flow_overlap,
|
||||
tag: scheduleCfg.tag,
|
||||
paused_until: scheduleCfg.paused_until,
|
||||
cron_version: scheduleCfg.cron_version
|
||||
}
|
||||
try {
|
||||
if (edit) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace,
|
||||
path: scheduleCfg.path,
|
||||
requestBody: requestBody
|
||||
})
|
||||
sendUserToast(`Schedule ${scheduleCfg.path} updated`)
|
||||
} else {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace,
|
||||
requestBody: {
|
||||
path: scheduleCfg.path,
|
||||
script_path: scheduleCfg.script_path,
|
||||
is_flow: scheduleCfg.is_flow,
|
||||
...requestBody,
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
sendUserToast(`Schedule ${scheduleCfg.path} created`)
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import TriggersWrapper from '../triggers/TriggersWrapper.svelte'
|
||||
import { type GraphEventHandlers, type SimplifiableFlow } from '../../graphBuilder'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { FlowModule, TriggersCount } from '$lib/gen'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { Maximize2, Minimize2, Calendar } from 'lucide-svelte'
|
||||
import { getStateColor, getStateHoverColor } from '../../util'
|
||||
import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers'
|
||||
import VirtualItemWrapper from '$lib/components/flows/map/VirtualItemWrapper.svelte'
|
||||
import { type Trigger, type TriggerType } from '$lib/components/triggers/utils'
|
||||
import { tick } from 'svelte'
|
||||
|
||||
export let data: {
|
||||
path: string
|
||||
@@ -26,11 +28,24 @@
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
|
||||
const { primarySchedule, triggersCount, selectedTrigger } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
function getScheduleCfg(primary: Trigger | undefined, triggersCount: TriggersCount | undefined) {
|
||||
return primary?.draftConfig
|
||||
? {
|
||||
enabled: primary?.draftConfig?.enabled,
|
||||
schedule: primary?.draftConfig?.schedule
|
||||
}
|
||||
: primary?.lightConfig
|
||||
? { enabled: primary?.lightConfig?.enabled, schedule: primary?.lightConfig?.schedule }
|
||||
: {
|
||||
enabled: !!triggersCount?.primary_schedule,
|
||||
schedule: triggersCount?.primary_schedule?.schedule
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<NodeWrapper wrapperClass="shadow-md" let:darkMode>
|
||||
<NodeWrapper wrapperClass="shadow-md rounded-sm" let:darkMode>
|
||||
{#if data.simplifiableFlow?.simplifiedFlow != true}
|
||||
<TriggersWrapper
|
||||
disableAi={data.disableAi}
|
||||
@@ -38,6 +53,7 @@
|
||||
path={data.path}
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
bgHoverColor={getStateHoverColor(undefined, darkMode)}
|
||||
showDraft={data.isEditor ?? false}
|
||||
on:new={(e) => {
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
@@ -57,14 +73,24 @@
|
||||
data?.eventHandlers?.simplifyFlow(true)
|
||||
}}
|
||||
on:openScheduledPoll={(e) => {
|
||||
$selectedTrigger = 'scheduledPoll'
|
||||
const primarySchedule = triggersState.triggers.findIndex((t) => t.isPrimary && !t.isDraft)
|
||||
triggersState.selectedTriggerIndex = primarySchedule
|
||||
}}
|
||||
on:select={(e) => {
|
||||
on:select={() => data?.eventHandlers?.select('triggers')}
|
||||
onSelect={async (triggerIndex: number) => {
|
||||
data?.eventHandlers?.select('triggers')
|
||||
await tick()
|
||||
triggersState.selectedTriggerIndex = triggerIndex
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
data.eventHandlers.delete(e, '')
|
||||
}}
|
||||
onAddDraftTrigger={async (type: TriggerType) => {
|
||||
const newTrigger = triggersState.addDraftTrigger(triggersCount, type)
|
||||
data?.eventHandlers?.select('triggers')
|
||||
await tick()
|
||||
triggersState.selectedTriggerIndex = newTrigger
|
||||
}}
|
||||
selected={$selectedId == 'triggers'}
|
||||
newItem={data.newFlow}
|
||||
modules={data.modules}
|
||||
@@ -80,22 +106,23 @@
|
||||
data?.eventHandlers?.select(e.detail)
|
||||
}}
|
||||
>
|
||||
{#if $primarySchedule || ($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule)}
|
||||
{#if triggersState.triggers.some((t) => t.isPrimary) || $triggersCount?.primary_schedule}
|
||||
{@const { enabled, schedule } = getScheduleCfg(
|
||||
triggersState.triggers.find((t) => t.isPrimary),
|
||||
$triggersCount
|
||||
)}
|
||||
<div class="text-2xs text-primary p-2 flex gap-2 items-center">
|
||||
<Calendar size={12} />
|
||||
<div>
|
||||
Schedule every {$primarySchedule?.cron ?? $triggersCount?.primary_schedule?.schedule}
|
||||
{$primarySchedule?.enabled ||
|
||||
($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule)
|
||||
? ''
|
||||
: ' (disabled)'}
|
||||
Schedule every {schedule}
|
||||
{enabled ? '' : ' (disabled)'}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="px-2 py-1 hover:bg-surface-inverse w-full hover:text-primary-inverse"
|
||||
on:click={() => {
|
||||
setScheduledPollSchedule(primarySchedule, triggersCount)
|
||||
setScheduledPollSchedule(triggersState, triggersCount)
|
||||
}}
|
||||
>
|
||||
Set primary schedule
|
||||
|
||||
@@ -1,110 +1,313 @@
|
||||
<script lang="ts">
|
||||
import { Calendar, Mail, Webhook, Unplug, Database, PlugZap } from 'lucide-svelte'
|
||||
import TriggerButton from './TriggerButton.svelte'
|
||||
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import TriggerCount from './TriggerCount.svelte'
|
||||
import { createEventDispatcher, onMount, type ComponentType } from 'svelte'
|
||||
import { Calendar, Mail, Webhook, Unplug, Database, Terminal } from 'lucide-svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { type ComponentType } from 'svelte'
|
||||
import { Route } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { type TriggerContext } from '$lib/components/triggers'
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { MqttIcon, NatsIcon, KafkaIcon, AwsIcon } from '$lib/components/icons'
|
||||
import GoogleCloudIcon from '$lib/components/icons/GoogleCloudIcon.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { MqttIcon, NatsIcon, KafkaIcon, AwsIcon, GoogleCloudIcon } from '$lib/components/icons'
|
||||
import { type Trigger, type TriggerType } from '$lib/components/triggers/utils'
|
||||
import { Menu, Menubar, MeltButton, MenuItem, Tooltip } from '$lib/components/meltComponents'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import SchedulePollIcon from '$lib/components/icons/SchedulePollIcon.svelte'
|
||||
import TriggerLabel from '$lib/components/triggers/TriggerLabel.svelte'
|
||||
|
||||
const { selectedTrigger, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
export let path: string
|
||||
export let newItem: boolean
|
||||
export let isFlow: boolean
|
||||
export let selected: boolean
|
||||
export let showOnlyWithCount: boolean
|
||||
export let triggersToDisplay: (
|
||||
| 'webhooks'
|
||||
| 'schedules'
|
||||
| 'routes'
|
||||
| 'websockets'
|
||||
| 'kafka'
|
||||
| 'nats'
|
||||
| 'mqtt'
|
||||
| 'emails'
|
||||
| 'eventStreams'
|
||||
| 'postgres'
|
||||
| 'sqs'
|
||||
| 'gcp'
|
||||
)[] = showOnlyWithCount
|
||||
? ['webhooks', 'schedules', 'routes', 'websockets', 'kafka', 'nats', 'emails']
|
||||
: ['webhooks', 'schedules', 'routes', 'websockets', 'eventStreams', 'emails']
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
if (!newItem) {
|
||||
loadCount()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadCount() {
|
||||
if (isFlow) {
|
||||
$triggersCount = await FlowService.getTriggersCountOfFlow({
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
})
|
||||
} else {
|
||||
$triggersCount = await ScriptService.getTriggersCountOfScript({
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
})
|
||||
}
|
||||
interface Props {
|
||||
path: string
|
||||
newItem: boolean
|
||||
isFlow: boolean
|
||||
selected: boolean
|
||||
showOnlyWithCount: boolean
|
||||
numberOfTriggers?: number
|
||||
small?: boolean
|
||||
vertical?: boolean
|
||||
limit?: number
|
||||
showDraft?: boolean
|
||||
onSelect?: (triggerIndex: number) => void
|
||||
}
|
||||
|
||||
let {
|
||||
selected,
|
||||
showOnlyWithCount,
|
||||
// @ts-ignore - This is an output-only prop used with bind:
|
||||
numberOfTriggers = $bindable(0),
|
||||
small = true,
|
||||
vertical = false,
|
||||
limit,
|
||||
showDraft = true,
|
||||
onSelect
|
||||
}: Props = $props()
|
||||
|
||||
let menuOpen = $state(false)
|
||||
|
||||
const triggerTypeConfig: {
|
||||
[key: string]: { icon: ComponentType; countKey?: string }
|
||||
[key in TriggerType]: { icon: ComponentType; countKey?: string; disabled?: boolean }
|
||||
} = {
|
||||
webhooks: { icon: Webhook, countKey: 'webhook_count' },
|
||||
schedules: { icon: Calendar, countKey: 'schedule_count' },
|
||||
routes: { icon: Route, countKey: 'http_routes_count' },
|
||||
websockets: { icon: Unplug, countKey: 'websocket_count' },
|
||||
webhook: { icon: Webhook, countKey: 'webhook_count' },
|
||||
schedule: { icon: Calendar, countKey: 'schedule_count' },
|
||||
http: { icon: Route, countKey: 'http_routes_count' },
|
||||
websocket: { icon: Unplug, countKey: 'websocket_count' },
|
||||
postgres: { icon: Database, countKey: 'postgres_count' },
|
||||
kafka: { icon: KafkaIcon, countKey: 'kafka_count' },
|
||||
emails: { icon: Mail, countKey: 'email_count' },
|
||||
nats: { icon: NatsIcon, countKey: 'nats_count' },
|
||||
mqtt: { icon: MqttIcon, countKey: 'mqtt_count' },
|
||||
sqs: { icon: AwsIcon, countKey: 'sqs_count' },
|
||||
gcp: { icon: GoogleCloudIcon, countKey: 'gcp_count' },
|
||||
eventStreams: { icon: PlugZap }
|
||||
kafka: { icon: KafkaIcon, countKey: 'kafka_count', disabled: !$enterpriseLicense },
|
||||
email: { icon: Mail, countKey: 'email_count' },
|
||||
nats: { icon: NatsIcon, countKey: 'nats_count', disabled: !$enterpriseLicense },
|
||||
mqtt: { icon: MqttIcon, countKey: 'mqtt_count', disabled: !$enterpriseLicense },
|
||||
sqs: { icon: AwsIcon, countKey: 'sqs_count', disabled: !$enterpriseLicense },
|
||||
gcp: { icon: GoogleCloudIcon, countKey: 'gcp_count', disabled: !$enterpriseLicense },
|
||||
poll: { icon: SchedulePollIcon },
|
||||
cli: { icon: Terminal }
|
||||
}
|
||||
|
||||
const allTypes = [
|
||||
'webhook',
|
||||
'schedule',
|
||||
'http',
|
||||
'websocket',
|
||||
'postgres',
|
||||
'kafka',
|
||||
'email',
|
||||
'nats',
|
||||
'mqtt',
|
||||
'sqs',
|
||||
'gcp',
|
||||
'poll',
|
||||
'cli'
|
||||
]
|
||||
|
||||
function camelCaseToWords(s: string) {
|
||||
const result = s.replace(/([A-Z])/g, ' $1')
|
||||
return result.charAt(0).toUpperCase() + result.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
const itemClass = twMerge(
|
||||
'text-secondary text-left font-normal w-full block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary flex flex-row items-center flex-nowrap'
|
||||
)
|
||||
|
||||
// Group triggers by their mapped type
|
||||
let triggersGrouped = $derived.by(() => {
|
||||
const triggersWithIndex = triggersState.triggers.map((trigger, index) => ({
|
||||
...trigger,
|
||||
index
|
||||
}))
|
||||
const triggersFiltered = showDraft
|
||||
? triggersWithIndex
|
||||
: triggersWithIndex.filter((trigger) => !trigger.isDraft)
|
||||
return triggersFiltered.reduce(
|
||||
(acc, trigger) => {
|
||||
const configType = trigger.type
|
||||
|
||||
if (!acc[configType]) {
|
||||
acc[configType] = []
|
||||
}
|
||||
acc[configType].push(trigger)
|
||||
return acc
|
||||
},
|
||||
{} as Record<TriggerType, Trigger[]>
|
||||
)
|
||||
})
|
||||
|
||||
const noTriggers = $derived(triggersState.triggers.length === 0)
|
||||
|
||||
// Extract unique trigger types for display, only keep the first
|
||||
let allTriggerTypes = $derived.by(() => {
|
||||
const types = !noTriggers ? (Object.keys(triggersGrouped) as TriggerType[]) : allTypes
|
||||
//filter out types if showOnlyTriggersWithCount is true and there are no triggers for that type
|
||||
return types.filter(
|
||||
(type) =>
|
||||
(!showOnlyTriggersWithCount ||
|
||||
((triggerTypeConfig[type].countKey &&
|
||||
($triggersCount?.[triggerTypeConfig[type].countKey] ?? 0)) ||
|
||||
0) > 0) &&
|
||||
!triggerTypeConfig[type].disabled
|
||||
)
|
||||
})
|
||||
let triggersToDisplay = $derived(limit ? allTriggerTypes.slice(0, limit) : allTriggerTypes)
|
||||
let extraTriggers = $derived(
|
||||
limit && allTriggerTypes.length > limit
|
||||
? allTriggerTypes.slice(limit).flatMap((type) => triggersGrouped[type])
|
||||
: []
|
||||
)
|
||||
// Fallback for when there are no triggers in the store but we have the types
|
||||
let extraTriggersType = $derived(
|
||||
limit && allTriggerTypes.length > limit ? allTriggerTypes.slice(limit) : []
|
||||
)
|
||||
let showOnlyTriggersWithCount = $derived(showOnlyWithCount || triggersState.triggers.length === 0)
|
||||
|
||||
$effect(() => {
|
||||
if (allTriggerTypes) {
|
||||
numberOfTriggers = allTriggerTypes?.length
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each triggersToDisplay as type}
|
||||
{@const { icon, countKey } = triggerTypeConfig[type]}
|
||||
{#if (!showOnlyWithCount || ((countKey && $triggersCount?.[countKey]) || 0) > 0) && !(type === 'gcp' && !$enterpriseLicense) && !(type === 'sqs' && !$enterpriseLicense) && !(type === 'kafka' && !$enterpriseLicense) && !(type === 'nats' && !$enterpriseLicense) && !(type === 'mqtt')}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">{camelCaseToWords(type)}</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = type === 'eventStreams' ? 'kafka' : type
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected &&
|
||||
($selectedTrigger === type ||
|
||||
(type === 'eventStreams' &&
|
||||
($selectedTrigger === 'kafka' ||
|
||||
$selectedTrigger === 'nats' ||
|
||||
$selectedTrigger === 'sqs' ||
|
||||
$selectedTrigger === 'mqtt' ||
|
||||
$selectedTrigger === 'gcp')))}
|
||||
<Menubar
|
||||
class={twMerge(
|
||||
'items-center justify-center',
|
||||
vertical ? 'flex flex-col gap-2' : 'flex flex-row gap-1'
|
||||
)}
|
||||
>
|
||||
{#snippet children({ createMenu })}
|
||||
{#each triggersToDisplay as type}
|
||||
{@const isSelected =
|
||||
selected && triggersState.selectedTrigger && triggersState.selectedTrigger?.type === type}
|
||||
{@const singleItem =
|
||||
type === 'webhook' ||
|
||||
type === 'email' ||
|
||||
type === 'cli' ||
|
||||
(triggersGrouped[type] && triggersGrouped[type].length === 1)}
|
||||
<Tooltip
|
||||
disablePopup={menuOpen}
|
||||
placement={vertical ? 'right' : 'bottom'}
|
||||
on:click={(e) => e.stopPropagation()}
|
||||
>
|
||||
{#if countKey}
|
||||
<TriggerCount count={$triggersCount?.[countKey]} />
|
||||
{#snippet text()}
|
||||
{camelCaseToWords(type)}
|
||||
{/snippet}
|
||||
{#if singleItem}
|
||||
{@render triggerButton({ type, isSelected, singleItem })}
|
||||
{:else}
|
||||
<Menu
|
||||
{createMenu}
|
||||
usePointerDownOutside
|
||||
placement={vertical ? 'right-start' : 'bottom'}
|
||||
menuClass={'max-w-56'}
|
||||
class="h-fit"
|
||||
bind:open={menuOpen}
|
||||
disabled={!triggersGrouped[type]}
|
||||
>
|
||||
{#snippet trigger({ trigger })}
|
||||
{@render triggerButton({
|
||||
type,
|
||||
isSelected,
|
||||
meltElement: trigger
|
||||
})}
|
||||
{/snippet}
|
||||
|
||||
{#snippet children({ item })}
|
||||
{#if triggersGrouped[type] && triggersGrouped[type].length > 0}
|
||||
{#each triggersGrouped[type] as trigger}
|
||||
{@render triggerItem({ triggerIndex: trigger.index, item })}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-xs text-gray-400 p-2">No {camelCaseToWords(type)} triggers</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Menu>
|
||||
{/if}
|
||||
<svelte:component this={icon} size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
{/each}
|
||||
</Tooltip>
|
||||
{/each}
|
||||
{#if extraTriggers.length > 0}
|
||||
<Menu
|
||||
{createMenu}
|
||||
usePointerDownOutside
|
||||
placement="bottom"
|
||||
menuClass={'w-56'}
|
||||
class="h-fit center-center mr-1"
|
||||
bind:open={menuOpen}
|
||||
>
|
||||
{#snippet trigger({ trigger })}
|
||||
<MeltButton
|
||||
class="w-[23px] h-[23px] rounded-md center-center text-[12px] hover:bg-slate-300 transition-all duration-100 font-normal text-secondary hover:text-primary"
|
||||
meltElement={trigger}
|
||||
>
|
||||
+{extraTriggers.length}
|
||||
</MeltButton>
|
||||
{/snippet}
|
||||
|
||||
{#snippet children({ item })}
|
||||
{#if extraTriggers.length > 0}
|
||||
{#each extraTriggers as trigger}
|
||||
{@render triggerItem({ triggerIndex: trigger.index, item })}
|
||||
{/each}
|
||||
{:else if extraTriggersType.length > 0}
|
||||
{#each extraTriggersType as type}
|
||||
{@render simpleTriggerItem({ item, type })}
|
||||
{/each}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Menu>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Menubar>
|
||||
|
||||
{#snippet triggerButton({ type, isSelected, meltElement = undefined, singleItem = false })}
|
||||
{@const { icon: SvelteComponent, countKey } = triggerTypeConfig[type]}
|
||||
|
||||
<MeltButton
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover rounded-md shadow-sm text-xs relative center-center cursor-pointer bg-slate-100 dark:bg-slate-700',
|
||||
'dark:outline dark:outline-1 outline-tertiary/20 group',
|
||||
isSelected ? 'outline-tertiary outline' : '',
|
||||
small ? 'w-[23px] h-[23px] outline-[1.5px]' : 'p-2 outline-[2px]'
|
||||
)}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (singleItem) {
|
||||
onSelect?.(triggersGrouped[type][0].index)
|
||||
}
|
||||
}}
|
||||
{meltElement}
|
||||
>
|
||||
{#if countKey}
|
||||
{@const count = $triggersCount?.[countKey] ?? 0}
|
||||
{#if count > 0}
|
||||
<div
|
||||
class={twMerge(
|
||||
// Base styles that apply in all cases
|
||||
'absolute z-10 rounded-full overflow-hidden',
|
||||
'flex center-center text-primary-inverse font-mono',
|
||||
'bg-tertiary/50 group-hover:bg-primary transition-all duration-[100ms]',
|
||||
noTriggers ? 'bg-primary' : '',
|
||||
|
||||
// Size variants based on small prop
|
||||
small
|
||||
? '-right-[3px] -top-[3px] h-3 w-3 text-[8px]'
|
||||
: '-right-1.5 -top-1.5 h-4 w-4 text-xs',
|
||||
|
||||
// Special case for no triggers
|
||||
noTriggers && small ? 'h-3 w-3 text-[8px] -right-0.5 -top-0.5' : '',
|
||||
noTriggers && !small ? 'h-4 w-4 text-xs -right-1 -top-1' : ''
|
||||
)}
|
||||
>
|
||||
{#if count === undefined}
|
||||
<Loader2 class="animate-spin text-2xs" />
|
||||
{:else}
|
||||
<p>{count}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<SvelteComponent size={small ? 12 : 14} />
|
||||
</MeltButton>
|
||||
{/snippet}
|
||||
|
||||
{#snippet triggerItem({ triggerIndex, item })}
|
||||
<MenuItem
|
||||
{item}
|
||||
class={itemClass}
|
||||
on:click={() => {
|
||||
onSelect?.(triggerIndex)
|
||||
}}
|
||||
>
|
||||
<TriggerLabel trigger={triggersState.triggers[triggerIndex]} />
|
||||
</MenuItem>
|
||||
{/snippet}
|
||||
|
||||
{#snippet simpleTriggerItem({ item, type })}
|
||||
{@const { icon: SvelteComponent, countKey } = triggerTypeConfig[type]}
|
||||
<MenuItem {item} class={itemClass}>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<SvelteComponent size={14} />
|
||||
{camelCaseToWords(type)}
|
||||
{#if countKey}
|
||||
<div class="text-xs text-gray-400">
|
||||
{$triggersCount?.[countKey] ?? 0}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,59 +1,124 @@
|
||||
<script lang="ts">
|
||||
import { NODE } from '../../util'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import type { TriggerType } from '$lib/components/triggers/utils'
|
||||
import TriggersBadge from './TriggersBadge.svelte'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import InsertModuleInner from '$lib/components/flows/map/InsertModuleInner.svelte'
|
||||
import AddTriggersButton from '$lib/components/triggers/AddTriggersButton.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let path: string
|
||||
export let newItem: boolean
|
||||
export let selected: boolean
|
||||
export let isEditor: boolean = false
|
||||
export let disableAi: boolean = false
|
||||
export let modules: FlowModule[] = []
|
||||
export let bgColor: string
|
||||
export let bgHoverColor: string = ''
|
||||
interface Props {
|
||||
path: string
|
||||
newItem: boolean
|
||||
selected: boolean
|
||||
isEditor?: boolean
|
||||
disableAi?: boolean
|
||||
modules?: FlowModule[]
|
||||
bgColor: string
|
||||
bgHoverColor?: string
|
||||
showDraft?: boolean
|
||||
onSelect?: (triggerIndex: number) => void
|
||||
onAddDraftTrigger?: (type: TriggerType) => void
|
||||
}
|
||||
|
||||
let {
|
||||
path,
|
||||
newItem,
|
||||
selected,
|
||||
isEditor = false,
|
||||
disableAi = false,
|
||||
modules = [],
|
||||
bgColor,
|
||||
bgHoverColor = '',
|
||||
showDraft,
|
||||
onSelect,
|
||||
onAddDraftTrigger
|
||||
}: Props = $props()
|
||||
|
||||
let showTriggerScriptPicker = $state(false)
|
||||
let numberOfTriggers = $state(0)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let hover = false
|
||||
let hover = $state(false)
|
||||
let addTriggersButton = $state<AddTriggersButton | undefined>(undefined)
|
||||
</script>
|
||||
|
||||
<div style={`width: ${NODE.width}px;`}>
|
||||
<button
|
||||
style="background-color: {hover && bgHoverColor ? bgHoverColor : bgColor};"
|
||||
class="flex w-full flex-row gap-1 px-2 p-1 items-center {selected
|
||||
? 'outline outline-2 outline-gray-600 rounded-sm dark:bg-white/5 dark:outline-gray-400'
|
||||
class="relative flex w-full flex-row gap-1.5 px-2 p-1 items-center justify-center rounded-sm {selected
|
||||
? 'outline outline-2 outline-gray-600 dark:bg-white/5 dark:outline-gray-400'
|
||||
: ''}"
|
||||
on:pointerdown={() => {
|
||||
onclick={() => {
|
||||
dispatch('select')
|
||||
}}
|
||||
on:mouseenter={() => (hover = true)}
|
||||
on:mouseleave={() => (hover = false)}
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
>
|
||||
<div class="flex flex-col mr-1 ml-1">
|
||||
<div class="flex flex-row items-center text-2xs font-normal"> Triggers </div>
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-row items-center text-2xs font-normal',
|
||||
numberOfTriggers > 6 ? 'absolute left-0 -top-[20px]' : ''
|
||||
)}
|
||||
>
|
||||
Triggers
|
||||
</div>
|
||||
<TriggersBadge showOnlyWithCount={false} {path} {newItem} isFlow {selected} on:select />
|
||||
|
||||
<TriggersBadge
|
||||
showOnlyWithCount={false}
|
||||
{showDraft}
|
||||
{path}
|
||||
{newItem}
|
||||
isFlow
|
||||
{selected}
|
||||
bind:numberOfTriggers
|
||||
limit={isEditor ? 7 : 8}
|
||||
{onSelect}
|
||||
/>
|
||||
|
||||
{#if isEditor}
|
||||
<InsertModuleButton
|
||||
{disableAi}
|
||||
on:new
|
||||
on:pickScript
|
||||
on:select
|
||||
on:open={() => {
|
||||
dispatch('openScheduledPoll')
|
||||
<AddTriggersButton
|
||||
bind:this={addTriggersButton}
|
||||
onAddScheduledPoll={() => {
|
||||
showTriggerScriptPicker = true
|
||||
}}
|
||||
kind="trigger"
|
||||
index={0}
|
||||
{modules}
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover rounded-md border text-xs w-[23px] h-[23px] relative center-center cursor-pointer bg-surface outline-0'
|
||||
)}
|
||||
/>
|
||||
class="w-fit h-fit"
|
||||
triggerScriptPicker={showTriggerScriptPicker ? triggerScriptPicker : undefined}
|
||||
onClose={() => {
|
||||
showTriggerScriptPicker = false
|
||||
}}
|
||||
isEditor
|
||||
{onAddDraftTrigger}
|
||||
>
|
||||
<button
|
||||
class="hover:bg-slate-300 dark:hover:bg-slate-600 rounded-md outline-1 outline-dashed outline-secondary outline-offset-[-1px] text-xs w-[23px] h-[23px] relative center-center cursor-pointer text-secondary"
|
||||
>
|
||||
<Plus size={12} />
|
||||
</button>
|
||||
</AddTriggersButton>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#snippet triggerScriptPicker()}
|
||||
<div class="border rounded-lg shadow-lg bg-surface z5000">
|
||||
<InsertModuleInner
|
||||
{disableAi}
|
||||
on:new
|
||||
on:pickScript
|
||||
on:select
|
||||
on:open={() => {
|
||||
dispatch('openScheduledPoll')
|
||||
}}
|
||||
on:close={() => {
|
||||
addTriggersButton?.close()
|
||||
}}
|
||||
kind="trigger"
|
||||
index={0}
|
||||
{modules}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
export let height = '20px'
|
||||
export let width = '20px'
|
||||
export let size: number = 16
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 -25 256 256"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { type AnyMeltElement } from '@melt-ui/svelte'
|
||||
import { conditionalMelt } from '$lib/utils'
|
||||
|
||||
export let meltElement: AnyMeltElement
|
||||
export let meltElement: AnyMeltElement | undefined = undefined
|
||||
export let type: 'button' | 'submit' | 'reset' | null | undefined = undefined
|
||||
export let title: string = ''
|
||||
export let id: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<button use:meltElement class={$$props.class} {type} {title} {id} {...$meltElement}>
|
||||
<button
|
||||
use:conditionalMelt={meltElement}
|
||||
class={$$props.class}
|
||||
{type}
|
||||
{title}
|
||||
{id}
|
||||
{...$meltElement}
|
||||
on:click
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
export let createMenu: MenubarBuilders['createMenu']
|
||||
export let invisible: boolean = false
|
||||
export let usePointerDownOutside: boolean = false
|
||||
export let menuClass: string = ''
|
||||
export let open = false
|
||||
|
||||
// Use the passed createMenu function
|
||||
const menu = createMenu({
|
||||
@@ -32,8 +34,6 @@
|
||||
states
|
||||
} = menu
|
||||
|
||||
let open = false
|
||||
|
||||
const sync = createSync(states)
|
||||
$: sync.open(open, (v) => (open = Boolean(v)))
|
||||
|
||||
@@ -76,12 +76,13 @@
|
||||
class={twMerge(
|
||||
'z-[6000] border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto',
|
||||
lightMode ? 'bg-surface-inverse' : 'bg-surface',
|
||||
invisible ? 'opacity-0' : ''
|
||||
invisible ? 'opacity-0' : '',
|
||||
menuClass
|
||||
)}
|
||||
on:click
|
||||
>
|
||||
<div class="py-1" style="max-height: {maxHeight}px; ">
|
||||
<slot {item} />
|
||||
<slot {item} {open} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import { ExternalLink, InfoIcon } from 'lucide-svelte'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
import { InfoIcon } from 'lucide-svelte'
|
||||
import { zIndexes } from '$lib/zIndexes'
|
||||
|
||||
import { createTooltip, melt } from '@melt-ui/svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import TooltipInner from '../TooltipInner.svelte'
|
||||
|
||||
export let light = false
|
||||
export let placement: Placement | undefined = 'bottom'
|
||||
@@ -18,8 +17,6 @@
|
||||
export let closeDelay: number = 0
|
||||
export let portal: string | undefined | null = 'body'
|
||||
|
||||
const plugins = [gfmPlugin()]
|
||||
|
||||
const {
|
||||
elements: { trigger, content },
|
||||
states: { open }
|
||||
@@ -49,27 +46,9 @@
|
||||
{/if}
|
||||
|
||||
{#if $open && !disablePopup}
|
||||
<div
|
||||
use:melt={$content}
|
||||
transition:fade={{ duration: 100 }}
|
||||
class="shadow max-w-sm break-words py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800 whitespace-normal text-left"
|
||||
style="z-index: {zIndexes.tooltip}"
|
||||
>
|
||||
{#if markdownTooltip}
|
||||
<div class="prose-sm">
|
||||
<Markdown md={markdownTooltip} {plugins} />
|
||||
</div>
|
||||
{:else}
|
||||
<div use:melt={$content} transition:fade={{ duration: 100 }} style="z-index: {zIndexes.tooltip}">
|
||||
<TooltipInner {documentationLink} {markdownTooltip}>
|
||||
<slot name="text" />
|
||||
{/if}
|
||||
|
||||
{#if documentationLink}
|
||||
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
|
||||
<div class="flex flex-row gap-2 mt-4">
|
||||
See documentation
|
||||
<ExternalLink size="16" />
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</TooltipInner>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</script>
|
||||
|
||||
<Portal name="run-row">
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
<ScheduleEditor onUpdate={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
</Portal>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
let name: string = ''
|
||||
export let customName: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -15,7 +16,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover closeButton={false} class="w-full">
|
||||
<Popover closeButton={false} class="w-full" {disabled}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<slot name="trigger" />
|
||||
</svelte:fragment>
|
||||
@@ -30,6 +31,7 @@
|
||||
close()
|
||||
}
|
||||
}}
|
||||
{disabled}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -40,7 +42,7 @@
|
||||
addField()
|
||||
close()
|
||||
}}
|
||||
disabled={!name}
|
||||
disabled={!name || disabled}
|
||||
shortCut={{ Icon: CornerDownLeft, withoutModifier: true }}
|
||||
>
|
||||
Add {customName ? customName.toLowerCase() : 'field'}
|
||||
|
||||
@@ -76,7 +76,11 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={twMerge('h-full', rounded ? 'rounded-md' : '', noBorder ? 'border-0' : 'border')}
|
||||
class={twMerge(
|
||||
'h-full',
|
||||
rounded ? 'rounded-md overflow-hidden' : '',
|
||||
noBorder ? 'border-0' : 'border'
|
||||
)}
|
||||
bind:clientHeight={tableHeight}
|
||||
>
|
||||
<List justify="between" gap="none" hFull={true}>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CaptureTriggerKind, TriggersCount } from '$lib/gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { type Writable } from 'svelte/store'
|
||||
import { formatCron } from '$lib/utils'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
|
||||
export type ScheduleTrigger = {
|
||||
summary: string | undefined
|
||||
@@ -11,34 +13,32 @@ export type ScheduleTrigger = {
|
||||
}
|
||||
|
||||
export type TriggerContext = {
|
||||
selectedTrigger: Writable<TriggerKind>
|
||||
primarySchedule: Writable<ScheduleTrigger | undefined | false>
|
||||
triggersCount: Writable<TriggersCount | undefined>
|
||||
simplifiedPoll: Writable<boolean | undefined>
|
||||
defaultValues: Writable<Record<string, any> | undefined>
|
||||
captureOn: Writable<boolean | undefined>
|
||||
showCaptureHint: Writable<boolean | undefined>
|
||||
triggersState: Triggers
|
||||
}
|
||||
|
||||
export function setScheduledPollSchedule(
|
||||
primarySchedule: Writable<ScheduleTrigger | undefined | false>,
|
||||
triggersState: Triggers,
|
||||
triggersCount: Writable<TriggersCount | undefined>
|
||||
) {
|
||||
const cron = '0 */5 * * * *'
|
||||
primarySchedule.set({
|
||||
enabled: true,
|
||||
summary: 'Check for new events every 5 minutes',
|
||||
cron: cron,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
args: {}
|
||||
})
|
||||
triggersCount.update((triggersCount) => {
|
||||
return {
|
||||
...(triggersCount ?? {}),
|
||||
schedule_count: (triggersCount?.schedule_count ?? 0) + 1,
|
||||
primary_schedule: { schedule: cron }
|
||||
const primarySchedule = triggersState.triggers.findIndex((t) => t.isPrimary)
|
||||
if (primarySchedule !== -1) {
|
||||
triggersState.selectedTriggerIndex = primarySchedule
|
||||
} else {
|
||||
const draftCfg = {
|
||||
enabled: true,
|
||||
summary: 'Check for new events every 5 minutes',
|
||||
schedule: formatCron('0 */5 * * * *'),
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
args: {},
|
||||
is_flow: true
|
||||
}
|
||||
})
|
||||
|
||||
triggersState.addDraftTrigger(triggersCount, 'schedule', undefined, draftCfg)
|
||||
triggersState.selectedTriggerIndex = triggersState.triggers.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
export type TriggerKind =
|
||||
@@ -74,7 +74,7 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg
|
||||
case 'sqs':
|
||||
return 'sqs'
|
||||
case 'postgres':
|
||||
return 'postgres'
|
||||
return 'postgres'
|
||||
case 'gcp':
|
||||
return 'gcp'
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import { triggerIconMap, type TriggerType } from './utils'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import { SchedulePollIcon } from '$lib/components/icons'
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { CloudOff } from 'lucide-svelte'
|
||||
|
||||
interface Props {
|
||||
setDropdownWidthToButtonWidth?: boolean
|
||||
children?: import('svelte').Snippet
|
||||
triggerScriptPicker?: import('svelte').Snippet | undefined
|
||||
class?: string
|
||||
placement?: Placement
|
||||
isEditor?: boolean
|
||||
onAddDraftTrigger?: (type: TriggerType) => void
|
||||
onAddScheduledPoll?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
setDropdownWidthToButtonWidth = false,
|
||||
children,
|
||||
class: className,
|
||||
triggerScriptPicker,
|
||||
placement = 'bottom',
|
||||
isEditor = false,
|
||||
onAddDraftTrigger,
|
||||
onAddScheduledPoll,
|
||||
onClose
|
||||
}: Props = $props()
|
||||
|
||||
let dropdown: DropdownV2 | undefined
|
||||
|
||||
const cloudHosted = isCloudHosted()
|
||||
|
||||
// Dropdown items for adding new triggers
|
||||
const addTriggerItems: Item[] = [
|
||||
{
|
||||
displayName: 'Schedule',
|
||||
action: () => onAddDraftTrigger?.('schedule'),
|
||||
icon: triggerIconMap.schedule
|
||||
},
|
||||
{ displayName: 'HTTP', action: () => onAddDraftTrigger?.('http'), icon: triggerIconMap.http },
|
||||
{
|
||||
displayName: 'WebSocket',
|
||||
action: () => onAddDraftTrigger?.('websocket'),
|
||||
icon: triggerIconMap.websocket,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'Postgres',
|
||||
action: () => onAddDraftTrigger?.('postgres'),
|
||||
icon: triggerIconMap.postgres,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'Kafka',
|
||||
action: () => onAddDraftTrigger?.('kafka'),
|
||||
icon: triggerIconMap.kafka,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'NATS',
|
||||
action: () => onAddDraftTrigger?.('nats'),
|
||||
icon: triggerIconMap.nats,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'MQTT',
|
||||
action: () => onAddDraftTrigger?.('mqtt'),
|
||||
icon: triggerIconMap.mqtt,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'SQS',
|
||||
action: () => onAddDraftTrigger?.('sqs'),
|
||||
icon: triggerIconMap.sqs,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'GCP Pub/Sub',
|
||||
action: () => onAddDraftTrigger?.('gcp'),
|
||||
icon: triggerIconMap.gcp,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'Scheduled Poll',
|
||||
action: (e) => {
|
||||
e.preventDefault()
|
||||
onAddDraftTrigger?.('poll')
|
||||
onAddScheduledPoll?.()
|
||||
},
|
||||
icon: SchedulePollIcon,
|
||||
hidden: !isEditor
|
||||
}
|
||||
].filter((item) => !item.hidden)
|
||||
|
||||
let triggersButtonWidth = $state(0)
|
||||
|
||||
export function close() {
|
||||
dropdown?.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet extra()}
|
||||
<p
|
||||
class="text-xs text-yellow-700 dark:text-yellow-100/90 bg-yellow-50 dark:bg-yellow-900/40 rounded-md p-1 px-2 -my-1"
|
||||
title="Disabled in multi-tenant cloud"
|
||||
>
|
||||
<CloudOff size={14} />
|
||||
</p>
|
||||
{/snippet}
|
||||
|
||||
<DropdownV2
|
||||
bind:this={dropdown}
|
||||
items={addTriggerItems}
|
||||
{placement}
|
||||
class={className}
|
||||
customWidth={setDropdownWidthToButtonWidth ? triggersButtonWidth : undefined}
|
||||
usePointerDownOutside
|
||||
customMenu={!!triggerScriptPicker}
|
||||
on:close={() => onClose?.()}
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<div class={className} bind:clientWidth={triggersButtonWidth}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet menu()}
|
||||
{@render triggerScriptPicker?.()}
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
@@ -1,4 +1,4 @@
|
||||
<script context="module" lang="ts">
|
||||
<script module lang="ts">
|
||||
export type CaptureInfo = {
|
||||
active: boolean
|
||||
hasPreprocessor: boolean
|
||||
@@ -6,33 +6,70 @@
|
||||
isFlow: boolean
|
||||
path: string
|
||||
connectionInfo: ConnectionInfo | undefined
|
||||
loading?: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition'
|
||||
import { fade } from 'svelte/transition'
|
||||
import AnimatedButton from '../common/button/AnimatedButton.svelte'
|
||||
import PulseButton from '../common/button/PulseButton.svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { CircleStop } from 'lucide-svelte'
|
||||
import { CircleStop, History, Play, Loader2 } from 'lucide-svelte'
|
||||
import ConnectionIndicator, {
|
||||
type ConnectionInfo
|
||||
} from '../common/alert/ConnectionIndicator.svelte'
|
||||
import CaptureTable from './CaptureTable.svelte'
|
||||
import { createEventDispatcher, onDestroy, getContext } from 'svelte'
|
||||
import type { CaptureTriggerKind } from '$lib/gen'
|
||||
import { createEventDispatcher, onDestroy, getContext, onMount } from 'svelte'
|
||||
import type { CaptureTriggerKind, Capture } from '$lib/gen'
|
||||
import CaptureIcon from './CaptureIcon.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
export let disabled: boolean
|
||||
export let captureType: CaptureTriggerKind
|
||||
export let captureInfo: CaptureInfo
|
||||
export let captureTable: CaptureTable | undefined
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { Popover } from '$lib/components/meltComponents'
|
||||
import { CaptureService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { isObject, sendUserToast } from '$lib/utils'
|
||||
import { triggerIconMap } from './utils'
|
||||
import { formatDateShort } from '$lib/utils'
|
||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||
import DisplayResultControlBar from '$lib/components/DisplayResultControlBar.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { FlaskConical } from 'lucide-svelte'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean | undefined
|
||||
captureType: CaptureTriggerKind
|
||||
captureInfo: CaptureInfo
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
captureLoading?: boolean
|
||||
description?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
displayAlert?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
disabled = undefined,
|
||||
captureType,
|
||||
captureInfo,
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
captureLoading = false,
|
||||
description,
|
||||
children,
|
||||
displayAlert = false
|
||||
}: Props = $props()
|
||||
|
||||
const testKind: 'preprocessor' | 'main' = $derived(hasPreprocessor ? 'preprocessor' : 'main')
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
captureToggle: { disableOnly?: boolean }
|
||||
updateSchema: { payloadData: Record<string, any>; redirect: boolean }
|
||||
updateSchema: { payloadData: Record<string, any>; redirect: boolean; args?: boolean }
|
||||
addPreprocessor: null
|
||||
testWithArgs: Record<string, any>
|
||||
applyArgs: { kind: 'main' | 'preprocessor'; args: Record<string, any> }
|
||||
}>()
|
||||
|
||||
const { showCaptureHint } = getContext<TriggerContext>('TriggerContext')
|
||||
@@ -44,16 +81,16 @@
|
||||
disableOnly: true
|
||||
})
|
||||
}
|
||||
stopCaptureListening()
|
||||
})
|
||||
|
||||
function handleUpdateSchema(e: any) {
|
||||
/* function handleUpdateSchema(e: any) {
|
||||
dispatch('updateSchema', {
|
||||
payloadData: e.detail.payloadData,
|
||||
redirect: e.detail.redirect
|
||||
})
|
||||
}
|
||||
} */
|
||||
|
||||
let openingDuration = 400
|
||||
let pulseButton: PulseButton | undefined
|
||||
function updateShowCaptureHint(show: boolean | undefined) {
|
||||
if (show) {
|
||||
@@ -63,77 +100,400 @@
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
$: updateShowCaptureHint($showCaptureHint)
|
||||
$effect(() => {
|
||||
updateShowCaptureHint($showCaptureHint)
|
||||
})
|
||||
|
||||
let selectedCapture: Capture | undefined = $state(undefined)
|
||||
function handleSelectCapture(e: any) {
|
||||
if (e.detail) {
|
||||
selectCapture(e.detail)
|
||||
} else if (lastCapture) {
|
||||
selectCapture(lastCapture)
|
||||
}
|
||||
}
|
||||
|
||||
// New code for capture fetching and management
|
||||
let lastCapture: Capture | undefined = $state(undefined)
|
||||
let newCaptureReceived = $state(false)
|
||||
let isLoadingBigPayload = $state(false)
|
||||
let capturePollingInterval: ReturnType<typeof setInterval> | undefined = undefined
|
||||
let lastCaptureId: number | undefined = undefined
|
||||
let displayResult: DisplayResult | undefined = $state(undefined)
|
||||
let toolbarLocation: 'internal' | 'external' | undefined = $state(undefined)
|
||||
|
||||
function selectCapture(capture: Capture) {
|
||||
selectedCapture = capture
|
||||
if (
|
||||
capture.main_args === 'WINDMILL_TOO_BIG' ||
|
||||
capture.preprocessor_args === 'WINDMILL_TOO_BIG'
|
||||
) {
|
||||
loadBigPayload(capture)
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch the last capture when component mounts
|
||||
async function fetchLastCapture() {
|
||||
try {
|
||||
if (!captureInfo.path) return
|
||||
|
||||
const captures = await CaptureService.listCaptures({
|
||||
workspace: $workspaceStore!,
|
||||
runnableKind: captureInfo.isFlow ? 'flow' : 'script',
|
||||
path: captureInfo.path,
|
||||
triggerKind: captureType,
|
||||
page: 1,
|
||||
perPage: 1
|
||||
})
|
||||
|
||||
if (captures.length > 0) {
|
||||
lastCapture = captures[0]
|
||||
lastCaptureId = lastCapture.id
|
||||
|
||||
selectCapture(lastCapture)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch last capture:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Function to listen for new captures (polls every 100ms)
|
||||
function listenForCaptures() {
|
||||
if (capturePollingInterval) return
|
||||
|
||||
capturePollingInterval = setInterval(async () => {
|
||||
if (!captureInfo.active) return
|
||||
|
||||
try {
|
||||
const captures = await CaptureService.listCaptures({
|
||||
workspace: $workspaceStore!,
|
||||
runnableKind: captureInfo.isFlow ? 'flow' : 'script',
|
||||
path: captureInfo.path,
|
||||
triggerKind: captureType,
|
||||
page: 1,
|
||||
perPage: 1
|
||||
})
|
||||
|
||||
if (captures.length > 0 && lastCaptureId !== captures[0].id) {
|
||||
lastCapture = captures[0]
|
||||
lastCaptureId = lastCapture.id
|
||||
|
||||
// Trigger animation for new capture
|
||||
showNewCaptureAnimation()
|
||||
|
||||
selectCapture(lastCapture)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error polling for new captures:', error)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// Stop listening for captures
|
||||
function stopCaptureListening() {
|
||||
if (capturePollingInterval) {
|
||||
clearInterval(capturePollingInterval)
|
||||
capturePollingInterval = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Show animation when new capture arrives
|
||||
function showNewCaptureAnimation() {
|
||||
newCaptureReceived = true
|
||||
setTimeout(() => {
|
||||
newCaptureReceived = false
|
||||
}, 2000) // Animation duration
|
||||
}
|
||||
|
||||
// Load big payload when requested
|
||||
async function loadBigPayload(capture: Capture | undefined) {
|
||||
if (!capture) return
|
||||
|
||||
try {
|
||||
isLoadingBigPayload = true
|
||||
const fullCapture = await CaptureService.getCapture({
|
||||
workspace: $workspaceStore!,
|
||||
id: capture.id
|
||||
})
|
||||
|
||||
capture.main_args = fullCapture.main_args
|
||||
capture.preprocessor_args = fullCapture.preprocessor_args
|
||||
isLoadingBigPayload = false
|
||||
} catch (error) {
|
||||
sendUserToast('Failed to load large payload', true)
|
||||
isLoadingBigPayload = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCapturePayload(capture: Capture) {
|
||||
let payloadData: any = {}
|
||||
const preprocessor_args = isObject(capture.preprocessor_args) ? capture.preprocessor_args : {}
|
||||
if ('wm_trigger' in preprocessor_args) {
|
||||
// v1
|
||||
payloadData =
|
||||
testKind === 'preprocessor'
|
||||
? {
|
||||
...(typeof capture.main_args === 'object' ? capture.main_args : {}),
|
||||
...preprocessor_args
|
||||
}
|
||||
: capture.main_args
|
||||
} else {
|
||||
// v2
|
||||
payloadData = testKind === 'preprocessor' ? capture.preprocessor_args : capture.main_args
|
||||
}
|
||||
return payloadData
|
||||
}
|
||||
|
||||
// Start or stop capture listening based on active state
|
||||
$effect(() => {
|
||||
if (captureInfo.active) {
|
||||
listenForCaptures()
|
||||
} else {
|
||||
stopCaptureListening()
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch last capture when component mounts
|
||||
onMount(() => {
|
||||
fetchLastCapture()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div transition:slide={{ duration: openingDuration }} class="pb-12 overflow-hidden">
|
||||
<div class="border p-4 rounded-lg">
|
||||
<div class="flex flex-col gap-1 mb-4">
|
||||
<div class="flex flex-row items-center justify-start gap-1">
|
||||
<PulseButton bind:this={pulseButton} numberOfPulses={1} pulseDuration={1}>
|
||||
<AnimatedButton
|
||||
animate={captureInfo.active || captureInfo.loading}
|
||||
baseRadius="6px"
|
||||
wrapperClasses="ml-[-2px]"
|
||||
>
|
||||
<Button
|
||||
size="xs2"
|
||||
on:click={() => dispatch('captureToggle', {})}
|
||||
variant="border"
|
||||
{disabled}
|
||||
color="light"
|
||||
btnClasses={captureInfo.active ? 'text-blue-500 hover:text-blue-500' : ''}
|
||||
<Splitpanes>
|
||||
<Pane size={50} minSize={30}>
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-2 h-full w-full overflow-auto"
|
||||
style="scrollbar-gutter: stable"
|
||||
>
|
||||
<div class="text-sm text-secondary flex items-center gap-1">
|
||||
<FlaskConical size={16} />
|
||||
Test trigger
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 mb-4 w-full">
|
||||
<div class="flex justify-center w-full">
|
||||
<div class="relative h-fit">
|
||||
<AnimatedButton
|
||||
animate={captureInfo.active}
|
||||
wrapperClasses={captureInfo.active ? 'm-[-2px]' : ''}
|
||||
baseRadius="7px"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-1 w-28 justify-center">
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => dispatch('captureToggle', {})}
|
||||
{disabled}
|
||||
color={captureInfo.active ? 'light' : 'dark'}
|
||||
btnClasses={captureInfo.active ? 'text-blue-500' : ''}
|
||||
startIcon={captureInfo.active
|
||||
? { icon: CircleStop }
|
||||
: { icon: CaptureIcon, props: { variant: 'redDot' } }}
|
||||
loading={captureLoading}
|
||||
>
|
||||
{#if captureInfo.active}
|
||||
<CircleStop size={14} />
|
||||
<p class="w-24" transition:fade={{ duration: 300 }}>Stop capturing</p>
|
||||
{:else}
|
||||
<CaptureIcon variant="redDot" size={14} />
|
||||
<p class="w-24" transition:fade={{ duration: 300 }}>Start capturing</p>
|
||||
{/if}
|
||||
{captureInfo.active ? 'Stop' : 'Start capturing'}
|
||||
</div>
|
||||
</Button>
|
||||
</AnimatedButton>
|
||||
</PulseButton>
|
||||
</Button>
|
||||
</AnimatedButton>
|
||||
|
||||
{#if captureInfo.active}
|
||||
<ConnectionIndicator connectionInfo={captureInfo.connectionInfo} />
|
||||
{:else}
|
||||
<Tooltip>
|
||||
Start capturing to test your runnables with real data. Once active, all incoming
|
||||
payloads will be captured and displayed below, allowing you to test your runnables
|
||||
effectively.
|
||||
</Tooltip>
|
||||
<div class="absolute top-1/2 -translate-y-1/2 -right-5">
|
||||
{#if captureInfo.active}
|
||||
<ConnectionIndicator connectionInfo={captureInfo.connectionInfo} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-2">
|
||||
{#if displayAlert}
|
||||
<Alert type="warning" title="Trigger deployed" size="xs" class="mb-4">
|
||||
Capturing will suscribe to the trigger endpoint. Treat carefully.
|
||||
</Alert>
|
||||
{/if}
|
||||
<Description>
|
||||
<div class="relative min-h-8">
|
||||
{#key (captureInfo.active, disabled)}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute top-0 left-0 w-full text-center',
|
||||
disabled === true ? 'text-red-600 dark:text-red-400' : ''
|
||||
)}
|
||||
in:fade={{ duration: 100, delay: 50 }}
|
||||
out:fade={{ duration: 50 }}
|
||||
>
|
||||
{#if disabled === true}
|
||||
Enter a valid configuration to start capturing.
|
||||
{:else}
|
||||
{@render description?.()}
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
</Description>
|
||||
</div>
|
||||
|
||||
{#if children}
|
||||
<div class="grow min-h-0 flex flex-col gap-4">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<Pane minSize={30} class="flex flex-col">
|
||||
<div class="flex flex-row gap-1 justify-between min-h-[33.5px] pl-1 pr-4">
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
{#if lastCapture}
|
||||
<Popover
|
||||
placement="left"
|
||||
contentClasses="w-48 min-h-48 max-h-64 overflow-auto"
|
||||
floatingConfig={{
|
||||
placement: 'left-start',
|
||||
offset: { mainAxis: 8, crossAxis: -4.5 },
|
||||
gutter: 0 // hack to make offset effective, see https://github.com/melt-ui/melt-ui/issues/528
|
||||
}}
|
||||
usePointerDownOutside
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
iconOnly
|
||||
startIcon={{ icon: History }}
|
||||
nonCaptureEvent
|
||||
btnClasses="h-[27px]"
|
||||
></Button>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<CaptureTable
|
||||
{captureType}
|
||||
isFlow={captureInfo.isFlow}
|
||||
path={captureInfo.path}
|
||||
on:selectCapture={handleSelectCapture}
|
||||
fullHeight
|
||||
headless
|
||||
addButton={false}
|
||||
noBorder
|
||||
/>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if selectedCapture}
|
||||
{@const SvelteComponent = triggerIconMap[captureType]}
|
||||
<div
|
||||
class={'min-w-16 text-secondary flex flex-row w-fit items-center gap-2 rounded-md bg-surface-secondary p-1 px-2 h-[27px]'}
|
||||
>
|
||||
<SvelteComponent size={12} />
|
||||
<span class="text-xs text-secondary truncate">
|
||||
Capture {formatDateShort(selectedCapture?.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedCapture}
|
||||
{@const label = isFlow && testKind === 'main' ? 'Test flow with args' : 'Apply args'}
|
||||
{@const title =
|
||||
isFlow && testKind === 'main'
|
||||
? 'Test flow using captured data'
|
||||
: testKind === 'preprocessor'
|
||||
? 'Apply args to preprocessor'
|
||||
: 'Apply args to inputs'}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="dark"
|
||||
btnClasses="h-[27px]"
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Use as input schema',
|
||||
onClick: async () => {
|
||||
if (!lastCapture) return
|
||||
const payloadData = selectedCapture?.main_args
|
||||
dispatch('updateSchema', {
|
||||
payloadData: payloadData ?? {},
|
||||
redirect: true,
|
||||
args: true
|
||||
})
|
||||
},
|
||||
disabled: !selectedCapture,
|
||||
hidden: !isFlow || testKind !== 'main'
|
||||
}
|
||||
].filter((item) => !item.hidden)}
|
||||
on:click={async () => {
|
||||
if (!selectedCapture) return
|
||||
const payloadData = selectedCapture?.main_args ?? {}
|
||||
if (isFlow && testKind === 'main') {
|
||||
dispatch('testWithArgs', payloadData)
|
||||
} else {
|
||||
const trigger_extra = isObject(selectedCapture.preprocessor_args)
|
||||
? selectedCapture.preprocessor_args
|
||||
: {}
|
||||
|
||||
dispatch('applyArgs', {
|
||||
kind: testKind,
|
||||
args: { ...structuredClone(payloadData), ...trigger_extra }
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={testKind === 'preprocessor' && !hasPreprocessor}
|
||||
{title}
|
||||
startIcon={isFlow && testKind === 'main' ? { icon: Play } : {}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if disabled}
|
||||
<div class="text-sm font-normal text-red-600 dark:text-red-400" transition:slide>
|
||||
Enter a valid configuration to start capturing.
|
||||
</div>
|
||||
{#if displayResult && toolbarLocation === 'external'}
|
||||
<DisplayResultControlBar
|
||||
{base}
|
||||
result={selectedCapture?.main_args}
|
||||
disableTooltips={false}
|
||||
on:open-drawer={() => {
|
||||
if (displayResult && typeof displayResult.openDrawer === 'function') {
|
||||
displayResult.openDrawer()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grow min-h-0 rounded-md w-full pl-2 py-1 pb-2 overflow-auto">
|
||||
{#if isLoadingBigPayload}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else if selectedCapture?.main_args}
|
||||
<div class="bg-surface rounded-md text-sm" class:animate-highlight={newCaptureReceived}>
|
||||
<DisplayResult
|
||||
bind:this={displayResult}
|
||||
workspaceId={undefined}
|
||||
jobId={undefined}
|
||||
result={getCapturePayload(selectedCapture)}
|
||||
externalToolbarAvailable
|
||||
on:toolbar-location-changed={({ detail }) => {
|
||||
toolbarLocation = detail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-tertiary p-4 bg-surface rounded-md"
|
||||
>No captures to show yet.</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
{#if $$slots.default}
|
||||
<div class:opacity-50={disabled || !captureInfo.active} class="flex flex-col gap-4 mb-4">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
<style>
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
color: rgba(59, 130, 246, 1);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
100% {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
<CaptureTable
|
||||
bind:this={captureTable}
|
||||
{captureType}
|
||||
hasPreprocessor={captureInfo.hasPreprocessor}
|
||||
canHavePreprocessor={captureInfo.canHavePreprocessor}
|
||||
isFlow={captureInfo.isFlow}
|
||||
path={captureInfo.path}
|
||||
canEdit={true}
|
||||
on:applyArgs
|
||||
on:updateSchema={handleUpdateSchema}
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
fullHeight={false}
|
||||
captureActiveIndicator={captureInfo.active}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
.animate-highlight {
|
||||
animation: highlight 2s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
export let canEdit = false
|
||||
export let fullHeight = true
|
||||
export let limitPayloadSize = false
|
||||
export let noBorder = false
|
||||
export let captureActiveIndicator: boolean | undefined = undefined
|
||||
|
||||
let selected: number | undefined = undefined
|
||||
@@ -59,6 +60,7 @@
|
||||
}
|
||||
select: any
|
||||
testWithArgs: any
|
||||
selectCapture: Capture | undefined
|
||||
}>()
|
||||
|
||||
interface CaptureWithPayload extends Capture {
|
||||
@@ -141,6 +143,7 @@
|
||||
const payloadData = await getPayload(capture)
|
||||
selected = capture.id
|
||||
dispatch('select', structuredClone(payloadData))
|
||||
dispatch('selectCapture', capture)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +274,7 @@
|
||||
on:error={(e) => handleError(e.detail)}
|
||||
on:select={(e) => handleSelect(e.detail)}
|
||||
bind:length={capturesLength}
|
||||
{noBorder}
|
||||
neverShowLoader={captureActiveIndicator !== undefined}
|
||||
>
|
||||
<svelte:fragment slot="columns">
|
||||
@@ -388,7 +392,7 @@
|
||||
</SchemaPickerRow>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="empty">
|
||||
<div class="text-center text-xs text-tertiary my-2">No captures yet</div>
|
||||
<div class="text-center text-xs text-tertiary py-2">No captures yet</div>
|
||||
</svelte:fragment>
|
||||
</InfiniteList>
|
||||
</div>
|
||||
|
||||
@@ -2,36 +2,55 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { CaptureService, type CaptureConfig, type CaptureTriggerKind } from '$lib/gen'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { capitalize, isObject, sendUserToast, sleep } from '$lib/utils'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import RouteEditorConfigSection from './http/RouteEditorConfigSection.svelte'
|
||||
import WebsocketEditorConfigSection from './websocket/WebsocketEditorConfigSection.svelte'
|
||||
import WebhooksConfigSection from './webhook/WebhooksConfigSection.svelte'
|
||||
import EmailTriggerConfigSection from '../details/EmailTriggerConfigSection.svelte'
|
||||
import KafkaTriggersConfigSection from './kafka/KafkaTriggersConfigSection.svelte'
|
||||
import { isObject, 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 CaptureTable from './CaptureTable.svelte'
|
||||
import NatsTriggersConfigSection from './nats/NatsTriggersConfigSection.svelte'
|
||||
import MqttEditorConfigSection from './mqtt/MqttEditorConfigSection.svelte'
|
||||
import SqsTriggerEditorConfigSection from './sqs/SqsTriggerEditorConfigSection.svelte'
|
||||
import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte'
|
||||
import { invalidRelations } from './postgres/utils'
|
||||
import { DEFAULT_V3_CONFIG, DEFAULT_V5_CONFIG } from './mqtt/constant'
|
||||
import GcpTriggerEditorConfigSection from './gcp/GcpTriggerEditorConfigSection.svelte'
|
||||
import WebhooksCapture from './webhook/WebhooksCapture.svelte'
|
||||
import EmailTriggerCaptures from '../details/EmailTriggerCaptures.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 SqsCapture from './sqs/SqsCapture.svelte'
|
||||
import GcpCapture from './gcp/GcpCapture.svelte'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let hasPreprocessor: boolean
|
||||
export let canHavePreprocessor: boolean
|
||||
export let captureType: CaptureTriggerKind = 'webhook'
|
||||
export let showCapture = false
|
||||
export let data: any = {}
|
||||
export let connectionInfo: ConnectionInfo | undefined = undefined
|
||||
export let loading = false
|
||||
export let args: Record<string, any> = {}
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
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 = $bindable({}),
|
||||
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> {
|
||||
if (captureType === 'postgres') {
|
||||
@@ -39,18 +58,18 @@
|
||||
sendUserToast('Table to track must be set', true)
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
invalidRelations(args.publication.table_to_track, {
|
||||
showError: true,
|
||||
trackSchemaTableError: true
|
||||
}) === true
|
||||
}) !== ''
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
loading = true
|
||||
args = await CaptureService.setCaptureConfig({
|
||||
await CaptureService.setCaptureConfig({
|
||||
requestBody: {
|
||||
trigger_kind: captureType,
|
||||
path,
|
||||
@@ -59,22 +78,19 @@
|
||||
},
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
loading = false
|
||||
return true
|
||||
} catch (error) {
|
||||
loading = false
|
||||
sendUserToast(error.body, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let captureActive = false
|
||||
|
||||
let captureConfigs: {
|
||||
[key: string]: CaptureConfig
|
||||
} = {}
|
||||
|
||||
const STREAMING_CAPTURES = ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp']
|
||||
function isStreamingCapture() {
|
||||
if (captureType === 'gcp' && args.delivery_type === 'push') {
|
||||
return false
|
||||
}
|
||||
return ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp'].includes(captureType)
|
||||
}
|
||||
|
||||
async function getCaptureConfigs() {
|
||||
const captureConfigsList = await CaptureService.getCaptureConfigs({
|
||||
@@ -82,10 +98,12 @@
|
||||
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) {
|
||||
@@ -115,30 +133,15 @@
|
||||
}
|
||||
i++
|
||||
await sleep(1000)
|
||||
captureTable?.loadCaptures(true)
|
||||
}
|
||||
}
|
||||
|
||||
let ready = false
|
||||
function setDefaultArgs(captureConfigs: { [key: string]: CaptureConfig }) {
|
||||
if (captureType in captureConfigs) {
|
||||
const triggerConfig = captureConfigs[captureType].trigger_config
|
||||
args = isObject(triggerConfig) ? triggerConfig : {}
|
||||
} else {
|
||||
switch (captureType) {
|
||||
case 'mqtt':
|
||||
//define these field so any reactive statement that may use them will not crash trying to access their property
|
||||
args = {
|
||||
v3_config: DEFAULT_V3_CONFIG,
|
||||
v5_config: DEFAULT_V5_CONFIG,
|
||||
client_version: 'v5',
|
||||
subscribe_topics: []
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
args = {}
|
||||
}
|
||||
args = {}
|
||||
}
|
||||
ready = true
|
||||
}
|
||||
@@ -158,25 +161,19 @@
|
||||
if (captureActive || e.detail.disableOnly) {
|
||||
captureActive = false
|
||||
} else {
|
||||
const configSet = await setConfig()
|
||||
if (configSet) {
|
||||
capture()
|
||||
try {
|
||||
captureLoading = true
|
||||
const configSet = await setConfig()
|
||||
|
||||
if (configSet) {
|
||||
capture()
|
||||
}
|
||||
} finally {
|
||||
captureLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config: CaptureConfig | undefined
|
||||
$: config = captureConfigs[captureType]
|
||||
|
||||
let cloudDisabled = STREAMING_CAPTURES.includes(captureType) && isCloudHosted()
|
||||
|
||||
function isStreamingCapture() {
|
||||
if (captureType === 'gcp' && args.delivery_type === 'push') {
|
||||
return false
|
||||
}
|
||||
return ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp'].includes(captureType)
|
||||
}
|
||||
|
||||
function updateConnectionInfo(config: CaptureConfig | undefined, captureActive: boolean) {
|
||||
if (isStreamingCapture() && config && captureActive) {
|
||||
const serverEnabled = getServerEnabled(config)
|
||||
@@ -192,37 +189,31 @@
|
||||
connectionInfo = undefined
|
||||
}
|
||||
}
|
||||
$: updateConnectionInfo(config, captureActive)
|
||||
$effect(() => {
|
||||
updateConnectionInfo(config, captureActive)
|
||||
})
|
||||
|
||||
let captureInfo: CaptureInfo
|
||||
$: captureInfo = {
|
||||
let captureInfo: CaptureInfo = $derived({
|
||||
active: captureActive,
|
||||
hasPreprocessor,
|
||||
canHavePreprocessor,
|
||||
isFlow,
|
||||
path,
|
||||
connectionInfo,
|
||||
loading: loading
|
||||
}
|
||||
connectionInfo
|
||||
})
|
||||
|
||||
$: args && (captureActive = false)
|
||||
$effect(() => {
|
||||
args && (captureActive = false)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#key ready}
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
{#if cloudDisabled}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
{capitalize(captureType)} triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else if captureType === 'websocket'}
|
||||
<WebsocketEditorConfigSection
|
||||
can_write={true}
|
||||
headless={true}
|
||||
bind:url={args.url}
|
||||
bind:url_runnable_args={args.url_runnable_args}
|
||||
{showCapture}
|
||||
<div class="flex flex-col gap-4 w-full h-full">
|
||||
{#if captureType === 'websocket'}
|
||||
<WebsocketCapture
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{captureLoading}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -230,14 +221,13 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'postgres'}
|
||||
<PostgresEditorConfigSection
|
||||
bind:postgres_resource_path={args.postgres_resource_path}
|
||||
bind:publication={args.publication}
|
||||
{showCapture}
|
||||
<PostgresCapture
|
||||
{captureInfo}
|
||||
can_write={true}
|
||||
headless={true}
|
||||
bind:captureTable
|
||||
{captureLoading}
|
||||
postgres_resource_path={args.postgres_resource_path}
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{triggerDeployed}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -245,16 +235,13 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'webhook'}
|
||||
<WebhooksConfigSection
|
||||
<WebhooksCapture
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{path}
|
||||
hash={data?.hash}
|
||||
token={data?.token}
|
||||
runnableArgs={data?.args}
|
||||
scopes={data?.scopes}
|
||||
{showCapture}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{captureLoading}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -262,18 +249,15 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'http'}
|
||||
<RouteEditorConfigSection
|
||||
{showCapture}
|
||||
can_write={true}
|
||||
<RouteCapture
|
||||
runnableArgs={data?.args}
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
bind:raw_string={args.raw_string}
|
||||
bind:wrap_body={args.wrap_body}
|
||||
capture_mode={true}
|
||||
headless
|
||||
route_path={args.route_path}
|
||||
http_method={args.http_method}
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -281,16 +265,13 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'email'}
|
||||
<EmailTriggerConfigSection
|
||||
hash={data?.hash}
|
||||
token={data?.token}
|
||||
<EmailTriggerCaptures
|
||||
{path}
|
||||
{isFlow}
|
||||
userSettings={data?.userSettings}
|
||||
emailDomain={data?.emailDomain}
|
||||
{showCapture}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{hasPreprocessor}
|
||||
{captureLoading}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -298,14 +279,13 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'kafka'}
|
||||
<KafkaTriggersConfigSection
|
||||
headless={true}
|
||||
{path}
|
||||
bind:args
|
||||
staticInputDisabled={false}
|
||||
{showCapture}
|
||||
<KafkaCapture
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
{triggerDeployed}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -313,32 +293,27 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'nats'}
|
||||
<NatsTriggersConfigSection
|
||||
headless={true}
|
||||
bind:args
|
||||
{path}
|
||||
staticInputDisabled={false}
|
||||
{showCapture}
|
||||
<NatsCapture
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
{triggerDeployed}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'mqtt'}
|
||||
<MqttEditorConfigSection
|
||||
can_write={true}
|
||||
headless={true}
|
||||
{showCapture}
|
||||
<MqttCapture
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
bind:v3_config={args.v3_config}
|
||||
bind:v5_config={args.v5_config}
|
||||
bind:client_version={args.client_version}
|
||||
bind:subscribe_topics={args.subscribe_topics}
|
||||
bind:mqtt_resource_path={args.mqtt_resource_path}
|
||||
bind:client_id={args.client_id}
|
||||
bind:captureTable
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
{triggerDeployed}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -346,16 +321,13 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'sqs'}
|
||||
<SqsTriggerEditorConfigSection
|
||||
can_write={true}
|
||||
headless={true}
|
||||
bind:queue_url={args.queue_url}
|
||||
bind:aws_resource_path={args.aws_resource_path}
|
||||
bind:message_attributes={args.message_attributes}
|
||||
bind:aws_auth_resource_type={args.aws_auth_resource_type}
|
||||
{showCapture}
|
||||
<SqsCapture
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
{triggerDeployed}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
@@ -363,21 +335,14 @@
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'gcp'}
|
||||
<GcpTriggerEditorConfigSection
|
||||
can_write={true}
|
||||
headless={true}
|
||||
bind:gcp_resource_path={args.gcp_resource_path}
|
||||
bind:topic_id={args.topic_id}
|
||||
bind:subscription_id={args.subscription_id}
|
||||
bind:cloud_subscription_id={args.subscription_id}
|
||||
bind:create_update_subscription_id={args.subscription_id}
|
||||
bind:delivery_config={args.delivery_config}
|
||||
bind:delivery_type={args.delivery_type}
|
||||
bind:subscription_mode={args.subscription_mode}
|
||||
bind:base_endpoint={args.base_endpoint}
|
||||
{showCapture}
|
||||
<GcpCapture
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{triggerDeployed}
|
||||
deliveryType={args.delivery_type}
|
||||
{captureLoading}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Trash, Save, RotateCcw } from 'lucide-svelte'
|
||||
import { type Snippet } from 'svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Tooltip } from '../meltComponents'
|
||||
|
||||
interface Props {
|
||||
isDraftOnly: any
|
||||
hasDraft: any
|
||||
saveDisabled: any
|
||||
enabled: boolean | undefined
|
||||
allowDraft: any
|
||||
edit: any
|
||||
isLoading: any
|
||||
permissions: 'write' | 'create' | 'none'
|
||||
isDeployed: boolean
|
||||
extra?: Snippet
|
||||
onDelete?: () => void
|
||||
onReset?: () => void
|
||||
onToggleEnabled?: (enabled: boolean) => void
|
||||
onUpdate?: () => void
|
||||
cloudDisabled?: boolean
|
||||
triggerType?: string
|
||||
}
|
||||
|
||||
let {
|
||||
isDraftOnly,
|
||||
hasDraft,
|
||||
saveDisabled,
|
||||
enabled,
|
||||
allowDraft,
|
||||
edit,
|
||||
isLoading,
|
||||
permissions,
|
||||
isDeployed,
|
||||
extra,
|
||||
onDelete,
|
||||
onReset,
|
||||
onToggleEnabled,
|
||||
onUpdate,
|
||||
cloudDisabled = false
|
||||
}: Props = $props()
|
||||
|
||||
const canSave = $derived((permissions === 'write' && edit) || permissions === 'create')
|
||||
</script>
|
||||
|
||||
{#if !allowDraft}
|
||||
{@render extra?.()}
|
||||
{#if edit && enabled !== undefined}
|
||||
<Toggle
|
||||
size="sm"
|
||||
disabled={permissions === 'none'}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
on:change={({ detail }) => {
|
||||
onToggleEnabled?.(detail)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if canSave}
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={saveDisabled}
|
||||
on:click={() => {
|
||||
onUpdate?.()
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
{#if !isDraftOnly && !hasDraft && enabled !== undefined}
|
||||
<div class="center-center">
|
||||
<Toggle
|
||||
size="2sm"
|
||||
disabled={permissions === 'none'}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
on:change={({ detail }) => {
|
||||
onToggleEnabled?.(detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isDraftOnly}
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: Trash }}
|
||||
iconOnly
|
||||
color={'light'}
|
||||
on:click={() => {
|
||||
onDelete?.()
|
||||
}}
|
||||
btnClasses="hover:bg-red-500 hover:text-white"
|
||||
/>
|
||||
{:else if hasDraft}
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: RotateCcw }}
|
||||
color={'light'}
|
||||
on:click={() => {
|
||||
onReset?.()
|
||||
}}
|
||||
>
|
||||
Reset changes
|
||||
</Button>
|
||||
{/if}
|
||||
{#if canSave && (isDraftOnly || hasDraft)}
|
||||
<Tooltip placement="bottom-end" disablePopup={!saveDisabled && !cloudDisabled && isDeployed}>
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={saveDisabled || cloudDisabled || !isDeployed}
|
||||
on:click={() => {
|
||||
onUpdate?.()
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isDraftOnly ? 'Deploy' : 'Update'}
|
||||
</Button>
|
||||
<span slot="text">
|
||||
{#if !isDeployed}
|
||||
Deploy the runnable to enable trigger creation
|
||||
{:else if cloudDisabled}
|
||||
This trigger is disabled in the multi-tenant cloud
|
||||
{:else}
|
||||
Enter a valid config to {isDraftOnly ? 'deploy' : 'update'} the trigger
|
||||
{/if}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { getTriggerLabel, type Trigger } from './utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
let { trigger, discard }: { trigger: Trigger; discard?: boolean } = $props()
|
||||
|
||||
let label = $derived.by(() => getTriggerLabel(trigger))
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={twMerge(
|
||||
trigger.isDraft ? 'text-frost-400 italic dark:text-frost-200' : 'font-normal',
|
||||
'truncate pr-1',
|
||||
discard ? 'line-through' : ''
|
||||
)}
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{#if trigger.isPrimary}
|
||||
<span
|
||||
class="ml-2 bg-blue-50 dark:bg-blue-900/40 px-1.5 py-0.5 rounded text-xs text-blue-700 dark:text-blue-100 whitespace-nowrap"
|
||||
>
|
||||
Primary
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if trigger.draftConfig && !trigger.isDraft}
|
||||
<span
|
||||
class="ml-2 text-xs bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-100 px-1.5 py-0.5 rounded whitespace-nowrap"
|
||||
>
|
||||
Modified
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if trigger.isDraft}
|
||||
<span
|
||||
class="ml-2 text-xs bg-frost-400 dark:bg-frost-900 text-white px-1.5 py-0.5 rounded whitespace-nowrap"
|
||||
>
|
||||
New
|
||||
</span>
|
||||
{/if}
|
||||
@@ -1,269 +1,348 @@
|
||||
<script lang="ts">
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import { Tabs } from '$lib/components/common'
|
||||
import WebhooksPanel from '$lib/components/triggers/webhook/WebhooksPanel.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
import RoutesPanel from '$lib/components/triggers/http/RoutesPanel.svelte'
|
||||
import RunPageSchedules from '$lib/components/RunPageSchedules.svelte'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import FlowCard from '../flows/common/FlowCard.svelte'
|
||||
import { getContext, onDestroy, createEventDispatcher } from 'svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import ScheduledPollPanel from './scheduled/ScheduledPollPanel.svelte'
|
||||
import WebsocketTriggersPanel from './websocket/WebsocketTriggersPanel.svelte'
|
||||
import PostgresTriggersPanel from './postgres/PostgresTriggersPanel.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { KafkaIcon, MqttIcon, NatsIcon, AwsIcon, GoogleCloudIcon } from '../icons'
|
||||
import KafkaTriggersPanel from './kafka/KafkaTriggersPanel.svelte'
|
||||
import NatsTriggersPanel from './nats/NatsTriggersPanel.svelte'
|
||||
import MqttTriggersPanel from './mqtt/MqttTriggersPanel.svelte'
|
||||
import SqsTriggerPanel from './sqs/SqsTriggerPanel.svelte'
|
||||
import GcpTriggerPanel from './gcp/GcpTriggerPanel.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import TriggersTable from './TriggersTable.svelte'
|
||||
import CaptureWrapper from './CaptureWrapper.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import TriggersBadge from '../graph/renderers/triggers/TriggersBadge.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import AddTriggersButton from '$lib/components/triggers/AddTriggersButton.svelte'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import TriggersWrapperV2 from './TriggersWrapper.svelte'
|
||||
import {
|
||||
triggerTypeToCaptureKind,
|
||||
type TriggerType,
|
||||
CLOUD_DISABLED_TRIGGER_TYPES,
|
||||
type Trigger
|
||||
} from './utils'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
|
||||
export let noEditor: boolean
|
||||
export let newItem = false
|
||||
export let currentPath: string
|
||||
export let fakeInitialPath: string
|
||||
export let hash: string | undefined = undefined
|
||||
export let initialPath: string
|
||||
export let schema: any
|
||||
export let isFlow: boolean
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
export let args: Record<string, any> = {}
|
||||
let eventStreamType: 'kafka' | 'nats' | 'sqs' | 'mqtt' | 'gcp' = 'kafka'
|
||||
|
||||
$: {
|
||||
if (
|
||||
$selectedTrigger === 'kafka' ||
|
||||
$selectedTrigger === 'nats' ||
|
||||
$selectedTrigger === 'sqs' ||
|
||||
$selectedTrigger === 'mqtt' ||
|
||||
$selectedTrigger === 'gcp'
|
||||
) {
|
||||
eventStreamType = $selectedTrigger
|
||||
}
|
||||
interface Props {
|
||||
noEditor: boolean
|
||||
newItem?: boolean
|
||||
currentPath: string
|
||||
fakeInitialPath?: string
|
||||
hash?: string | undefined
|
||||
args?: Record<string, any>
|
||||
initialPath: string
|
||||
isFlow: boolean
|
||||
canHavePreprocessor?: boolean
|
||||
hasPreprocessor?: boolean
|
||||
isDeployed?: boolean
|
||||
schema?: Record<string, any> | undefined
|
||||
noCapture?: boolean
|
||||
isEditor?: boolean
|
||||
onDeployTrigger?: (trigger: Trigger) => void
|
||||
}
|
||||
|
||||
const { selectedTrigger, simplifiedPoll } = getContext<TriggerContext>('TriggerContext')
|
||||
let {
|
||||
noEditor,
|
||||
newItem = false,
|
||||
currentPath,
|
||||
fakeInitialPath = '',
|
||||
hash = undefined,
|
||||
args = {},
|
||||
initialPath,
|
||||
isFlow,
|
||||
canHavePreprocessor = false,
|
||||
hasPreprocessor = false,
|
||||
isDeployed = false,
|
||||
schema = undefined,
|
||||
noCapture = false,
|
||||
isEditor = true,
|
||||
onDeployTrigger
|
||||
}: Props = $props()
|
||||
|
||||
let config: Record<string, any> = $state({})
|
||||
let width = $state(0)
|
||||
let emailDomain: string | undefined = $state(undefined)
|
||||
let isValid = $state(false)
|
||||
let renderCount = $state(0)
|
||||
let loading = $state(false)
|
||||
|
||||
const useVerticalTriggerBar = $derived(width < 1000)
|
||||
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
onDestroy(() => {
|
||||
dispatch('exitTriggers')
|
||||
})
|
||||
|
||||
// Handle trigger selection
|
||||
function onSelect(triggerIndex: number) {
|
||||
triggersState.selectedTriggerIndex = triggerIndex
|
||||
}
|
||||
|
||||
function deleteDraftTrigger(triggerIndex: number | undefined) {
|
||||
if (triggerIndex === undefined) {
|
||||
return
|
||||
}
|
||||
triggersState.deleteTrigger(triggersCount, triggerIndex)
|
||||
triggersState.selectedTriggerIndex = triggersState.triggers.length - 1
|
||||
}
|
||||
|
||||
async function handleUpdate(trigger: number | undefined, path: string) {
|
||||
if (!trigger || trigger === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const { type: triggerType, id: triggerId, path: triggerPath } = triggersState.triggers[trigger]
|
||||
loading = true
|
||||
|
||||
triggersState.selectedTriggerIndex = undefined
|
||||
triggersState.deleteTrigger(triggersCount, trigger)
|
||||
|
||||
if (triggerType === 'schedule') {
|
||||
await triggersState.fetchSchedules(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
undefined,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'websocket') {
|
||||
await triggersState.fetchWebsocketTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'postgres') {
|
||||
await triggersState.fetchPostgresTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'kafka') {
|
||||
await triggersState.fetchKafkaTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'nats') {
|
||||
await triggersState.fetchNatsTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'gcp') {
|
||||
await triggersState.fetchGcpTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'sqs') {
|
||||
await triggersState.fetchSqsTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'mqtt') {
|
||||
await triggersState.fetchMqttTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'http') {
|
||||
await triggersState.fetchHttpTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
}
|
||||
|
||||
triggersState.selectedTriggerIndex = triggersState.triggers.findIndex(
|
||||
(t) => t.path === path && t.type === triggerType
|
||||
)
|
||||
loading = false
|
||||
|
||||
onDeployTrigger?.({ type: triggerType, id: triggerId, path: triggerPath })
|
||||
}
|
||||
|
||||
function handleUpdateDraftConfig(
|
||||
triggerIndex: number | undefined,
|
||||
newConfig: Record<string, any>,
|
||||
saveDisabled: boolean
|
||||
) {
|
||||
if (triggerIndex && triggerIndex !== -1 && newConfig) {
|
||||
triggersState.setDraftConfig(triggerIndex, { ...newConfig, canSave: !saveDisabled })
|
||||
}
|
||||
}
|
||||
|
||||
function handleResetDraft(trigger: number | undefined) {
|
||||
if (!trigger) {
|
||||
return
|
||||
}
|
||||
triggersState.setDraftConfig(trigger, undefined)
|
||||
renderCount++
|
||||
}
|
||||
|
||||
function handleAddTrigger(type: TriggerType) {
|
||||
const newTrigger = triggersState.addDraftTrigger(
|
||||
triggersCount,
|
||||
type,
|
||||
type === 'schedule' ? initialPath : undefined
|
||||
)
|
||||
triggersState.selectedTriggerIndex = newTrigger
|
||||
}
|
||||
|
||||
const cloudDisabled = $derived(
|
||||
CLOUD_DISABLED_TRIGGER_TYPES.includes(triggersState.selectedTrigger?.type ?? '') &&
|
||||
isCloudHosted()
|
||||
)
|
||||
</script>
|
||||
|
||||
<FlowCard {noEditor} title="Triggers">
|
||||
{#if !$simplifiedPoll}
|
||||
<div class="h-full flex flex-col">
|
||||
<Tabs bind:selected={$selectedTrigger} wrapperClass="overflow-hidden shrink-0">
|
||||
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
|
||||
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
|
||||
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">HTTP</Tab>
|
||||
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">WebSockets</Tab>
|
||||
<Tab value="postgres" selectedClass="text-primary text-sm font-semibold">Postgres</Tab>
|
||||
<Tab
|
||||
value="kafka"
|
||||
otherValues={['nats', 'sqs', 'mqtt', 'gcp']}
|
||||
selectedClass="text-primary text-sm font-semibold"
|
||||
>
|
||||
Event streams
|
||||
</Tab>
|
||||
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
|
||||
{#if isFlow}
|
||||
<Tab value="scheduledPoll" selectedClass="text-primary text-sm font-semibold"
|
||||
>Scheduled Poll</Tab
|
||||
>
|
||||
{/if}
|
||||
<div bind:clientWidth={width} class="h-full w-full">
|
||||
<FlowCard {noEditor} noHeader>
|
||||
<Splitpanes horizontal>
|
||||
<Pane>
|
||||
<div class="flex flex-row h-full">
|
||||
<!-- Left Pane - Triggers List -->
|
||||
{#if !useVerticalTriggerBar}
|
||||
<div class="w-[350px] flex-shrink-0 overflow-auto pr-2 pl-4 pt-2 pb-2">
|
||||
<TriggersTable
|
||||
selectedTrigger={triggersState.selectedTriggerIndex}
|
||||
{onSelect}
|
||||
triggers={triggersState.triggers}
|
||||
{isEditor}
|
||||
onAddDraftTrigger={handleAddTrigger}
|
||||
onDeleteDraft={deleteDraftTrigger}
|
||||
onReset={handleResetDraft}
|
||||
webhookToken={$triggersCount?.webhook_count}
|
||||
emailToken={$triggersCount?.email_count}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-2 flex flex-col gap-2 border-r">
|
||||
<AddTriggersButton
|
||||
onAddDraftTrigger={handleAddTrigger}
|
||||
class="w-fit h-fit"
|
||||
placement="right-start"
|
||||
>
|
||||
<Button size="xs" nonCaptureEvent btnClasses="p-2 w-fit" wrapperClasses="p-0">
|
||||
<Plus size="14" />
|
||||
</Button>
|
||||
</AddTriggersButton>
|
||||
<TriggersBadge
|
||||
showOnlyWithCount={false}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{newItem}
|
||||
isFlow
|
||||
selected={true}
|
||||
small={false}
|
||||
vertical
|
||||
onSelect={(triggerIndex: number) => {
|
||||
triggersState.selectedTriggerIndex = triggerIndex
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<div class="min-h-0 grow overflow-y-auto">
|
||||
{#if $selectedTrigger === 'webhooks'}
|
||||
<div class="p-4">
|
||||
<WebhooksPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{hash}
|
||||
{isFlow}
|
||||
{args}
|
||||
token=""
|
||||
{newItem}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'emails'}
|
||||
<div class="p-4">
|
||||
<EmailTriggerPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
token=""
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'routes'}
|
||||
<div class="p-4">
|
||||
<RoutesPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
{args}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'websockets'}
|
||||
<div class="p-4">
|
||||
<WebsocketTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'postgres'}
|
||||
<div class="p-4">
|
||||
<PostgresTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
isEditor={true}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats' || $selectedTrigger === 'sqs' || $selectedTrigger === 'mqtt' || $selectedTrigger === 'gcp'}
|
||||
<div class="p-4 flex flex-col gap-2">
|
||||
<ToggleButtonGroup bind:selected={eventStreamType} let:item>
|
||||
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} {item} />
|
||||
<ToggleButton value="nats" label="NATS" icon={NatsIcon} {item} />
|
||||
<ToggleButton value="mqtt" label="MQTT" icon={MqttIcon} {item} />
|
||||
<ToggleButton value="sqs" label="SQS" icon={AwsIcon} {item} />
|
||||
<ToggleButton value="gcp" label="GCP" icon={GoogleCloudIcon} {item} />
|
||||
</ToggleButtonGroup>
|
||||
{#if eventStreamType === 'kafka'}
|
||||
<KafkaTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={initialPath || fakeInitialPath}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex-grow overflow-auto pl-2 pr-4 pb-4 pt-2',
|
||||
useVerticalTriggerBar ? 'pl-4 pt-2' : ''
|
||||
)}
|
||||
style="scrollbar-gutter: stable"
|
||||
>
|
||||
{#if loading}
|
||||
<div
|
||||
class="animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms] h-full w-full"
|
||||
></div>
|
||||
{:else if triggersState.selectedTrigger}
|
||||
{#key [renderCount, triggersState.selectedTriggerIndex].join('-')}
|
||||
<div in:fade={{ duration: 100, delay: 100 }} out:fade={{ duration: 100 }}>
|
||||
<TriggersWrapperV2
|
||||
selectedTrigger={triggersState.selectedTrigger}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
{:else if eventStreamType === 'nats'}
|
||||
<NatsTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
{initialPath}
|
||||
{fakeInitialPath}
|
||||
{currentPath}
|
||||
{hash}
|
||||
{isDeployed}
|
||||
small={useVerticalTriggerBar}
|
||||
{args}
|
||||
{newItem}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{schema}
|
||||
{isEditor}
|
||||
onDelete={() => {
|
||||
deleteDraftTrigger(triggersState.selectedTriggerIndex)
|
||||
}}
|
||||
onUpdate={(path) => {
|
||||
handleUpdate(triggersState.selectedTriggerIndex, path)
|
||||
}}
|
||||
onConfigChange={(cfg, canSave, updated) => {
|
||||
if (updated) {
|
||||
handleUpdateDraftConfig(triggersState.selectedTriggerIndex, cfg, canSave)
|
||||
}
|
||||
}}
|
||||
onCaptureConfigChange={(cfg, isValidConfig) => {
|
||||
config = cfg
|
||||
isValid = isValidConfig
|
||||
}}
|
||||
onReset={() => {
|
||||
handleResetDraft(triggersState.selectedTriggerIndex)
|
||||
}}
|
||||
on:email-domain={({ detail }) => {
|
||||
emailDomain = detail
|
||||
}}
|
||||
/>
|
||||
{:else if eventStreamType === 'sqs'}
|
||||
<SqsTriggerPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
{:else if eventStreamType === 'mqtt'}
|
||||
<MqttTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
{:else if eventStreamType === 'gcp'}
|
||||
<GcpTriggerPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'schedules'}
|
||||
<div class="p-4">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'scheduledPoll'}
|
||||
<div class="p-4">
|
||||
<ScheduledPollPanel />
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{:else}
|
||||
<span class="text-sm text-tertiary text-center mx-auto mt-2"
|
||||
>{`Select a trigger from the ${useVerticalTriggerBar ? 'left toolbar' : 'table'} or a create a new one`}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-4 pb-2">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</FlowCard>
|
||||
</div>
|
||||
</Pane>
|
||||
{#if !cloudDisabled && triggersState.selectedTrigger && triggersState.selectedTrigger.type !== 'schedule' && triggersState.selectedTrigger.type != 'poll' && !noCapture}
|
||||
{@const captureKind = triggersState.selectedTrigger
|
||||
? triggerTypeToCaptureKind(triggersState.selectedTrigger.type)
|
||||
: undefined}
|
||||
{#if captureKind}
|
||||
{#key captureKind}
|
||||
<Pane minSize={20} size={40}>
|
||||
<CaptureWrapper
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
captureType={captureKind}
|
||||
{hasPreprocessor}
|
||||
{canHavePreprocessor}
|
||||
args={config}
|
||||
data={{ args, hash, emailDomain }}
|
||||
{isValid}
|
||||
triggerDeployed={!triggersState.selectedTrigger.isDraft}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
/>
|
||||
</Pane>
|
||||
{/key}
|
||||
{/if}
|
||||
{/if}
|
||||
</Splitpanes>
|
||||
</FlowCard>
|
||||
</div>
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { ChevronDown, Save } from 'lucide-svelte'
|
||||
import CaptureWrapper from './CaptureWrapper.svelte'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import Popover from '../Popover.svelte'
|
||||
import Section from '../Section.svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { type CaptureTriggerKind } from '$lib/gen'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersWrapper from './TriggersWrapper.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let cloudDisabled: boolean
|
||||
export let triggerType: CaptureTriggerKind
|
||||
export let isFlow: boolean = false
|
||||
export let data: any = {}
|
||||
export let noSave = false
|
||||
export let isEditor: boolean = false
|
||||
export let path: string = ''
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
export let newItem: boolean
|
||||
export let openForm: boolean = false
|
||||
export let alwaysOpened: boolean = false
|
||||
export let showCapture: boolean = false
|
||||
|
||||
const captureTypeLabels: Record<CaptureTriggerKind, string> = {
|
||||
http: 'custom HTTP route',
|
||||
websocket: 'WebSocket trigger',
|
||||
webhook: 'Webhook',
|
||||
kafka: 'Kafka trigger',
|
||||
email: 'Email trigger',
|
||||
nats: 'NATS trigger',
|
||||
mqtt: 'MQTT trigger',
|
||||
sqs: 'SQS trigger',
|
||||
postgres: 'Postgres trigger',
|
||||
gcp: 'GCP Pub/Sub trigger'
|
||||
}
|
||||
|
||||
const { captureOn } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let args: Record<string, any> = {}
|
||||
$: collapsed = !openForm
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let init = false
|
||||
$: updateShowCapture(!!$captureOn)
|
||||
function updateShowCapture(show: boolean) {
|
||||
if (show && !init) {
|
||||
$captureOn = undefined
|
||||
showCapture = true
|
||||
init = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section
|
||||
label={`+ New ${captureTypeLabels[triggerType]}`}
|
||||
collapsable={!alwaysOpened}
|
||||
bind:collapsed
|
||||
>
|
||||
<svelte:fragment slot="action">
|
||||
{#if !collapsed || alwaysOpened}
|
||||
<div class="flex flex-row grow w-min-0 gap-2 items-center justify-end">
|
||||
{#if isEditor}
|
||||
<Button
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
showCapture = !showCapture
|
||||
}}
|
||||
variant="border"
|
||||
color="light"
|
||||
endIcon={{
|
||||
icon: ChevronDown,
|
||||
classes: twMerge('transition', showCapture ? 'rotate-180' : '')
|
||||
}}
|
||||
>
|
||||
Test trigger
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if !noSave}
|
||||
{@const disabled = newItem || cloudDisabled}
|
||||
<Popover notClickable>
|
||||
<Button
|
||||
size="xs2"
|
||||
{disabled}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={() => {
|
||||
dispatch('saveTrigger', {
|
||||
config: args
|
||||
})
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<svelte:fragment slot="text">
|
||||
{#if disabled}
|
||||
{#if newItem}
|
||||
Deploy the runnable to enable trigger creation
|
||||
{:else if cloudDisabled}
|
||||
{capitalize(triggerType)} triggers are disabled in the multi-tenant cloud
|
||||
{/if}
|
||||
{:else}
|
||||
Create new {captureTypeLabels[triggerType].toLowerCase()}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isEditor}
|
||||
<CaptureWrapper
|
||||
{path}
|
||||
{isFlow}
|
||||
captureType={triggerType}
|
||||
{hasPreprocessor}
|
||||
{canHavePreprocessor}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:saveTrigger
|
||||
on:testWithArgs
|
||||
bind:args
|
||||
{data}
|
||||
{showCapture}
|
||||
/>
|
||||
{:else}
|
||||
<TriggersWrapper {path} {isFlow} {triggerType} {cloudDisabled} {args} {data} />
|
||||
{/if}
|
||||
</Section>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script lang="ts">
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Plus, Star, Loader2, Trash, RotateCcw } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { triggerIconMap, type Trigger, type TriggerType } from './utils'
|
||||
import AddTriggersButton from './AddTriggersButton.svelte'
|
||||
import TriggerLabel from './TriggerLabel.svelte'
|
||||
|
||||
interface Props {
|
||||
// Props
|
||||
selectedTrigger?: number | undefined
|
||||
triggers?: Trigger[]
|
||||
isEditor?: boolean
|
||||
webhookToken?: number
|
||||
emailToken?: number
|
||||
onSelect?: (trigger: number) => void
|
||||
onAddDraftTrigger?: (type: TriggerType) => void
|
||||
onDeleteDraft?: (triggerIndex: number) => void
|
||||
onReset?: (triggerIndex: number) => void
|
||||
}
|
||||
|
||||
let {
|
||||
selectedTrigger = undefined,
|
||||
triggers = [],
|
||||
isEditor = false,
|
||||
webhookToken,
|
||||
emailToken,
|
||||
onSelect,
|
||||
onAddDraftTrigger,
|
||||
onDeleteDraft,
|
||||
onReset
|
||||
}: Props = $props()
|
||||
|
||||
// Component state
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<div class="w-full">
|
||||
<AddTriggersButton {onAddDraftTrigger} setDropdownWidthToButtonWidth class="w-full" {isEditor}>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
startIcon={{ icon: Plus }}
|
||||
nonCaptureEvent
|
||||
btnClasses="w-full justify-center"
|
||||
>
|
||||
<span>Add trigger</span>
|
||||
</Button>
|
||||
</AddTriggersButton>
|
||||
</div>
|
||||
<DataTable {loading} size="sm" tableFixed={true}>
|
||||
<tbody>
|
||||
{#each triggers as trigger, index}
|
||||
{@const SvelteComponent = triggerIconMap[trigger.type]}
|
||||
<tr
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover cursor-pointer h-10 group',
|
||||
selectedTrigger === index ? 'bg-surface-hover ' : ''
|
||||
)}
|
||||
onclick={() => onSelect?.(index)}
|
||||
>
|
||||
<td class="w-12 text-center py-2 px-2">
|
||||
<div class="relative flex justify-center items-center">
|
||||
<SvelteComponent
|
||||
size={16}
|
||||
class={trigger.isDraft ? 'text-frost-400' : 'text-tertiary'}
|
||||
/>
|
||||
|
||||
{#if trigger.isPrimary}
|
||||
<Star size={10} class="absolute -mt-3 ml-3 text-blue-400" />
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 px-2 text-xs">
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<div class="flex items-center grow min-w-0">
|
||||
<TriggerLabel {trigger} />
|
||||
{#if trigger.type === 'webhook' && webhookToken}
|
||||
<span
|
||||
class="ml-2 px-1.5 text-xs rounded-md bg-tertiary/50 group-hover:bg-primary text-primary-inverse py-0.5"
|
||||
>
|
||||
{`${webhookToken} token${webhookToken > 1 ? 's' : ''}`}
|
||||
</span>
|
||||
{:else if trigger.type === 'email' && emailToken}
|
||||
<span
|
||||
class="ml-2 text-xs rounded-md bg-tertiary/50 group-hover:bg-primary text-primary-inverse px-1.5 py-0.5"
|
||||
>
|
||||
{`${emailToken} token${emailToken > 1 ? 's' : ''}`}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !['email', 'webhook', 'cli'].includes(trigger.type)}
|
||||
{#if trigger.isDraft}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
btnClasses="transition-all duration-200 text-transparent hover:bg-surface group-hover:text-primary bg-transparent px-1 py-1"
|
||||
startIcon={{ icon: Trash }}
|
||||
iconOnly
|
||||
on:click={() => onDeleteDraft?.(index)}
|
||||
/>
|
||||
{:else if !!trigger.draftConfig && !trigger.isDraft}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
btnClasses="transition-all duration-200 text-transparent hover:bg-surface group-hover:text-primary bg-transparent px-1 py-1"
|
||||
startIcon={{ icon: RotateCcw }}
|
||||
iconOnly
|
||||
title="Reset to deployed version"
|
||||
on:click={() => onReset?.(index)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
{#if !loading && triggers.length === 0}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center py-4 text-tertiary text-sm"> No triggers found </td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if loading && triggers.length === 0}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center py-4 text-tertiary text-sm">
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<Loader2 class="animate-spin" size={16} />
|
||||
<span>Loading triggers...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</DataTable>
|
||||
</div>
|
||||
@@ -1,117 +1,173 @@
|
||||
<script lang="ts">
|
||||
import { type CaptureTriggerKind } from '$lib/gen'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import RouteEditorConfigSection from './http/RouteEditorConfigSection.svelte'
|
||||
import WebsocketEditorConfigSection from './websocket/WebsocketEditorConfigSection.svelte'
|
||||
import WebhooksConfigSection from './webhook/WebhooksConfigSection.svelte'
|
||||
import EmailTriggerConfigSection from '../details/EmailTriggerConfigSection.svelte'
|
||||
import KafkaTriggersConfigSection from './kafka/KafkaTriggersConfigSection.svelte'
|
||||
import NatsTriggersConfigSection from './nats/NatsTriggersConfigSection.svelte'
|
||||
import MqttEditorConfigSection from './mqtt/MqttEditorConfigSection.svelte'
|
||||
import SqsTriggerEditorConfigSection from './sqs/SqsTriggerEditorConfigSection.svelte'
|
||||
import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte'
|
||||
import GcpTriggerEditorConfigSection from './gcp/GcpTriggerEditorConfigSection.svelte'
|
||||
import RoutesPanel from './http/RoutesPanel.svelte'
|
||||
import WebhooksPanel from './webhook/WebhooksPanel.svelte'
|
||||
import EmailTriggerPanel from '../details/EmailTriggerPanel.svelte'
|
||||
import SchedulePanel from '$lib/components/SchedulePanel.svelte'
|
||||
import PostgresTriggersPanel from './postgres/PostgresTriggersPanel.svelte'
|
||||
import KafkaTriggerPanel from './kafka/KafkaTriggersPanel.svelte'
|
||||
import NatsTriggersPanel from './nats/NatsTriggersPanel.svelte'
|
||||
import MqttTriggerPanel from './mqtt/MqttTriggersPanel.svelte'
|
||||
import SqsTriggerPanel from './sqs/SqsTriggerPanel.svelte'
|
||||
import GcpTriggerPanel from './gcp/GcpTriggerPanel.svelte'
|
||||
import ScheduledPollPanel from './scheduled/ScheduledPollPanel.svelte'
|
||||
import WebsocketTriggersPanel from './websocket/WebsocketTriggersPanel.svelte'
|
||||
import { triggerIconMap, type Trigger } from './utils'
|
||||
import ClipboardPanel from '../details/ClipboardPanel.svelte'
|
||||
import CliHelpBox from '../CliHelpBox.svelte'
|
||||
import TriggerLabel from './TriggerLabel.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let triggerType: CaptureTriggerKind = 'webhook'
|
||||
export let cloudDisabled: boolean = false
|
||||
export let args: any
|
||||
export let isFlow: boolean = false
|
||||
export let path: string = ''
|
||||
export let data: any = {}
|
||||
interface Props {
|
||||
selectedTrigger: Trigger
|
||||
isFlow: boolean
|
||||
initialPath: string
|
||||
fakeInitialPath: string
|
||||
currentPath: string
|
||||
hash?: string
|
||||
isDeployed: boolean
|
||||
small: boolean
|
||||
args: Record<string, any>
|
||||
newItem: boolean
|
||||
schema: Record<string, any> | undefined
|
||||
isEditor?: boolean
|
||||
onConfigChange?: (cfg: Record<string, any>, canSave: boolean, updated: boolean) => void
|
||||
onCaptureConfigChange?: (cfg: Record<string, any>, isValidConfig: boolean) => void
|
||||
onUpdate?: (path: string) => void
|
||||
onDelete?: () => void
|
||||
onReset?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow = false,
|
||||
initialPath,
|
||||
fakeInitialPath,
|
||||
currentPath,
|
||||
hash,
|
||||
small,
|
||||
args,
|
||||
newItem,
|
||||
schema,
|
||||
...props
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
{#if cloudDisabled}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
{capitalize(triggerType)} triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else if triggerType === 'websocket'}
|
||||
<WebsocketEditorConfigSection
|
||||
can_write={true}
|
||||
headless={true}
|
||||
bind:url={args.url}
|
||||
bind:url_runnable_args={args.url_runnable_args}
|
||||
showCapture={false}
|
||||
{#if selectedTrigger.type === 'http'}
|
||||
<RoutesPanel
|
||||
{selectedTrigger}
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'webhook'}
|
||||
<WebhooksPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{hash}
|
||||
token=""
|
||||
{args}
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
{newItem}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'email'}
|
||||
<EmailTriggerPanel
|
||||
token=""
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
on:email-domain
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'schedule'}
|
||||
<SchedulePanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
{schema}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'websocket'}
|
||||
<WebsocketTriggersPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'kafka'}
|
||||
<KafkaTriggerPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'postgres'}
|
||||
<PostgresTriggersPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'nats'}
|
||||
<NatsTriggersPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'mqtt'}
|
||||
<MqttTriggerPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'sqs'}
|
||||
<SqsTriggerPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'gcp'}
|
||||
<GcpTriggerPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
customLabel={small ? customLabel : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'poll'}
|
||||
<ScheduledPollPanel />
|
||||
{:else if selectedTrigger.type === 'cli'}
|
||||
<div class="py-1 flex flex-col gap-4">
|
||||
<ClipboardPanel content={selectedTrigger.extra?.cliCommand ?? ''} />
|
||||
<CliHelpBox />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#snippet customLabel()}
|
||||
{@const IconComponent = triggerIconMap[selectedTrigger.type]}
|
||||
<div class="flex flex-row gap-2 items-center grow min-w-0 pr-2">
|
||||
<IconComponent
|
||||
size={16}
|
||||
class={twMerge(selectedTrigger.isDraft ? 'text-frost-400' : '', 'shrink-0')}
|
||||
/>
|
||||
{:else if triggerType === 'postgres'}
|
||||
<PostgresEditorConfigSection
|
||||
can_write={true}
|
||||
headless={true}
|
||||
showCapture={false}
|
||||
bind:publication={args.publication}
|
||||
bind:postgres_resource_path={args.postgres_resource_path}
|
||||
/>
|
||||
{:else if triggerType === 'webhook'}
|
||||
<WebhooksConfigSection
|
||||
{isFlow}
|
||||
{path}
|
||||
hash={data?.hash}
|
||||
token={data?.token}
|
||||
runnableArgs={data?.args}
|
||||
scopes={data?.scopes}
|
||||
showCapture={false}
|
||||
/>
|
||||
{:else if triggerType === 'http'}
|
||||
<RouteEditorConfigSection
|
||||
showCapture={false}
|
||||
can_write={true}
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
bind:raw_string={args.raw_string}
|
||||
bind:wrap_body={args.wrap_body}
|
||||
capture_mode={true}
|
||||
headless
|
||||
/>
|
||||
{:else if triggerType === 'email'}
|
||||
<EmailTriggerConfigSection
|
||||
hash={data?.hash}
|
||||
token={data?.token}
|
||||
{path}
|
||||
{isFlow}
|
||||
userSettings={data?.userSettings}
|
||||
emailDomain={data?.emailDomain}
|
||||
/>
|
||||
{:else if triggerType === 'kafka'}
|
||||
<KafkaTriggersConfigSection headless={true} bind:args staticInputDisabled={false} {path} />
|
||||
{:else if triggerType === 'nats'}
|
||||
<NatsTriggersConfigSection headless={true} bind:args staticInputDisabled={false} {path} />
|
||||
{:else if triggerType === 'mqtt'}
|
||||
<MqttEditorConfigSection
|
||||
bind:v3_config={args.v3_config}
|
||||
bind:v5_config={args.v5_config}
|
||||
bind:client_version={args.client_version}
|
||||
bind:subscribe_topics={args.subscribe_topics}
|
||||
bind:mqtt_resource_path={args.mqtt_resource_path}
|
||||
bind:client_id={args.client_id}
|
||||
showCapture={false}
|
||||
headless={true}
|
||||
can_write={true}
|
||||
/>
|
||||
{:else if triggerType === 'sqs'}
|
||||
<SqsTriggerEditorConfigSection
|
||||
bind:queue_url={args.queue_url}
|
||||
bind:aws_resource_path={args.aws_resource_path}
|
||||
bind:message_attributes={args.message_attributes}
|
||||
bind:aws_auth_resource_type={args.aws_auth_resource_type}
|
||||
headless={true}
|
||||
can_write={true}
|
||||
showCapture={false}
|
||||
/>
|
||||
{:else if triggerType === 'gcp'}
|
||||
<GcpTriggerEditorConfigSection
|
||||
bind:gcp_resource_path={args.gcp_resource_path}
|
||||
bind:topic_id={args.topic_id}
|
||||
bind:subscription_id={args.subscription_id}
|
||||
bind:delivery_config={args.delivery_config}
|
||||
bind:delivery_type={args.delivery_type}
|
||||
bind:subscription_mode={args.subscription_mode}
|
||||
bind:cloud_subscription_id={args.subscription_id}
|
||||
bind:create_update_subscription_id={args.subscription_id}
|
||||
bind:base_endpoint={args.base_endpoint}
|
||||
headless={true}
|
||||
can_write={true}
|
||||
showCapture={false}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<TriggerLabel trigger={selectedTrigger} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Url } from '$lib/components/common'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
interface Props {
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
isValid?: boolean | undefined
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
deliveryType?: 'push' | 'pull'
|
||||
captureLoading?: boolean
|
||||
triggerDeployed?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
captureInfo = undefined,
|
||||
isValid = undefined,
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
deliveryType = 'pull',
|
||||
captureLoading = false,
|
||||
triggerDeployed = false
|
||||
}: Props = $props()
|
||||
|
||||
function getCaptureUrl(captureInfo: CaptureInfo | undefined) {
|
||||
if (!captureInfo) {
|
||||
return
|
||||
}
|
||||
return `${window.location.origin}${base}/api/w/${$workspaceStore}/capture_u/gcp/${
|
||||
captureInfo.isFlow ? 'flow' : 'script'
|
||||
}/${captureInfo.path}`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="gcp"
|
||||
disabled={isValid === false}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
displayAlert={triggerDeployed}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Listening to GCP Pub/Sub events...
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to GCP Pub/Sub events.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if deliveryType === 'push'}
|
||||
{@const captureUrl = getCaptureUrl(captureInfo)}
|
||||
<Url url={captureUrl} label="Test URL" />
|
||||
{/if}
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -2,7 +2,9 @@
|
||||
import { tick } from 'svelte'
|
||||
import GcpTriggerEditorInner from './GcpTriggerEditorInner.svelte'
|
||||
|
||||
let open = false
|
||||
let { onUpdate }: { onUpdate?: (path?: string) => void } = $props()
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
open = true
|
||||
await tick()
|
||||
@@ -12,16 +14,17 @@
|
||||
export async function openNew(
|
||||
is_flow: boolean,
|
||||
initial_script_path?: string,
|
||||
defaultValues?: Record<string, any>
|
||||
defaultValues?: Record<string, any>,
|
||||
newDraft?: boolean
|
||||
) {
|
||||
open = true
|
||||
await tick()
|
||||
drawer?.openNew(is_flow, initial_script_path, defaultValues)
|
||||
}
|
||||
|
||||
let drawer: GcpTriggerEditorInner
|
||||
let drawer: GcpTriggerEditorInner | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<GcpTriggerEditorInner on:update bind:this={drawer} />
|
||||
<GcpTriggerEditorInner {onUpdate} bind:this={drawer} />
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import { copyToClipboard, emptyStringTrimmed } from '$lib/utils'
|
||||
import { emptyStringTrimmed } from '$lib/utils'
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
import Subsection from '$lib/components/Subsection.svelte'
|
||||
import {
|
||||
@@ -15,8 +13,6 @@
|
||||
} from '$lib/gen'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
|
||||
@@ -24,15 +20,13 @@
|
||||
import Select from '$lib/components/apps/svelte-select/lib/Select.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
import { Button } from '$lib/components/common'
|
||||
import { ClipboardCopy, RefreshCw } from 'lucide-svelte'
|
||||
import { Button, Url } from '$lib/components/common'
|
||||
import { RefreshCw } from 'lucide-svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
|
||||
export let can_write: boolean = false
|
||||
export let headless: boolean = false
|
||||
export let showCapture: boolean = false
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let isValid: boolean = false
|
||||
export let gcp_resource_path = ''
|
||||
export let subscription_id = ''
|
||||
@@ -40,16 +34,17 @@
|
||||
export let delivery_type: DeliveryType | undefined = 'pull'
|
||||
export let delivery_config: PushConfig | undefined
|
||||
export let subscription_mode: SubscriptionMode = 'create_update'
|
||||
export let base_endpoint: string = getBaseUrl(captureInfo)
|
||||
export let path = captureInfo ? captureInfo.path : ''
|
||||
export let base_endpoint: string = getBaseUrl()
|
||||
export let path = ''
|
||||
export let showTestingBadge: boolean = false
|
||||
|
||||
let topic_items: string[] = []
|
||||
let subscription_items: string[] = []
|
||||
let darkMode = false
|
||||
|
||||
const DEFAULT_PUSH_CONFIG: PushConfig = {
|
||||
audience: '',
|
||||
authenticate: false,
|
||||
audience: getBaseUrl(),
|
||||
authenticate: false
|
||||
}
|
||||
|
||||
async function loadAllPubSubTopicsFromProject() {
|
||||
@@ -76,7 +71,10 @@
|
||||
export let cloud_subscription_id = ''
|
||||
export let create_update_subscription_id = ''
|
||||
|
||||
$: isValid = !emptyStringTrimmed(gcp_resource_path) && !emptyStringTrimmed(topic_id)
|
||||
$: isValid =
|
||||
!emptyStringTrimmed(gcp_resource_path) &&
|
||||
!emptyStringTrimmed(topic_id) &&
|
||||
!emptyStringTrimmed(subscription_id)
|
||||
$: if (!delivery_type) {
|
||||
delivery_type = 'pull'
|
||||
}
|
||||
@@ -84,43 +82,28 @@
|
||||
delivery_config = DEFAULT_PUSH_CONFIG
|
||||
}
|
||||
|
||||
function getBaseUrl(captureInfo: CaptureInfo | undefined) {
|
||||
if (captureInfo) {
|
||||
return `${window.location.origin}${base}/api/w/${$workspaceStore}/capture_u/gcp/${
|
||||
captureInfo.isFlow ? 'flow' : 'script'
|
||||
}`
|
||||
} else {
|
||||
return `${window.location.origin}${base}/api/gcp/w/${$workspaceStore!}`
|
||||
}
|
||||
function getBaseUrl() {
|
||||
return `${window.location.origin}${base}/api/gcp/w/${$workspaceStore!}`
|
||||
}
|
||||
|
||||
$: !base_endpoint && (base_endpoint = getBaseUrl(captureInfo))
|
||||
$: !base_endpoint && (base_endpoint = getBaseUrl())
|
||||
|
||||
$: {
|
||||
if (emptyStringTrimmed(subscription_id) && !emptyStringTrimmed(path)) {
|
||||
subscription_id = `windmill-${$workspaceStore!}-${path.replaceAll('/', '_')}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
|
||||
<div>
|
||||
{#if showCapture && captureInfo}
|
||||
{@const captureURL = `${base_endpoint}/${path}`}
|
||||
<CaptureSection
|
||||
captureType="gcp"
|
||||
disabled={!isValid}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
>
|
||||
{#if delivery_type === 'push'}
|
||||
<Label label="URL">
|
||||
<ClipboardPanel content={captureURL} disabled={!captureInfo.active} />
|
||||
</Label>
|
||||
{/if}
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
<Section label="GCP Pub/Sub" {headless}>
|
||||
<svelte:fragment slot="header">
|
||||
{#if showTestingBadge}
|
||||
<TestingBadge />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<Subsection label="Connection setup">
|
||||
<div class="flex flex-col gap-1 mt-2">
|
||||
@@ -208,7 +191,7 @@
|
||||
{#if subscription_mode === 'create_update'}
|
||||
<Subsection
|
||||
label="Subscription id"
|
||||
tooltip="The unique identifier for the Pub/Sub subscription. Leave it empty to let the system automatically generate a subscription ID, or provide a custom one if needed."
|
||||
tooltip="The unique identifier for the Pub/Sub subscription."
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
@@ -248,28 +231,9 @@
|
||||
</Subsection>
|
||||
{#if delivery_type === 'push' && delivery_config}
|
||||
<div class="flex flex-col gap-3 mt-1">
|
||||
<Subsection
|
||||
label="URL"
|
||||
tooltip="The URL of the service that receives push messages."
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="URL"
|
||||
disabled
|
||||
value={`${base_endpoint}/${path}`}
|
||||
/>
|
||||
<Button
|
||||
on:click={() => copyToClipboard(`${base_endpoint}/${path}`)}
|
||||
color="dark"
|
||||
size="xs"
|
||||
startIcon={{ icon: ClipboardCopy }}
|
||||
>
|
||||
Copy URL
|
||||
</Button>
|
||||
</div>
|
||||
</Subsection>
|
||||
<div class="flex gap-2">
|
||||
<Url url={`${base_endpoint}/${path}`} label="Production URL" />
|
||||
</div>
|
||||
<Subsection label="Authenticate">
|
||||
<p class="text-xs mb-2 text-tertiary">
|
||||
Enable Google Cloud authentication for push delivery using a verified token.<Required
|
||||
@@ -329,9 +293,9 @@
|
||||
</div>
|
||||
<Alert title="Push Subscription URL Requirements" type="warning">
|
||||
If the subscription uses <strong>push delivery</strong>, its endpoint URL must
|
||||
match the following format: <strong>{base_endpoint}/*</strong>, meaning it must
|
||||
start with
|
||||
<strong>{base_endpoint}</strong> followed by any path segment.
|
||||
match the following format: <strong>{`${base_endpoint}/${path}`}/*</strong>,
|
||||
meaning it must start with
|
||||
<strong>{`${base_endpoint}/${path}`}</strong> followed by any path segment.
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Loader2, Save } from 'lucide-svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import {
|
||||
GcpTriggerService,
|
||||
type DeliveryType,
|
||||
@@ -20,34 +18,83 @@
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import GcpTriggerEditorConfigSection from './GcpTriggerEditorConfigSection.svelte'
|
||||
import { base } from '$app/paths'
|
||||
import type { Snippet } from 'svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import { saveGcpTriggerFromCfg } from './utils'
|
||||
import { handleConfigChange } from '../utils'
|
||||
|
||||
let is_loading = false
|
||||
let drawer: Drawer
|
||||
let is_flow: boolean = false
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
let delivery_type: DeliveryType = 'pull'
|
||||
let itemKind: 'flow' | 'script' = 'script'
|
||||
let script_path = ''
|
||||
let initialScriptPath = ''
|
||||
let fixedScriptPath = ''
|
||||
let path: string = ''
|
||||
let pathError = ''
|
||||
let enabled = false
|
||||
let dirtyPath = false
|
||||
let can_write = true
|
||||
let drawerLoading = true
|
||||
let topic_id: string = ''
|
||||
let gcp_resource_path: string = ''
|
||||
let subscription_id: string = ''
|
||||
let isValid = false
|
||||
let delivery_config: PushConfig | undefined = undefined
|
||||
let subscription_mode: SubscriptionMode = 'create_update'
|
||||
const dispatch = createEventDispatcher()
|
||||
let drawer: Drawer | undefined = $state(undefined)
|
||||
let is_flow: boolean = $state(false)
|
||||
let initialPath = $state('')
|
||||
let edit = $state(true)
|
||||
let delivery_type: DeliveryType = $state('pull')
|
||||
let itemKind: 'flow' | 'script' = $state('script')
|
||||
let script_path = $state('')
|
||||
let initialScriptPath = $state('')
|
||||
let fixedScriptPath = $state('')
|
||||
let path: string = $state('')
|
||||
let pathError = $state('')
|
||||
let enabled = $state(false)
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let drawerLoading = $state(true)
|
||||
let topic_id: string = $state('')
|
||||
let gcp_resource_path: string = $state('')
|
||||
let subscription_id: string = $state('')
|
||||
let isValid = $state(false)
|
||||
let delivery_config: PushConfig | undefined = $state(undefined)
|
||||
let subscription_mode: SubscriptionMode = $state('create_update')
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
let deploymentLoading = $state(false)
|
||||
let base_endpoint = $derived(`${window.location.origin}${base}`)
|
||||
|
||||
$: is_flow = itemKind === 'flow'
|
||||
let {
|
||||
useDrawer = true,
|
||||
description = undefined,
|
||||
hideTarget = false,
|
||||
hideTooltips = false,
|
||||
isEditor = false,
|
||||
allowDraft = false,
|
||||
hasDraft = false,
|
||||
isDraftOnly = false,
|
||||
isDeployed = false,
|
||||
customLabel = undefined,
|
||||
onConfigChange = undefined,
|
||||
onCaptureConfigChange = undefined,
|
||||
onUpdate = undefined,
|
||||
onDelete = undefined,
|
||||
onReset = undefined,
|
||||
cloudDisabled = false
|
||||
}: {
|
||||
useDrawer?: boolean
|
||||
description?: Snippet | undefined
|
||||
hideTarget?: boolean
|
||||
hideTooltips?: boolean
|
||||
isEditor?: boolean
|
||||
allowDraft?: boolean
|
||||
hasDraft?: boolean
|
||||
isDraftOnly?: boolean
|
||||
isDeployed?: boolean
|
||||
customLabel?: Snippet
|
||||
onConfigChange?: (cfg: Record<string, any>, saveDisabled: boolean, updated: boolean) => void
|
||||
onCaptureConfigChange?: (cfg: Record<string, any>, isValid: boolean) => void
|
||||
onUpdate?: (path?: string) => void
|
||||
onDelete?: () => void
|
||||
onReset?: () => void
|
||||
cloudDisabled?: boolean
|
||||
} = $props()
|
||||
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
const gcpConfig = $derived.by(getGcpConfig)
|
||||
const saveDisabled = $derived(
|
||||
pathError != '' || emptyString(script_path) || !isValid || !can_write
|
||||
)
|
||||
const captureConfig = $derived.by(isEditor ? getGcpCaptureConfig : () => ({}))
|
||||
|
||||
export async function openEdit(
|
||||
ePath: string,
|
||||
isFlow: boolean,
|
||||
defaultValues?: Record<string, any>
|
||||
) {
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
@@ -55,11 +102,14 @@
|
||||
itemKind = isFlow ? 'flow' : 'script'
|
||||
edit = true
|
||||
dirtyPath = false
|
||||
await loadTrigger()
|
||||
await loadTrigger(defaultValues)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load GCP Pub/Sub trigger: ${err.body}`, true)
|
||||
} finally {
|
||||
drawerLoading = false
|
||||
if (!defaultValues) {
|
||||
initialConfig = structuredClone($state.snapshot(getGcpConfig()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,144 +132,183 @@
|
||||
subscription_id = ''
|
||||
topic_id = defaultValues?.topic_id
|
||||
subscription_mode = defaultValues?.subscription_mode ?? 'create_update'
|
||||
path = ''
|
||||
path = defaultValues?.path ?? ''
|
||||
initialPath = ''
|
||||
edit = false
|
||||
dirtyPath = false
|
||||
enabled = defaultValues?.enabled ?? false
|
||||
} finally {
|
||||
drawerLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrigger(): Promise<void> {
|
||||
try {
|
||||
const s = await GcpTriggerService.getGcpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
script_path = s.script_path
|
||||
initialScriptPath = s.script_path
|
||||
gcp_resource_path = s.gcp_resource_path
|
||||
delivery_type = s.delivery_type
|
||||
subscription_id = s.subscription_id
|
||||
delivery_config = s.delivery_config
|
||||
subscription_mode = s.subscription_mode
|
||||
is_flow = s.is_flow
|
||||
path = s.path
|
||||
enabled = s.enabled
|
||||
topic_id = s.topic_id
|
||||
can_write = canWrite(s.path, s.extra_perms, $userStore)
|
||||
} catch (error) {
|
||||
sendUserToast(`Could not load GCP Pub/Sub trigger: ${error.body}`, true)
|
||||
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
|
||||
if (defaultConfig) {
|
||||
loadTriggerConfig(defaultConfig)
|
||||
return
|
||||
} else {
|
||||
try {
|
||||
const s = await GcpTriggerService.getGcpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
loadTriggerConfig(s)
|
||||
} catch (error) {
|
||||
sendUserToast(`Could not load GCP Pub/Sub trigger: ${error.body}`, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTriggerConfig(cfg?: Record<string, any>): Promise<void> {
|
||||
script_path = cfg?.script_path
|
||||
initialScriptPath = cfg?.script_path
|
||||
gcp_resource_path = cfg?.gcp_resource_path
|
||||
delivery_type = cfg?.delivery_type
|
||||
subscription_id = cfg?.subscription_id
|
||||
delivery_config = cfg?.delivery_config
|
||||
subscription_mode = cfg?.subscription_mode
|
||||
is_flow = cfg?.is_flow
|
||||
path = cfg?.path
|
||||
enabled = cfg?.enabled
|
||||
topic_id = cfg?.topic_id
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
}
|
||||
|
||||
async function updateTrigger(): Promise<void> {
|
||||
try {
|
||||
is_loading = true
|
||||
const base_endpoint = `${window.location.origin}${base}`
|
||||
if (delivery_type === 'push') {
|
||||
if (!delivery_config) {
|
||||
sendUserToast('Must set route path when delivery type is push', true)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
delivery_config = undefined
|
||||
}
|
||||
if (edit) {
|
||||
await GcpTriggerService.updateGcpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
gcp_resource_path,
|
||||
subscription_mode,
|
||||
subscription_id,
|
||||
delivery_type,
|
||||
delivery_config,
|
||||
base_endpoint,
|
||||
topic_id,
|
||||
path,
|
||||
script_path,
|
||||
enabled,
|
||||
is_flow
|
||||
}
|
||||
})
|
||||
sendUserToast(`GCP Pub/Sub trigger ${path} updated`)
|
||||
} else {
|
||||
await GcpTriggerService.createGcpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
gcp_resource_path,
|
||||
subscription_mode,
|
||||
subscription_id,
|
||||
delivery_type,
|
||||
delivery_config,
|
||||
base_endpoint,
|
||||
topic_id,
|
||||
path,
|
||||
script_path,
|
||||
enabled: true,
|
||||
is_flow
|
||||
}
|
||||
})
|
||||
sendUserToast(`GCP Pub/Sub trigger ${path} created`)
|
||||
}
|
||||
deploymentLoading = true
|
||||
const cfg = gcpConfig
|
||||
if (!cfg) {
|
||||
return
|
||||
}
|
||||
const isSaved = await saveGcpTriggerFromCfg(
|
||||
initialPath,
|
||||
cfg,
|
||||
edit,
|
||||
$workspaceStore!,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
onUpdate?.(cfg.path)
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
deploymentLoading = false
|
||||
}
|
||||
|
||||
if (!$usedTriggerKinds.includes('gcp')) {
|
||||
$usedTriggerKinds = [...$usedTriggerKinds, 'gcp']
|
||||
}
|
||||
dispatch('update')
|
||||
drawer.closeDrawer()
|
||||
is_loading = false
|
||||
} catch (error) {
|
||||
is_loading = false
|
||||
sendUserToast(error.body, true)
|
||||
function getGcpConfig() {
|
||||
return {
|
||||
gcp_resource_path,
|
||||
subscription_mode,
|
||||
subscription_id,
|
||||
delivery_type,
|
||||
delivery_config,
|
||||
base_endpoint,
|
||||
topic_id,
|
||||
path,
|
||||
script_path,
|
||||
enabled,
|
||||
is_flow
|
||||
}
|
||||
}
|
||||
|
||||
function getGcpCaptureConfig() {
|
||||
return {
|
||||
gcp_resource_path,
|
||||
subscription_mode,
|
||||
subscription_id,
|
||||
delivery_type,
|
||||
delivery_config,
|
||||
base_endpoint,
|
||||
topic_id,
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleEnabled(toggleEnabled: boolean) {
|
||||
enabled = toggleEnabled
|
||||
if (!isDraftOnly && !hasDraft) {
|
||||
await GcpTriggerService.setGcpTriggerEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: toggleEnabled }
|
||||
})
|
||||
sendUserToast(`${toggleEnabled ? 'enabled' : 'disabled'} GCP Pub/Sub trigger ${initialPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onCaptureConfigChange?.(captureConfig, isValid)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!drawerLoading) {
|
||||
handleConfigChange(gcpConfig, initialConfig, saveDisabled, edit, onConfigChange)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit GCP Pub/Sub trigger ${initialPath}`
|
||||
: `GCP Pub/Sub trigger ${initialPath}`
|
||||
: 'New GCP Pub/Sub trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !drawerLoading && can_write}
|
||||
{#if edit}
|
||||
<div class="mr-8 center-center -mt-1">
|
||||
<Toggle
|
||||
disabled={!can_write}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
on:change={async (e) => {
|
||||
sendUserToast(
|
||||
`${e.detail ? 'enabled' : 'disabled'} GCP Pub/Sub trigger ${initialPath}`
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={pathError != '' || emptyString(script_path) || !isValid || !can_write}
|
||||
on:click={updateTrigger}
|
||||
loading={is_loading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{#if useDrawer}
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit GCP Pub/Sub trigger ${initialPath}`
|
||||
: `GCP Pub/Sub trigger ${initialPath}`
|
||||
: 'New GCP Pub/Sub trigger'}
|
||||
on:close={drawer?.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{@render actionsButtons()}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else}
|
||||
<Section label={!customLabel ? 'GCP Pub/Sub trigger' : ''} headerClass="grow min-w-0 h-[30px]">
|
||||
<svelte:fragment slot="header">
|
||||
{#if customLabel}
|
||||
{@render customLabel()}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#if drawerLoading}
|
||||
<div class="flex flex-col items-center justify-center h-full w-full">
|
||||
<Loader2 size="50" class="animate-spin" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-5">
|
||||
<svelte:fragment slot="action">
|
||||
{@render actionsButtons()}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
{#snippet actionsButtons()}
|
||||
{#if !drawerLoading && can_write}
|
||||
<TriggerEditorToolbar
|
||||
{isDraftOnly}
|
||||
{hasDraft}
|
||||
permissions={drawerLoading || !can_write ? 'none' : 'create'}
|
||||
{saveDisabled}
|
||||
{enabled}
|
||||
isLoading={deploymentLoading}
|
||||
{edit}
|
||||
{allowDraft}
|
||||
{isDeployed}
|
||||
onUpdate={updateTrigger}
|
||||
{onReset}
|
||||
{onDelete}
|
||||
onToggleEnabled={handleToggleEnabled}
|
||||
{cloudDisabled}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet config()}
|
||||
{#if drawerLoading}
|
||||
<div class="flex flex-col items-center justify-center h-full w-full">
|
||||
<Loader2 size="50" class="animate-spin" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-5">
|
||||
{#if description}
|
||||
{@render description()}
|
||||
{/if}
|
||||
{#if !hideTooltips}
|
||||
<Alert title="Info" type="info">
|
||||
{#if edit}
|
||||
Changes can take up to 30 seconds to take effect.
|
||||
@@ -227,23 +316,26 @@
|
||||
New GCP Pub/Sub trigger can take up to 30 seconds to start listening.
|
||||
{/if}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="gcp_trigger"
|
||||
kind="gcp_trigger"
|
||||
disabled={!can_write}
|
||||
disableEditing={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="gcp_trigger"
|
||||
kind="gcp_trigger"
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{#if !hideTarget}
|
||||
<Section label="Runnable">
|
||||
<p class="text-xs mb-1 text-tertiary">
|
||||
Pick a script or flow to be triggered <Required required={true} />
|
||||
@@ -256,35 +348,38 @@
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
/>
|
||||
{#if emptyString(script_path)}
|
||||
<Button
|
||||
btnClasses="ml-4 mt-2"
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={!can_write}
|
||||
href={itemKind === 'flow' ? '/flows/add?hub=68' : '/scripts/add?hub=hub%2F19662'}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<GcpTriggerEditorConfigSection
|
||||
bind:isValid
|
||||
bind:gcp_resource_path
|
||||
bind:subscription_id
|
||||
bind:delivery_type
|
||||
bind:delivery_config
|
||||
bind:topic_id
|
||||
bind:subscription_mode
|
||||
bind:path
|
||||
cloud_subscription_id={subscription_id}
|
||||
create_update_subscription_id={subscription_id}
|
||||
{can_write}
|
||||
headless={true}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<GcpTriggerEditorConfigSection
|
||||
bind:isValid
|
||||
bind:gcp_resource_path
|
||||
bind:subscription_id
|
||||
bind:delivery_type
|
||||
bind:delivery_config
|
||||
bind:topic_id
|
||||
bind:subscription_mode
|
||||
bind:path
|
||||
cloud_subscription_id={subscription_id}
|
||||
create_update_subscription_id={subscription_id}
|
||||
{can_write}
|
||||
headless={true}
|
||||
showTestingBadge={isEditor}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -1,131 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { GcpTriggerService, type GcpTrigger } from '$lib/gen'
|
||||
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersEditorSection from '../TriggersEditorSection.svelte'
|
||||
import GcpTriggerEditor from './GcpTriggerEditor.svelte'
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let newItem: boolean = false
|
||||
export let isEditor: boolean = false
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import GcpTriggerEditorInner from './GcpTriggerEditorInner.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let gcpTriggerEditor: GcpTriggerEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
isDeployed = false,
|
||||
defaultValues = undefined,
|
||||
customLabel = undefined,
|
||||
...restProps
|
||||
} = $props()
|
||||
let gcpTriggerEditor: GcpTriggerEditorInner | undefined = $state(undefined)
|
||||
|
||||
$: path && loadTriggers()
|
||||
|
||||
const { triggersCount, selectedTrigger, defaultValues } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
onMount(() => {
|
||||
if (
|
||||
defaultValues &&
|
||||
$selectedTrigger === 'gcp' &&
|
||||
Object.keys($defaultValues ?? {}).length > 0
|
||||
) {
|
||||
gcpTriggerEditor.openNew(isFlow, path, $defaultValues)
|
||||
defaultValues.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
let gcpTriggers: (GcpTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
export async function loadTriggers() {
|
||||
try {
|
||||
gcpTriggers = (
|
||||
await GcpTriggerService.listGcpTriggers({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
isFlow
|
||||
})
|
||||
).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), gcp_count: gcpTriggers?.length }
|
||||
openForm = gcpTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load GCP Pub/Sub triggers', e)
|
||||
async function openGcpTriggerEditor(isFlow: boolean, isDraft: boolean) {
|
||||
if (isDraft) {
|
||||
gcpTriggerEditor?.openNew(isFlow, path, defaultValues)
|
||||
} else {
|
||||
gcpTriggerEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<GcpTriggerEditor
|
||||
on:update={() => {
|
||||
loadTriggers()
|
||||
}}
|
||||
bind:this={gcpTriggerEditor}
|
||||
/>
|
||||
onMount(() => {
|
||||
gcpTriggerEditor && openGcpTriggerEditor(isFlow, selectedTrigger.isDraft ?? false)
|
||||
})
|
||||
|
||||
const cloudDisabled = $derived(isCloudHosted())
|
||||
</script>
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert title="EE Only" type="warning" size="xs">
|
||||
GCP Pub/Sub triggers are an enterprise only feature.
|
||||
</Alert>
|
||||
{:else if isCloudHosted()}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
GCP Pub/Sub triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<Description link="https://cloud.google.com/pubsub/docs?hl=en">
|
||||
GCP Pub/Sub triggers allow your scripts or flows to process messages from Google Cloud
|
||||
Pub/Sub in real time. Each trigger listens to a Pub/Sub subscription and executes a script or
|
||||
a flow when new messages are published to the corresponding topic.
|
||||
</Description>
|
||||
|
||||
{#if !newItem && gcpTriggers && gcpTriggers.length > 0}
|
||||
<Section label="GCP Pub/Sub">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each gcpTriggers as gcpTrigger (gcpTrigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{gcpTrigger.path}</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => gcpTriggerEditor?.openEdit(gcpTrigger.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if gcpTrigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<TriggersEditorSection
|
||||
on:applyArgs
|
||||
on:saveTrigger={(e) => {
|
||||
gcpTriggerEditor?.openNew(isFlow, path, e.detail.config)
|
||||
}}
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
cloudDisabled={false}
|
||||
triggerType="gcp"
|
||||
{isFlow}
|
||||
{path}
|
||||
{isEditor}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
{openForm}
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
/>
|
||||
<GcpTriggerEditorInner
|
||||
bind:this={gcpTriggerEditor}
|
||||
useDrawer={false}
|
||||
hideTarget
|
||||
hideTooltips={!isDeployed || cloudDisabled}
|
||||
allowDraft={true}
|
||||
hasDraft={!!selectedTrigger.draftConfig}
|
||||
isDraftOnly={selectedTrigger.isDraft}
|
||||
{customLabel}
|
||||
{isDeployed}
|
||||
{cloudDisabled}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if cloudDisabled}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
GCP Pub/Sub triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/gcp_triggers">
|
||||
GCP Pub/Sub triggers execute scripts and flows in response to messages published to
|
||||
Google Cloud Pub/Sub topics.
|
||||
</Description>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</GcpTriggerEditorInner>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { GcpTriggerService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
|
||||
export async function saveGcpTriggerFromCfg(
|
||||
initialPath: string,
|
||||
cfg: Record<string, any>,
|
||||
edit: boolean,
|
||||
workspace: string,
|
||||
usedTriggerKinds: Writable<string[]>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const requestBody = {
|
||||
gcp_resource_path: cfg.gcp_resource_path,
|
||||
subscription_mode: cfg.subscription_mode,
|
||||
subscription_id: cfg.subscription_id,
|
||||
delivery_type: cfg.delivery_type,
|
||||
delivery_config: cfg.delivery_config,
|
||||
base_endpoint: cfg.base_endpoint,
|
||||
topic_id: cfg.topic_id,
|
||||
path: cfg.path,
|
||||
script_path: cfg.script_path,
|
||||
enabled: cfg.enabled,
|
||||
is_flow: cfg.is_flow
|
||||
}
|
||||
if (edit) {
|
||||
await GcpTriggerService.updateGcpTrigger({
|
||||
workspace,
|
||||
path: initialPath,
|
||||
requestBody
|
||||
})
|
||||
sendUserToast(`GCP Pub/Sub trigger ${cfg.path} updated`)
|
||||
} else {
|
||||
await GcpTriggerService.createGcpTrigger({
|
||||
workspace: workspace,
|
||||
requestBody: {
|
||||
...requestBody,
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
sendUserToast(`GCP Pub/Sub trigger ${cfg.path} created`)
|
||||
}
|
||||
|
||||
if (!get(usedTriggerKinds).includes('gcp')) {
|
||||
usedTriggerKinds.update((t) => [...t, 'gcp'])
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,25 @@
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
export let raw_string: boolean
|
||||
export let wrap_body: boolean
|
||||
interface Props {
|
||||
raw_string: boolean
|
||||
wrap_body: boolean
|
||||
disabled?: boolean
|
||||
testingBadge?: Snippet | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
raw_string = $bindable(),
|
||||
wrap_body = $bindable(),
|
||||
disabled = false,
|
||||
testingBadge = undefined
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Label label="Raw body" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
{#snippet header()}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/http_routing#body-processing-options"
|
||||
>
|
||||
@@ -16,31 +28,39 @@
|
||||
script authentication method and useful for signature verification or other advanced use
|
||||
cases.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
{#if testingBadge}
|
||||
{@render testingBadge()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Toggle
|
||||
checked={raw_string}
|
||||
on:change={() => {
|
||||
raw_string = !raw_string
|
||||
}}
|
||||
{disabled}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</Label>
|
||||
<Label label="Wrap body" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
{#snippet header()}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/http_routing#body-processing-options"
|
||||
>
|
||||
Wraps the payload in an object under the 'body' key, useful for handling unknown payloads.
|
||||
Note that this will have no effect when using a preprocessor.
|
||||
Wraps the body in a JSON object with the key 'body'. Useful for compatibility with existing
|
||||
code that expects a JSON object.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
{#if testingBadge}
|
||||
{@render testingBadge()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Toggle
|
||||
checked={wrap_body}
|
||||
on:change={() => {
|
||||
wrap_body = !wrap_body
|
||||
}}
|
||||
{disabled}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</Label>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import CopyableCodeBlock from '$lib/components/details/CopyableCodeBlock.svelte'
|
||||
import { bash } from 'svelte-highlight/languages'
|
||||
// import { page } from '$app/stores'
|
||||
import { base } from '$lib/base'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
import { isObject } from '$lib/utils'
|
||||
import { Url } from '$lib/components/common'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
interface Props {
|
||||
route_path: string | undefined
|
||||
http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' | undefined
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
runnableArgs?: any
|
||||
isValid?: boolean | undefined
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
captureLoading?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
route_path,
|
||||
http_method,
|
||||
captureInfo = undefined,
|
||||
runnableArgs = {},
|
||||
isValid = undefined,
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
captureLoading = false
|
||||
}: Props = $props()
|
||||
|
||||
let captureURL = $derived(
|
||||
`${location.origin}${base}/api/w/${$workspaceStore}/capture_u/http/${
|
||||
captureInfo?.isFlow ? 'flow' : 'script'
|
||||
}/${captureInfo?.path.replaceAll('/', '.')}/${route_path ?? ''}`
|
||||
)
|
||||
let cleanedRunnableArgs = $derived(
|
||||
isObject(runnableArgs) && 'wm_trigger' in runnableArgs
|
||||
? Object.fromEntries(Object.entries(runnableArgs).filter(([key]) => key !== 'wm_trigger'))
|
||||
: runnableArgs
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="http"
|
||||
disabled={isValid === false}
|
||||
{captureInfo}
|
||||
{captureLoading}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Send a POST request to the URL below to simulate a http event.
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to HTTP requests on this test URL.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<Url url={captureURL} label="Test URL" />
|
||||
|
||||
<Label label="Example cURL" disabled={!captureInfo.active}>
|
||||
<CopyableCodeBlock
|
||||
disabled={!captureInfo.active}
|
||||
code={`curl \\
|
||||
-X ${(http_method ?? 'post').toUpperCase()} ${captureURL} \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '${JSON.stringify(cleanedRunnableArgs ?? {}, null, 2)}'`}
|
||||
language={bash}
|
||||
/>
|
||||
</Label>
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -2,7 +2,13 @@
|
||||
import { tick } from 'svelte'
|
||||
import RouteEditorInner from './RouteEditorInner.svelte'
|
||||
|
||||
let open = false
|
||||
interface Props {
|
||||
onUpdate?: (cfg?: Record<string, any>) => void
|
||||
}
|
||||
|
||||
let { onUpdate = undefined }: Props = $props()
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
open = true
|
||||
await tick()
|
||||
@@ -19,13 +25,9 @@
|
||||
drawer?.openNew(is_flow, initial_script_path, defaultValues)
|
||||
}
|
||||
|
||||
export async function getTriggers() {
|
||||
return drawer?.getTriggers()
|
||||
}
|
||||
|
||||
let drawer: RouteEditorInner
|
||||
let drawer: RouteEditorInner | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<RouteEditorInner on:update bind:this={drawer} />
|
||||
<RouteEditorInner {onUpdate} bind:this={drawer} />
|
||||
{/if}
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Badge } from '$lib/components/common'
|
||||
import { Alert, Url } from '$lib/components/common'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import CopyableCodeBlock from '$lib/components/details/CopyableCodeBlock.svelte'
|
||||
import { bash } from 'svelte-highlight/languages'
|
||||
import { HttpTriggerService } from '$lib/gen'
|
||||
// import { page } from '$app/stores'
|
||||
import { base } from '$lib/base'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import ClipboardPanel from '../../details/ClipboardPanel.svelte'
|
||||
import { isObject } from '$lib/utils'
|
||||
import { getHttpRoute } from './utils'
|
||||
import RouteBodyTransformerOption from './RouteBodyTransformerOption.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
|
||||
export let initialTriggerPath: string | undefined = undefined
|
||||
export let dirtyRoutePath: boolean = false
|
||||
@@ -28,16 +19,12 @@
|
||||
export let can_write: boolean = false
|
||||
export let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
|
||||
undefined
|
||||
export let showCapture = false
|
||||
export let headless: boolean = false
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
export let workspaced_route: boolean = false
|
||||
export let isValid = false
|
||||
export let runnableArgs: any = {}
|
||||
export let raw_string: boolean = false
|
||||
export let wrap_body: boolean = false
|
||||
export let capture_mode: boolean
|
||||
export let isDraftOnly: boolean = true
|
||||
export let showTestingBadge: boolean = false
|
||||
|
||||
let validateTimeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
let routeError: string = ''
|
||||
@@ -84,47 +71,20 @@
|
||||
|
||||
$: !http_method && (http_method = 'post')
|
||||
$: route_path === undefined && (route_path = '')
|
||||
|
||||
$: userIsAdmin = $userStore?.is_admin || $userStore?.is_super_admin
|
||||
$: userCanEditConfig = userIsAdmin || isDraftOnly // User can edit config if they are admin or if the trigger is a draft which will not be saved
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if showCapture && captureInfo}
|
||||
{@const captureURL = `${location.origin}${base}/api/w/${$workspaceStore}/capture_u/http/${
|
||||
captureInfo.isFlow ? 'flow' : 'script'
|
||||
}/${captureInfo.path.replaceAll('/', '.')}/${route_path}`}
|
||||
{@const cleanedRunnableArgs =
|
||||
isObject(runnableArgs) && 'wm_trigger' in runnableArgs
|
||||
? Object.fromEntries(Object.entries(runnableArgs).filter(([key]) => key !== 'wm_trigger'))
|
||||
: runnableArgs}
|
||||
<CaptureSection
|
||||
captureType="http"
|
||||
disabled={!isValid}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
>
|
||||
<Label label="URL">
|
||||
<ClipboardPanel content={captureURL} disabled={!captureInfo.active} />
|
||||
</Label>
|
||||
|
||||
<Label label="Example cUrl">
|
||||
<CopyableCodeBlock
|
||||
disabled={!captureInfo.active}
|
||||
code={`curl \\
|
||||
-X ${(http_method ?? 'post').toUpperCase()} ${captureURL} \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '${JSON.stringify(cleanedRunnableArgs ?? {}, null, 2)}'`}
|
||||
language={bash}
|
||||
/>
|
||||
</Label>
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
<Section label="HTTP" {headless}>
|
||||
{#if !($userStore?.is_admin || $userStore?.is_super_admin)}
|
||||
<Alert type="info" title="Admin only" collapsible>
|
||||
<svelte:fragment slot="header">
|
||||
{#if showTestingBadge}
|
||||
<TestingBadge />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#if !userCanEditConfig && isDraftOnly}
|
||||
<Alert type="info" title="Admin only" collapsible size="xs">
|
||||
Route endpoints can only be edited by workspace admins
|
||||
</Alert>
|
||||
<div class="my-2"></div>
|
||||
@@ -144,7 +104,7 @@
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
bind:value={route_path}
|
||||
disabled={!($userStore?.is_admin || $userStore?.is_super_admin) || !can_write}
|
||||
disabled={!userCanEditConfig || !can_write}
|
||||
class={routeError === ''
|
||||
? ''
|
||||
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
|
||||
@@ -158,44 +118,28 @@
|
||||
<ToggleButtonGroup
|
||||
class="w-auto"
|
||||
bind:selected={http_method}
|
||||
disabled={!($userStore?.is_admin || $userStore?.is_super_admin) ||
|
||||
!can_write ||
|
||||
!!static_asset_config}
|
||||
disabled={!userCanEditConfig || !can_write || !!static_asset_config}
|
||||
let:item
|
||||
let:disabled
|
||||
>
|
||||
<ToggleButton label="GET" value="get" {item} />
|
||||
<ToggleButton label="POST" value="post" {item} />
|
||||
<ToggleButton label="PUT" value="put" {item} />
|
||||
<ToggleButton label="PATCH" value="patch" {item} />
|
||||
<ToggleButton label="DELETE" value="delete" {item} />
|
||||
<ToggleButton label="GET" value="get" {item} {disabled} />
|
||||
<ToggleButton label="POST" value="post" {item} {disabled} />
|
||||
<ToggleButton label="PUT" value="put" {item} {disabled} />
|
||||
<ToggleButton label="PATCH" value="patch" {item} {disabled} />
|
||||
<ToggleButton label="DELETE" value="delete" {item} {disabled} />
|
||||
</ToggleButtonGroup>
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class="flex justify-start w-full">
|
||||
<Badge
|
||||
color="gray"
|
||||
class="center-center !bg-surface-secondary !text-tertiary !w-[90px] !h-[24px] rounded-r-none border"
|
||||
>
|
||||
Full endpoint
|
||||
</Badge>
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value={fullRoute}
|
||||
size={fullRoute.length || 50}
|
||||
class="font-mono !text-xs max-w-[calc(100%-70px)] !w-auto !h-[24px] !py-0 !border-l-0 !rounded-l-none"
|
||||
on:focus={({ currentTarget }) => {
|
||||
currentTarget.select()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<Url url={fullRoute} label="Production URL" />
|
||||
|
||||
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5"
|
||||
>{dirtyRoutePath ? routeError : ''}</div
|
||||
>
|
||||
{#if !capture_mode && !isCloudHosted()}
|
||||
{#if !isCloudHosted()}
|
||||
<div class="mt-1">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={workspaced_route}
|
||||
disabled={!can_write}
|
||||
on:change={() => {
|
||||
workspaced_route = !workspaced_route
|
||||
dirtyRoutePath = true
|
||||
@@ -211,9 +155,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if capture_mode}
|
||||
<RouteBodyTransformerOption bind:raw_string bind:wrap_body />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
import { HttpTriggerService, VariableService, type AuthenticationMethod } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Loader2, Save, Pipette, Plus } from 'lucide-svelte'
|
||||
import { Loader2, Pipette, Plus } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import VariableEditor from '../../VariableEditor.svelte'
|
||||
import { json } from 'svelte-highlight/languages'
|
||||
@@ -27,42 +26,94 @@
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import ItemPicker from '../../ItemPicker.svelte'
|
||||
import { Popover } from '$lib/components/meltComponents'
|
||||
import { HUB_SCRIPT_ID, SECRET_KEY_PATH } from './utils'
|
||||
import { HUB_SCRIPT_ID, saveHttpRouteFromCfg, SECRET_KEY_PATH } from './utils'
|
||||
import { HubFlow } from '$lib/hub'
|
||||
import RouteBodyTransformerOption from './RouteBodyTransformerOption.svelte'
|
||||
let is_flow: boolean = false
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import { handleConfigChange } from '../utils'
|
||||
|
||||
let itemKind: 'flow' | 'script' = 'script'
|
||||
let {
|
||||
useDrawer = true,
|
||||
hideTarget = false,
|
||||
description = undefined,
|
||||
isEditor = false,
|
||||
customLabel = undefined,
|
||||
isDraftOnly = false,
|
||||
allowDraft = false,
|
||||
hasDraft = false,
|
||||
isDeployed = false,
|
||||
onConfigChange = undefined,
|
||||
onCaptureConfigChange = undefined,
|
||||
onUpdate = undefined,
|
||||
onDelete = undefined,
|
||||
onReset = undefined
|
||||
} = $props()
|
||||
|
||||
$: is_flow = itemKind === 'flow'
|
||||
|
||||
let script_path = ''
|
||||
let initialScriptPath = ''
|
||||
let fixedScriptPath = ''
|
||||
let path: string = ''
|
||||
let pathError = ''
|
||||
let isValid = false
|
||||
let dirtyRoutePath = false
|
||||
let is_async = false
|
||||
let authentication_method: AuthenticationMethod = 'none'
|
||||
let route_path = ''
|
||||
let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
|
||||
let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
|
||||
// Form data state
|
||||
let initialPath = $state('')
|
||||
let edit = $state(true)
|
||||
let itemKind = $state<'flow' | 'script'>('script')
|
||||
let is_flow = $state(false)
|
||||
let script_path = $state('')
|
||||
let initialScriptPath = $state('')
|
||||
let fixedScriptPath = $state('')
|
||||
let path = $state('')
|
||||
let pathError = $state('')
|
||||
let isValid = $state(false)
|
||||
let dirtyRoutePath = $state(false)
|
||||
let dirtyPath = $state(false)
|
||||
let is_async = $state(false)
|
||||
let authentication_method = $state<AuthenticationMethod>('none')
|
||||
let route_path = $state('')
|
||||
let http_method = $state<'get' | 'post' | 'put' | 'patch' | 'delete'>('post')
|
||||
let static_asset_config = $state<{ s3: string; storage?: string; filename?: string } | undefined>(
|
||||
undefined
|
||||
let is_static_website: boolean = false
|
||||
let s3FilePicker: S3FilePicker
|
||||
let s3FileUploadRawMode = false
|
||||
let s3Editor: SimpleEditor | undefined = undefined
|
||||
let workspaced_route: boolean = false
|
||||
let raw_string = false
|
||||
let wrap_body = false
|
||||
let drawerLoading = true
|
||||
let authentication_resource_path: string = ''
|
||||
let variablePicker: ItemPicker
|
||||
let variableEditor: VariableEditor
|
||||
let variable_path: string = ''
|
||||
)
|
||||
let is_static_website = $state(false)
|
||||
let s3FileUploadRawMode = $state(false)
|
||||
let workspaced_route = $state(false)
|
||||
let raw_string = $state(false)
|
||||
let wrap_body = $state(false)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoader = $state(false)
|
||||
let authentication_resource_path = $state('')
|
||||
let variable_path = $state('')
|
||||
let signature_options_type = $state<'custom_script' | 'custom_signature'>('custom_signature')
|
||||
let can_write = $state(true)
|
||||
let extraPerms = $state<Record<string, string> | undefined>(undefined)
|
||||
|
||||
// Component references
|
||||
let s3FilePicker = $state<S3FilePicker | null>(null)
|
||||
let s3Editor = $state<SimpleEditor | null>(null)
|
||||
let variablePicker = $state<ItemPicker | null>(null)
|
||||
let variableEditor = $state<VariableEditor | null>(null)
|
||||
let drawer = $state<Drawer | null>(null)
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
let deploymentLoading = $state(false)
|
||||
|
||||
const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
const routeConfig = $derived.by(getRouteConfig)
|
||||
const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({}))
|
||||
const saveDisabled = $derived(
|
||||
drawerLoading ||
|
||||
!can_write ||
|
||||
pathError != '' ||
|
||||
!isValid ||
|
||||
(!static_asset_config && emptyString(script_path))
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
is_flow = itemKind === 'flow'
|
||||
})
|
||||
|
||||
// Update is_static_website based on static_asset_config
|
||||
$effect(() => {
|
||||
if (!static_asset_config) {
|
||||
is_static_website = false
|
||||
}
|
||||
})
|
||||
|
||||
type AuthenticationOption = {
|
||||
label: string
|
||||
value: AuthenticationMethod
|
||||
@@ -74,8 +125,6 @@
|
||||
return await VariableService.listVariable({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
let signature_options_type: 'custom_script' | 'custom_signature' = 'custom_signature'
|
||||
|
||||
const authentication_options: AuthenticationOption[] = [
|
||||
{
|
||||
label: 'No Auth',
|
||||
@@ -108,20 +157,34 @@
|
||||
}
|
||||
]
|
||||
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
export async function openEdit(
|
||||
ePath: string,
|
||||
isFlow: boolean,
|
||||
defaultConfig?: Record<string, any>
|
||||
) {
|
||||
drawerLoading = true
|
||||
let loader = setTimeout(() => {
|
||||
showLoader = true
|
||||
}, 100) // if loading takes less than 100ms, we don't show the loader
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
initialPath = ePath
|
||||
path = ePath
|
||||
itemKind = isFlow ? 'flow' : 'script'
|
||||
edit = true
|
||||
dirtyPath = false
|
||||
dirtyRoutePath = false
|
||||
await loadTrigger()
|
||||
await loadTrigger(defaultConfig)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load route: ${err}`, true)
|
||||
} finally {
|
||||
if (!defaultConfig) {
|
||||
// If the route is loaded from the backend, we to set the initial config
|
||||
initialConfig = structuredClone($state.snapshot(getRouteConfig()))
|
||||
}
|
||||
clearTimeout(loader)
|
||||
drawerLoading = false
|
||||
showLoader = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +194,9 @@
|
||||
defaultValues?: Record<string, any>
|
||||
) {
|
||||
drawerLoading = true
|
||||
let loader = setTimeout(() => {
|
||||
showLoader = true
|
||||
}, 100) // if loading takes less than 100ms, we don't show the loader
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
is_flow = nis_flow
|
||||
@@ -146,7 +212,7 @@
|
||||
script_path = fixedScriptPath
|
||||
static_asset_config = undefined
|
||||
s3FileUploadRawMode = false
|
||||
path = ''
|
||||
path = defaultValues?.path ?? ''
|
||||
initialPath = ''
|
||||
dirtyPath = false
|
||||
is_static_website = false
|
||||
@@ -157,47 +223,73 @@
|
||||
raw_string = defaultValues?.raw_string ?? false
|
||||
wrap_body = defaultValues?.wrap_body ?? false
|
||||
} finally {
|
||||
clearTimeout(loader)
|
||||
drawerLoading = false
|
||||
showLoader = false
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let can_write = true
|
||||
async function loadTrigger(): Promise<void> {
|
||||
const s = await HttpTriggerService.getHttpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
|
||||
script_path = s.script_path
|
||||
initialScriptPath = s.script_path
|
||||
is_flow = s.is_flow
|
||||
path = s.path
|
||||
route_path = s.route_path
|
||||
http_method = s.http_method ?? 'post'
|
||||
is_async = s.is_async
|
||||
workspaced_route = s.workspaced_route
|
||||
wrap_body = s.wrap_body
|
||||
raw_string = s.raw_string
|
||||
authentication_resource_path = s.authentication_resource_path ?? ''
|
||||
if (s.authentication_method === 'custom_script') {
|
||||
function loadTriggerConfig(cfg?: Record<string, any>): void {
|
||||
script_path = cfg?.script_path
|
||||
initialScriptPath = cfg?.script_path
|
||||
is_flow = cfg?.is_flow
|
||||
path = cfg?.path
|
||||
route_path = cfg?.route_path
|
||||
http_method = cfg?.http_method ?? 'post'
|
||||
is_async = cfg?.is_async
|
||||
workspaced_route = cfg?.workspaced_route
|
||||
wrap_body = cfg?.wrap_body
|
||||
raw_string = cfg?.raw_string
|
||||
authentication_resource_path = cfg?.authentication_resource_path ?? ''
|
||||
if (cfg?.authentication_method === 'custom_script') {
|
||||
authentication_method = 'signature'
|
||||
signature_options_type = 'custom_script'
|
||||
} else {
|
||||
authentication_method = s.authentication_method
|
||||
authentication_method = cfg?.authentication_method
|
||||
signature_options_type = 'custom_signature'
|
||||
}
|
||||
if (!isCloudHosted()) {
|
||||
static_asset_config = s.static_asset_config
|
||||
s3FileUploadRawMode = !!static_asset_config
|
||||
is_static_website = s.is_static_website
|
||||
static_asset_config = cfg?.static_asset_config
|
||||
s3FileUploadRawMode = !!cfg?.static_asset_config
|
||||
is_static_website = cfg?.is_static_website
|
||||
}
|
||||
extraPerms = cfg?.extra_perms
|
||||
can_write = canWrite(path, cfg?.extra_perms, $userStore)
|
||||
}
|
||||
|
||||
can_write = canWrite(s.path, s.extra_perms, $userStore)
|
||||
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
|
||||
if (defaultConfig) {
|
||||
loadTriggerConfig(defaultConfig)
|
||||
return
|
||||
} else {
|
||||
const s = await HttpTriggerService.getHttpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
|
||||
loadTriggerConfig(s)
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerScript(): Promise<void> {
|
||||
deploymentLoading = true
|
||||
const saveCfg = routeConfig
|
||||
const isSaved = await saveHttpRouteFromCfg(
|
||||
initialPath,
|
||||
saveCfg,
|
||||
edit,
|
||||
$workspaceStore!,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
onUpdate(saveCfg.path)
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
deploymentLoading = false
|
||||
}
|
||||
|
||||
function getRouteConfig(): Record<string, any> {
|
||||
// If the user selects "signature" with the "custom_script" option,
|
||||
// we explicitly set the authentication method to "custom_script"
|
||||
// (which is a valid enum on its own in the backend)
|
||||
@@ -205,60 +297,47 @@
|
||||
authentication_method === 'signature' && signature_options_type === 'custom_script'
|
||||
? 'custom_script'
|
||||
: authentication_method
|
||||
|
||||
if (edit) {
|
||||
await HttpTriggerService.updateHttpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
is_async,
|
||||
authentication_method: auth_method,
|
||||
route_path: $userStore?.is_admin || $userStore?.is_super_admin ? route_path : undefined,
|
||||
http_method,
|
||||
static_asset_config,
|
||||
is_static_website,
|
||||
workspaced_route,
|
||||
authentication_resource_path,
|
||||
wrap_body,
|
||||
raw_string
|
||||
}
|
||||
})
|
||||
sendUserToast(`Route ${path} updated`)
|
||||
} else {
|
||||
await HttpTriggerService.createHttpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
is_async,
|
||||
authentication_method: auth_method,
|
||||
route_path,
|
||||
http_method,
|
||||
static_asset_config,
|
||||
is_static_website,
|
||||
workspaced_route,
|
||||
authentication_resource_path,
|
||||
wrap_body,
|
||||
raw_string
|
||||
}
|
||||
})
|
||||
sendUserToast(`Route ${path} created`)
|
||||
const nCfg = {
|
||||
script_path,
|
||||
is_flow,
|
||||
path,
|
||||
route_path,
|
||||
http_method,
|
||||
is_async,
|
||||
workspaced_route,
|
||||
wrap_body,
|
||||
raw_string,
|
||||
authentication_resource_path,
|
||||
authentication_method: auth_method,
|
||||
static_asset_config,
|
||||
is_static_website,
|
||||
extra_perms: extraPerms
|
||||
}
|
||||
if (!$usedTriggerKinds.includes('http')) {
|
||||
$usedTriggerKinds = [...$usedTriggerKinds, 'http']
|
||||
}
|
||||
dispatch('update')
|
||||
drawer.closeDrawer()
|
||||
return nCfg
|
||||
}
|
||||
let drawer: Drawer
|
||||
|
||||
let dirtyPath = false
|
||||
// Update config for captures
|
||||
function getCaptureConfig() {
|
||||
const newCaptureConfig = {
|
||||
route_path: routeConfig.route_path,
|
||||
http_method: routeConfig.http_method,
|
||||
raw_string: routeConfig.raw_string,
|
||||
wrap_body: routeConfig.wrap_body,
|
||||
path: routeConfig.path
|
||||
}
|
||||
//
|
||||
return newCaptureConfig
|
||||
}
|
||||
|
||||
$: !static_asset_config && (is_static_website = false)
|
||||
$effect(() => {
|
||||
onCaptureConfigChange?.(captureConfig, isValid)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!drawerLoading) {
|
||||
handleConfigChange(routeConfig, initialConfig, saveDisabled, edit, onConfigChange)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if static_asset_config}
|
||||
@@ -274,46 +353,30 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Drawer size="700px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit ? (can_write ? `Edit route ${initialPath}` : `Route ${initialPath}`) : 'New route'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !drawerLoading && can_write}
|
||||
<Button
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={pathError != '' ||
|
||||
!isValid ||
|
||||
(!static_asset_config && emptyString(script_path)) ||
|
||||
(static_asset_config && emptyString(static_asset_config.s3)) ||
|
||||
!can_write}
|
||||
on:click={triggerScript}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#if drawerLoading}
|
||||
{#snippet config()}
|
||||
{#if drawerLoading}
|
||||
{#if showLoader}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
<div class="flex flex-col gap-12">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="route"
|
||||
kind="http_trigger"
|
||||
hideUser
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col gap-12">
|
||||
<Section label="Metadata">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="route"
|
||||
kind="http_trigger"
|
||||
hideUser
|
||||
disableEditing={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</Section>
|
||||
|
||||
{#if !hideTarget}
|
||||
<Section label="Target">
|
||||
{#if !isCloudHosted()}
|
||||
<ToggleButtonGroup
|
||||
@@ -341,10 +404,11 @@
|
||||
}
|
||||
}}
|
||||
let:item
|
||||
let:disabled
|
||||
>
|
||||
<ToggleButton label="Runnable" value="runnable" {item} />
|
||||
<ToggleButton label="Static asset" value="static_asset" {item} />
|
||||
<ToggleButton label="Static website" value="static_website" {item} />
|
||||
<ToggleButton label="Runnable" value="runnable" {item} {disabled} />
|
||||
<ToggleButton label="Static asset" value="static_asset" {item} {disabled} />
|
||||
<ToggleButton label="Static website" value="static_website" {item} {disabled} />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
|
||||
@@ -371,13 +435,7 @@
|
||||
{#if s3FileUploadRawMode}
|
||||
{#if can_write}
|
||||
<JsonEditor
|
||||
bind:editor={s3Editor}
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
bind:editor={s3Editor as any}
|
||||
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
|
||||
bind:value={static_asset_config}
|
||||
/>
|
||||
@@ -456,183 +514,257 @@
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<RouteEditorConfigSection
|
||||
initialTriggerPath={initialPath}
|
||||
bind:route_path
|
||||
bind:isValid
|
||||
bind:dirtyRoutePath
|
||||
bind:http_method
|
||||
bind:workspaced_route
|
||||
{can_write}
|
||||
capture_mode={false}
|
||||
bind:static_asset_config
|
||||
/>
|
||||
<RouteEditorConfigSection
|
||||
initialTriggerPath={initialPath}
|
||||
bind:route_path
|
||||
bind:isValid
|
||||
bind:dirtyRoutePath
|
||||
bind:http_method
|
||||
bind:workspaced_route
|
||||
{can_write}
|
||||
bind:static_asset_config
|
||||
showTestingBadge={isEditor}
|
||||
{isDraftOnly}
|
||||
/>
|
||||
|
||||
{#if !is_static_website}
|
||||
<Section label="Advanced">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if !static_asset_config}
|
||||
<div class="flex flex-row justify-between">
|
||||
<Label label="Request type" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
selected={is_async ? 'async' : 'sync'}
|
||||
on:selected={({ detail }) => {
|
||||
is_async = detail === 'async'
|
||||
}}
|
||||
disabled={!can_write || !!static_asset_config}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
label="Async"
|
||||
value="async"
|
||||
tooltip="The returning value is the uuid of the job assigned to execute the job."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Sync"
|
||||
value="sync"
|
||||
tooltip="Triggers the execution, wait for the job to complete and return it as a response."
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
{#if !is_static_website}
|
||||
<Section label="Advanced" collapsable>
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if !static_asset_config}
|
||||
<div class="flex flex-row justify-between">
|
||||
<Label label="Request type" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
selected={is_async ? 'async' : 'sync'}
|
||||
on:selected={({ detail }) => {
|
||||
is_async = detail === 'async'
|
||||
}}
|
||||
disabled={!can_write || !!static_asset_config}
|
||||
let:item
|
||||
let:disabled
|
||||
>
|
||||
<ToggleButton
|
||||
label="Async"
|
||||
value="async"
|
||||
tooltip="The returning value is the uuid of the job assigned to execute the job."
|
||||
{item}
|
||||
{disabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Sync"
|
||||
value="sync"
|
||||
tooltip="Triggers the execution, wait for the job to complete and return it as a response."
|
||||
{item}
|
||||
{disabled}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<Label label="Authentication" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={authentication_method}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'signature' && signature_options_type === 'custom_script') {
|
||||
raw_string = true
|
||||
}
|
||||
}}
|
||||
disabled={!can_write}
|
||||
let:item
|
||||
let:disabled
|
||||
>
|
||||
{#each authentication_options as option}
|
||||
{#if option.value === 'signature'}
|
||||
<Popover placement="top-end" usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger">
|
||||
<ToggleButton
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
tooltip={option.tooltip}
|
||||
{item}
|
||||
{disabled}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={signature_options_type}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'custom_script') {
|
||||
if (!raw_string) {
|
||||
raw_string = true
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={!can_write}
|
||||
let:item
|
||||
let:disabled
|
||||
>
|
||||
<ToggleButton
|
||||
label="Signature validation"
|
||||
value="custom_signature"
|
||||
tooltip="Use a predefined or custom signature-based authentication scheme"
|
||||
{item}
|
||||
{disabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Custom script"
|
||||
value="custom_script"
|
||||
tooltip="Use your own script logic"
|
||||
{item}
|
||||
{disabled}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{:else}
|
||||
<ToggleButton
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
tooltip={option.tooltip}
|
||||
{item}
|
||||
{disabled}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
|
||||
{#each authentication_options as option}
|
||||
{#if option.resource_type && authentication_method === option.value}
|
||||
<ResourcePicker
|
||||
bind:value={authentication_resource_path}
|
||||
resourceType={option.resource_type}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if authentication_method === 'signature'}
|
||||
{#if signature_options_type === 'custom_signature'}
|
||||
<ResourcePicker
|
||||
bind:value={authentication_resource_path}
|
||||
resourceType={'signature_auth'}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{:else if signature_options_type === 'custom_script'}
|
||||
<p class="text-xs mt-3 mb-1 text-tertiary">
|
||||
Pick a secret variable or create one which will be used as a secret key for your
|
||||
custom script/flow<Required required={true} /><br />
|
||||
</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
bind:value={variable_path}
|
||||
readonly
|
||||
disabled={true}
|
||||
/>
|
||||
<Button
|
||||
title="Add variable"
|
||||
on:click={() => variablePicker?.openDrawer()}
|
||||
size="xs"
|
||||
color="dark"
|
||||
disabled={!can_write}
|
||||
>
|
||||
Pick variable
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
disabled={emptyString(variable_path) || !can_write}
|
||||
color="dark"
|
||||
size="xs"
|
||||
href={itemKind === 'flow'
|
||||
? `/flows/add?${SECRET_KEY_PATH}=${encodeURIComponent(variable_path)}&hub=${
|
||||
HubFlow.SIGNATURE_TEMPLATE
|
||||
}`
|
||||
: `/scripts/add?${SECRET_KEY_PATH}=${encodeURIComponent(
|
||||
variable_path
|
||||
)}&hub=hub%2F${HUB_SCRIPT_ID}`}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<Label label="Authentication" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={authentication_method}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'signature' && signature_options_type === 'custom_script') {
|
||||
raw_string = true
|
||||
}
|
||||
}}
|
||||
disabled={!can_write}
|
||||
let:item
|
||||
>
|
||||
{#each authentication_options as option}
|
||||
{#if option.value === 'signature'}
|
||||
<Popover placement="top-end" usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger">
|
||||
<ToggleButton
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
tooltip={option.tooltip}
|
||||
{item}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={signature_options_type}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'custom_script') {
|
||||
if (!raw_string) {
|
||||
raw_string = true
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={!can_write}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
label="Signature validation"
|
||||
value="custom_signature"
|
||||
tooltip="Use a predefined or custom signature-based authentication scheme"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Custom script"
|
||||
value="custom_script"
|
||||
tooltip="Use your own script logic"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{:else}
|
||||
<ToggleButton
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
tooltip={option.tooltip}
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
{#each authentication_options as option}
|
||||
{#if option.resource_type && authentication_method === option.value}
|
||||
<ResourcePicker
|
||||
bind:value={authentication_resource_path}
|
||||
resourceType={option.resource_type}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
<RouteBodyTransformerOption
|
||||
bind:raw_string
|
||||
bind:wrap_body
|
||||
disabled={!can_write}
|
||||
{testingBadge}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if authentication_method === 'signature'}
|
||||
{#if signature_options_type === 'custom_signature'}
|
||||
<ResourcePicker
|
||||
bind:value={authentication_resource_path}
|
||||
resourceType={'signature_auth'}
|
||||
/>
|
||||
{:else if signature_options_type === 'custom_script'}
|
||||
<p class="text-xs mt-3 mb-1 text-tertiary">
|
||||
Pick a secret variable or create one which will be used as a secret key for your
|
||||
custom script/flow<Required required={true} /><br />
|
||||
</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
bind:value={variable_path}
|
||||
readonly
|
||||
disabled={true}
|
||||
/>
|
||||
<Button
|
||||
title="Add variable"
|
||||
on:click={variablePicker.openDrawer}
|
||||
size="xs"
|
||||
color="dark"
|
||||
>
|
||||
Pick variable
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
disabled={emptyString(variable_path)}
|
||||
color="dark"
|
||||
size="xs"
|
||||
href={itemKind === 'flow'
|
||||
? `/flows/add?${SECRET_KEY_PATH}=${encodeURIComponent(variable_path)}&hub=${
|
||||
HubFlow.SIGNATURE_TEMPLATE
|
||||
}`
|
||||
: `/scripts/add?${SECRET_KEY_PATH}=${encodeURIComponent(
|
||||
variable_path
|
||||
)}&hub=hub%2F${HUB_SCRIPT_ID}`}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#snippet testingBadge()}
|
||||
{#if isEditor}
|
||||
<TestingBadge />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if !static_asset_config}
|
||||
<RouteBodyTransformerOption bind:raw_string bind:wrap_body />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
{#snippet saveButton()}
|
||||
{#if !drawerLoading}
|
||||
<TriggerEditorToolbar
|
||||
{isDraftOnly}
|
||||
{hasDraft}
|
||||
permissions={drawerLoading || !can_write ? 'none' : can_write && isAdmin ? 'create' : 'write'}
|
||||
{saveDisabled}
|
||||
enabled={undefined}
|
||||
{allowDraft}
|
||||
{edit}
|
||||
isLoading={deploymentLoading}
|
||||
onUpdate={triggerScript}
|
||||
{onReset}
|
||||
{onDelete}
|
||||
{isDeployed}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if useDrawer}
|
||||
<Drawer size="700px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit route ${initialPath}`
|
||||
: `Route ${initialPath}`
|
||||
: 'New route'}
|
||||
on:close={() => drawer?.closeDrawer()}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{@render saveButton()}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else}
|
||||
<Section label={!customLabel ? 'HTTP Route' : ''} headerClass="grow min-w-0 h-[30px]">
|
||||
<svelte:fragment slot="header">
|
||||
{#if customLabel}
|
||||
{@render customLabel()}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
{@render saveButton()}
|
||||
</svelte:fragment>
|
||||
{#if description}
|
||||
{@render description()}
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{@render config()}
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<ItemPicker
|
||||
bind:this={variablePicker}
|
||||
@@ -644,7 +776,7 @@
|
||||
itemName="Variable"
|
||||
extraField="path"
|
||||
loadItems={loadVariables}
|
||||
buttons={{ 'Edit/View': (x) => variableEditor.editVariable(x) }}
|
||||
buttons={{ 'Edit/View': (x) => variableEditor?.editVariable(x) }}
|
||||
>
|
||||
<div slot="submission" class="flex flex-row">
|
||||
<Button
|
||||
@@ -653,7 +785,7 @@
|
||||
size="sm"
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => {
|
||||
variableEditor.initNew()
|
||||
variableEditor?.initNew()
|
||||
}}
|
||||
>
|
||||
New variable
|
||||
@@ -661,4 +793,4 @@
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openDrawer} />
|
||||
<VariableEditor bind:this={variableEditor} on:create={(e) => variablePicker?.openDrawer()} />
|
||||
|
||||
@@ -1,123 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { HttpTriggerService, type HttpTrigger } from '$lib/gen'
|
||||
import RouteEditor from './RouteEditor.svelte'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import RouteEditorInner from './RouteEditorInner.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersEditorSection from '../TriggersEditorSection.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let newItem: boolean = false
|
||||
export let isEditor: boolean = false
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
export let args: Record<string, any> = {}
|
||||
let routeEditor = $state<RouteEditorInner | null>(null)
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
defaultValues = undefined,
|
||||
isEditor = false,
|
||||
customLabel = undefined,
|
||||
...restProps
|
||||
} = $props()
|
||||
|
||||
let routeEditor: RouteEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
|
||||
$: path && loadTriggers()
|
||||
const { triggersCount, selectedTrigger, defaultValues } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
onMount(() => {
|
||||
if (
|
||||
defaultValues &&
|
||||
$selectedTrigger === 'routes' &&
|
||||
Object.keys($defaultValues ?? {}).length > 0
|
||||
) {
|
||||
routeEditor.openNew(isFlow, path, $defaultValues)
|
||||
defaultValues.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
let httpTriggers: (HttpTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
export async function loadTriggers() {
|
||||
try {
|
||||
httpTriggers = (
|
||||
await HttpTriggerService.listHttpTriggers({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
isFlow
|
||||
})
|
||||
).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), http_routes_count: httpTriggers?.length }
|
||||
openForm = httpTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load http routes', e)
|
||||
async function openRouteEditor(isFlow: boolean, isDraft: boolean) {
|
||||
if (isDraft) {
|
||||
routeEditor?.openNew(isFlow, path, defaultValues)
|
||||
} else {
|
||||
routeEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (routeEditor) {
|
||||
openRouteEditor(isFlow, selectedTrigger.isDraft ?? false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<RouteEditor
|
||||
on:update={() => {
|
||||
loadTriggers()
|
||||
}}
|
||||
<RouteEditorInner
|
||||
useDrawer={false}
|
||||
bind:this={routeEditor}
|
||||
/>
|
||||
hideTarget
|
||||
{isEditor}
|
||||
{customLabel}
|
||||
isDraftOnly={selectedTrigger.isDraft}
|
||||
allowDraft
|
||||
hasDraft={!!selectedTrigger.draftConfig}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet description()}
|
||||
<div class="flex flex-col gap-2 pb-4">
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/http_routing"
|
||||
>Routes expose your scripts and flows as HTTP endpoints. Each route can be configured with a
|
||||
specific HTTP method and path.</Description
|
||||
>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/http_routing">
|
||||
Routes expose your scripts and flows as HTTP endpoints. Each route can be configured with a
|
||||
specific HTTP method and path.
|
||||
</Description>
|
||||
{#if !newItem && httpTriggers && httpTriggers.length > 0}
|
||||
<Section label="Routes">
|
||||
{#if !$userStore?.is_admin && !$userStore?.is_super_admin}
|
||||
<Alert title="Only workspace admins can create routes" type="warning" size="xs" />
|
||||
{#if !$userStore?.is_admin && !$userStore?.is_super_admin && selectedTrigger.isDraft}
|
||||
<Alert title="Only workspace admins can create routes" type="info" size="xs" />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each httpTriggers as httpTriggers (httpTriggers.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{httpTriggers.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{httpTriggers.http_method.toUpperCase()} /{httpTriggers.route_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => routeEditor?.openEdit(httpTriggers.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if httpTriggers.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<TriggersEditorSection
|
||||
on:saveTrigger={(e) => {
|
||||
routeEditor?.openNew(isFlow, path, e.detail.config)
|
||||
}}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
cloudDisabled={false}
|
||||
triggerType="http"
|
||||
{isFlow}
|
||||
{path}
|
||||
{isEditor}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
data={{ args }}
|
||||
bind:openForm
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</RouteEditorInner>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { base } from '$lib/base'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { HttpTriggerService } from '$lib/gen/services.gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
export const SECRET_KEY_PATH = 'secret_key_path'
|
||||
export const HUB_SCRIPT_ID = 19670
|
||||
@@ -25,3 +29,50 @@ export function replacePlaceholderForSignatureScriptTemplate(content: string) {
|
||||
`$1${secret_key_path}$2`
|
||||
)
|
||||
}
|
||||
|
||||
export async function saveHttpRouteFromCfg(
|
||||
initialPath: string,
|
||||
routeCfg: Record<string, any>,
|
||||
edit: boolean,
|
||||
workspace: string,
|
||||
isAdmin: boolean,
|
||||
usedTriggerKinds: Writable<string[]>
|
||||
): Promise<boolean> {
|
||||
const requestBody = {
|
||||
path: routeCfg.path,
|
||||
script_path: routeCfg.script_path,
|
||||
is_flow: routeCfg.is_flow,
|
||||
is_async: routeCfg.is_async,
|
||||
authentication_method: routeCfg.authentication_method,
|
||||
route_path: isAdmin || !edit ? routeCfg.route_path : undefined,
|
||||
http_method: routeCfg.http_method,
|
||||
is_static_website: routeCfg.is_static_website,
|
||||
workspaced_route: routeCfg.workspaced_route,
|
||||
authentication_resource_path: routeCfg.authentication_resource_path,
|
||||
wrap_body: routeCfg.wrap_body,
|
||||
raw_string: routeCfg.raw_string
|
||||
}
|
||||
try {
|
||||
if (edit) {
|
||||
await HttpTriggerService.updateHttpTrigger({
|
||||
workspace: workspace,
|
||||
path: initialPath,
|
||||
requestBody: requestBody
|
||||
})
|
||||
sendUserToast(`Route ${routeCfg.path} updated`)
|
||||
} else {
|
||||
await HttpTriggerService.createHttpTrigger({
|
||||
workspace: workspace,
|
||||
requestBody: requestBody
|
||||
})
|
||||
sendUserToast(`Route ${routeCfg.path} created`)
|
||||
}
|
||||
if (!get(usedTriggerKinds).includes('http')) {
|
||||
usedTriggerKinds.update((t) => [...t, 'http'])
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
|
||||
interface Props {
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
isValid?: boolean | undefined
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
captureLoading?: boolean
|
||||
triggerDeployed?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
captureInfo = undefined,
|
||||
isValid = undefined,
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
captureLoading = false,
|
||||
triggerDeployed = false
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="kafka"
|
||||
disabled={isValid === false}
|
||||
{captureInfo}
|
||||
{captureLoading}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
displayAlert={triggerDeployed}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
{#if captureInfo.connectionInfo?.connected}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Listening to Kafka events...
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Connecting to kafka...
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to Kafka events.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -2,7 +2,9 @@
|
||||
import { tick } from 'svelte'
|
||||
import KafkaTriggerEditorInner from './KafkaTriggerEditorInner.svelte'
|
||||
|
||||
let open = false
|
||||
let { onUpdate }: { onUpdate?: (path?: string) => void } = $props()
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
open = true
|
||||
await tick()
|
||||
@@ -19,9 +21,9 @@
|
||||
drawer?.openNew(is_flow, initial_script_path, defaultValues)
|
||||
}
|
||||
|
||||
let drawer: KafkaTriggerEditorInner
|
||||
let drawer: KafkaTriggerEditorInner | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<KafkaTriggerEditorInner on:update bind:this={drawer} />
|
||||
<KafkaTriggerEditorInner {onUpdate} bind:this={drawer} />
|
||||
{/if}
|
||||
|
||||
@@ -8,35 +8,107 @@
|
||||
import { KafkaTriggerService } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Loader2, Save } from 'lucide-svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import KafkaTriggersConfigSection from './KafkaTriggersConfigSection.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import { saveKafkaTriggerFromCfg } from './utils'
|
||||
import { handleConfigChange } from '../utils'
|
||||
|
||||
let drawer: Drawer
|
||||
let is_flow: boolean = false
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
let itemKind: 'flow' | 'script' = 'script'
|
||||
let script_path = ''
|
||||
let initialScriptPath = ''
|
||||
let fixedScriptPath = ''
|
||||
let path: string = ''
|
||||
let pathError = ''
|
||||
let enabled = false
|
||||
let dirtyPath = false
|
||||
let can_write = true
|
||||
let drawerLoading = true
|
||||
let defaultValues: Record<string, any> | undefined = undefined
|
||||
let args: Record<string, any> = {}
|
||||
interface Props {
|
||||
useDrawer?: boolean
|
||||
description?: Snippet | undefined
|
||||
hideTarget?: boolean
|
||||
hideTooltips?: boolean
|
||||
isEditor?: boolean
|
||||
allowDraft?: boolean
|
||||
hasDraft?: boolean
|
||||
isDraftOnly?: boolean
|
||||
customLabel?: Snippet
|
||||
isDeployed?: boolean
|
||||
cloudDisabled?: boolean
|
||||
onConfigChange?: (cfg: Record<string, any>, saveDisabled: boolean, updated: boolean) => void
|
||||
onCaptureConfigChange?: (cfg: Record<string, any>, isValid: boolean) => void
|
||||
onUpdate?: (path?: string) => void
|
||||
onDelete?: () => void
|
||||
onReset?: () => void
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let {
|
||||
useDrawer = true,
|
||||
description = undefined,
|
||||
hideTarget = false,
|
||||
hideTooltips = false,
|
||||
isEditor = false,
|
||||
allowDraft = false,
|
||||
hasDraft = false,
|
||||
isDraftOnly = false,
|
||||
customLabel,
|
||||
isDeployed = false,
|
||||
cloudDisabled = false,
|
||||
onConfigChange = undefined,
|
||||
onCaptureConfigChange = undefined,
|
||||
onUpdate = undefined,
|
||||
onDelete = undefined,
|
||||
onReset = undefined
|
||||
}: Props = $props()
|
||||
|
||||
$: is_flow = itemKind === 'flow'
|
||||
let drawer: Drawer | undefined = $state()
|
||||
let is_flow: boolean = $state(false)
|
||||
let initialPath = $state('')
|
||||
let edit = $state(true)
|
||||
let itemKind: 'flow' | 'script' = $state('script')
|
||||
let script_path = $state('')
|
||||
let initialScriptPath = $state('')
|
||||
let fixedScriptPath = $state('')
|
||||
let path: string = $state('')
|
||||
let pathError = $state('')
|
||||
let enabled = $state(false)
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
let extra_perms = $state<Record<string, any> | undefined>(undefined)
|
||||
let kafkaCfgValid = $state(false)
|
||||
let kafkaResourcePath = $state('')
|
||||
let kafkaCfg: Record<string, any> = $state({})
|
||||
let deploymentLoading = $state(false)
|
||||
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
const isValid = $derived(
|
||||
!!kafkaResourcePath &&
|
||||
kafkaCfgValid &&
|
||||
kafkaCfg.topics &&
|
||||
kafkaCfg.topics.length > 0 &&
|
||||
kafkaCfg.topics.every((b) => /^[a-zA-Z0-9-_.]+$/.test(b))
|
||||
)
|
||||
const saveDisabled = $derived(
|
||||
pathError !== '' ||
|
||||
!isValid ||
|
||||
drawerLoading ||
|
||||
!can_write ||
|
||||
emptyString(script_path) ||
|
||||
emptyString(kafkaResourcePath) ||
|
||||
kafkaCfg.topics.length === 0 ||
|
||||
kafkaCfg.topics.some((t) => emptyString(t))
|
||||
)
|
||||
const kafkaConfig = $derived.by(getSaveCfg)
|
||||
const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({}))
|
||||
|
||||
$effect(() => {
|
||||
is_flow = itemKind === 'flow'
|
||||
})
|
||||
|
||||
export async function openEdit(
|
||||
ePath: string,
|
||||
isFlow: boolean,
|
||||
defaultConfig?: Record<string, any>
|
||||
) {
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
@@ -44,11 +116,16 @@
|
||||
itemKind = isFlow ? 'flow' : 'script'
|
||||
edit = true
|
||||
dirtyPath = false
|
||||
await loadTrigger()
|
||||
await loadTrigger(defaultConfig)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load Kafka trigger: ${err}`, true)
|
||||
} finally {
|
||||
if (!defaultConfig) {
|
||||
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
}
|
||||
clearTimeout(loadingTimeout)
|
||||
drawerLoading = false
|
||||
showLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,164 +134,209 @@
|
||||
fixedScriptPath_?: string,
|
||||
nDefaultValues?: Record<string, any>
|
||||
) {
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
is_flow = nis_flow
|
||||
edit = false
|
||||
itemKind = nis_flow ? 'flow' : 'script'
|
||||
args.kafka_resource_path = nDefaultValues?.kafka_resource_path ?? ''
|
||||
args.group_id = nDefaultValues?.group_id ?? ''
|
||||
args.topics = nDefaultValues?.topics ?? ['']
|
||||
kafkaResourcePath = nDefaultValues?.kafka_resource_path ?? ''
|
||||
kafkaCfg = {
|
||||
group_id: nDefaultValues?.group_id ?? '',
|
||||
topics: nDefaultValues?.topics ?? ['']
|
||||
}
|
||||
initialScriptPath = ''
|
||||
fixedScriptPath = fixedScriptPath_ ?? ''
|
||||
script_path = fixedScriptPath
|
||||
path = ''
|
||||
path = nDefaultValues?.path ?? ''
|
||||
initialPath = ''
|
||||
dirtyPath = false
|
||||
defaultValues = nDefaultValues
|
||||
} finally {
|
||||
clearTimeout(loadingTimeout)
|
||||
drawerLoading = false
|
||||
showLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrigger(): Promise<void> {
|
||||
const s = await KafkaTriggerService.getKafkaTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
script_path = s.script_path
|
||||
initialScriptPath = s.script_path
|
||||
function loadTriggerConfig(cfg?: Record<string, any>): void {
|
||||
script_path = cfg?.script_path
|
||||
initialScriptPath = cfg?.script_path
|
||||
is_flow = cfg?.is_flow
|
||||
path = cfg?.path
|
||||
kafkaResourcePath = cfg?.kafka_resource_path
|
||||
kafkaCfg = {
|
||||
group_id: cfg?.group_id,
|
||||
topics: cfg?.topics
|
||||
}
|
||||
enabled = cfg?.enabled
|
||||
extra_perms = cfg?.extra_perms
|
||||
can_write = canWrite(path, cfg?.extra_perms, $userStore)
|
||||
}
|
||||
|
||||
is_flow = s.is_flow
|
||||
path = s.path
|
||||
args.kafka_resource_path = s.kafka_resource_path
|
||||
args.group_id = s.group_id
|
||||
args.topics = s.topics
|
||||
enabled = s.enabled
|
||||
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
|
||||
if (defaultConfig) {
|
||||
loadTriggerConfig(defaultConfig)
|
||||
return
|
||||
} else {
|
||||
const s = await KafkaTriggerService.getKafkaTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
loadTriggerConfig(s)
|
||||
}
|
||||
}
|
||||
|
||||
can_write = canWrite(s.path, s.extra_perms, $userStore)
|
||||
function getSaveCfg(): Record<string, any> {
|
||||
return {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
kafka_resource_path: kafkaResourcePath,
|
||||
group_id: kafkaCfg.group_id,
|
||||
topics: kafkaCfg.topics,
|
||||
enabled,
|
||||
extra_perms: extra_perms
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTrigger(): Promise<void> {
|
||||
if (edit) {
|
||||
await KafkaTriggerService.updateKafkaTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
kafka_resource_path: args.kafka_resource_path,
|
||||
group_id: args.group_id,
|
||||
topics: args.topics
|
||||
}
|
||||
})
|
||||
sendUserToast(`Kafka trigger ${path} updated`)
|
||||
} else {
|
||||
await KafkaTriggerService.createKafkaTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
enabled: true,
|
||||
kafka_resource_path: args.kafka_resource_path,
|
||||
group_id: args.group_id,
|
||||
topics: args.topics
|
||||
}
|
||||
})
|
||||
sendUserToast(`Kafka trigger ${path} created`)
|
||||
}
|
||||
if (!$usedTriggerKinds.includes('kafka')) {
|
||||
$usedTriggerKinds = [...$usedTriggerKinds, 'kafka']
|
||||
}
|
||||
dispatch('update')
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
|
||||
function useDefaultValues() {
|
||||
if (args.kafka_resource_path && args.kafka_resource_path != '') {
|
||||
return false
|
||||
}
|
||||
if (!defaultValues) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
defaultValues.brokers &&
|
||||
defaultValues.brokers.length > 0 &&
|
||||
defaultValues.brokers.some((broker: string) => broker.trim() !== '')
|
||||
deploymentLoading = true
|
||||
const cfg = getSaveCfg()
|
||||
const isSaved = await saveKafkaTriggerFromCfg(
|
||||
initialPath,
|
||||
cfg,
|
||||
edit,
|
||||
$workspaceStore!,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
onUpdate?.(cfg.path)
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
deploymentLoading = false
|
||||
}
|
||||
|
||||
let isValid = false
|
||||
function getCaptureConfig(): Record<string, any> {
|
||||
return {
|
||||
kafka_resource_path: kafkaResourcePath,
|
||||
group_id: kafkaCfg.group_id,
|
||||
topics: structuredClone($state.snapshot(kafkaCfg.topics)),
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleEnabled(nEnabled: boolean) {
|
||||
await KafkaTriggerService.setKafkaTriggerEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: nEnabled }
|
||||
})
|
||||
sendUserToast(`${nEnabled ? 'enabled' : 'disabled'} Kafka trigger ${initialPath}`)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onCaptureConfigChange?.(captureConfig, isValid)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!drawerLoading) {
|
||||
handleConfigChange(kafkaConfig, initialConfig, saveDisabled, edit, onConfigChange)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit Kafka trigger ${initialPath}`
|
||||
: `Kafka trigger ${initialPath}`
|
||||
: 'New Kafka trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !drawerLoading}
|
||||
{#if edit}
|
||||
<div class="mr-8 center-center -mt-1">
|
||||
<Toggle
|
||||
disabled={!can_write}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
on:change={async (e) => {
|
||||
await KafkaTriggerService.setKafkaTriggerEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} Kafka trigger ${initialPath}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if can_write}
|
||||
<Button
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={pathError != '' || emptyString(script_path) || !can_write || !isValid}
|
||||
on:click={updateTrigger}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{/if}
|
||||
{#if useDrawer}
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit Kafka trigger ${initialPath}`
|
||||
: `Kafka trigger ${initialPath}`
|
||||
: 'New Kafka trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{@render actionsButtons('sm')}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else}
|
||||
<Section label={!customLabel ? 'Kafka trigger' : ''} headerClass="grow min-w-0 h-[30px]">
|
||||
<svelte:fragment slot="header">
|
||||
{#if customLabel}
|
||||
{@render customLabel()}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#if drawerLoading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
<Alert title="Info" type="info">
|
||||
{#if edit}
|
||||
Changes can take up to 30 seconds to take effect.
|
||||
{:else}
|
||||
Kafka consumers can take up to 30 seconds to start.
|
||||
{/if}
|
||||
</Alert>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="kafka_trigger"
|
||||
kind="kafka_trigger"
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<svelte:fragment slot="action">
|
||||
{@render actionsButtons('xs')}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
{#snippet actionsButtons(size: 'xs' | 'sm' = 'sm')}
|
||||
{#if !drawerLoading}
|
||||
<TriggerEditorToolbar
|
||||
{isDraftOnly}
|
||||
{hasDraft}
|
||||
permissions={drawerLoading || !can_write ? 'none' : 'create'}
|
||||
{enabled}
|
||||
{allowDraft}
|
||||
{edit}
|
||||
isLoading={deploymentLoading}
|
||||
{isDeployed}
|
||||
{saveDisabled}
|
||||
onUpdate={updateTrigger}
|
||||
{onReset}
|
||||
{onDelete}
|
||||
onToggleEnabled={handleToggleEnabled}
|
||||
{cloudDisabled}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet config()}
|
||||
{#if drawerLoading}
|
||||
{#if showLoading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if description}
|
||||
{@render description()}
|
||||
{/if}
|
||||
{#if !hideTooltips}
|
||||
<Alert title="Info" type="info" size="xs">
|
||||
{#if edit}
|
||||
Changes can take up to 30 seconds to take effect.
|
||||
{:else}
|
||||
Kafka consumers can take up to 30 seconds to start.
|
||||
{/if}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="kafka_trigger"
|
||||
kind="kafka_trigger"
|
||||
disabled={!can_write}
|
||||
disableEditing={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{#if !hideTarget}
|
||||
<Section label="Runnable">
|
||||
<p class="text-xs mb-1 text-tertiary">
|
||||
Pick a script or flow to be triggered<Required required={true} />
|
||||
@@ -235,20 +357,23 @@
|
||||
btnClasses="ml-4 mt-2"
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={!can_write}
|
||||
href={itemKind === 'flow' ? '/flows/add?hub=65' : '/scripts/add?hub=hub%2F19659'}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<KafkaTriggersConfigSection
|
||||
bind:args
|
||||
bind:isValid
|
||||
{path}
|
||||
defaultValues={useDefaultValues() ? defaultValues : undefined}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<KafkaTriggersConfigSection
|
||||
bind:kafkaCfgValid
|
||||
bind:kafkaResourcePath
|
||||
bind:kafkaCfg
|
||||
{path}
|
||||
{can_write}
|
||||
showTestingBadge={isEditor}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -1,162 +1,31 @@
|
||||
<script lang="ts">
|
||||
import ResourcePicker from '../../ResourcePicker.svelte'
|
||||
import { Boxes } from 'lucide-svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Subsection from '$lib/components/Subsection.svelte'
|
||||
import ToggleButton from '../../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '../../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import SchemaForm from '../../SchemaForm.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
export let path: string
|
||||
export let defaultValues: Record<string, any> | undefined = undefined
|
||||
export let headless: boolean = false
|
||||
export let args: Record<string, any> = {}
|
||||
export let staticInputDisabled: boolean = true
|
||||
export let showCapture: boolean = false
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
export let isValid: boolean = false
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
|
||||
let selected: 'resource' | 'static' = staticInputDisabled ? 'resource' : 'static'
|
||||
|
||||
const connnectionSchema = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
brokers: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
nullable: false,
|
||||
title: 'Brokers',
|
||||
default: ['']
|
||||
},
|
||||
security: {
|
||||
type: 'object',
|
||||
title: 'Security',
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
title: 'PLAINTEXT',
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['PLAINTEXT'],
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
order: ['mechanism', 'username', 'password'],
|
||||
title: 'SASL_PLAINTEXT',
|
||||
required: ['mechanism', 'username', 'password'],
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['SASL_PLAINTEXT'],
|
||||
type: 'string'
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
password: true
|
||||
},
|
||||
username: {
|
||||
type: 'string'
|
||||
},
|
||||
mechanism: {
|
||||
enum: ['PLAIN', 'SCRAM-SHA-256', 'SCRAM-SHA-512'],
|
||||
type: 'string',
|
||||
disableCreate: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
title: 'SSL',
|
||||
order: ['ca', 'certificate', 'key', 'key_password'],
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['SSL'],
|
||||
type: 'string'
|
||||
},
|
||||
ca: {
|
||||
type: 'string',
|
||||
description: 'CA certificate to verify the server certificate, in PEM format'
|
||||
},
|
||||
certificate: {
|
||||
type: 'string',
|
||||
description: 'Client certificate for authentication to the server, in PEM format'
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Client key for authentication to the server, in PEM format'
|
||||
},
|
||||
key_password: {
|
||||
type: 'string',
|
||||
description: 'Password to decrypt the client key, if encrypted',
|
||||
password: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
order: [
|
||||
'mechanism',
|
||||
'username',
|
||||
'password',
|
||||
'ca',
|
||||
'certificate',
|
||||
'key',
|
||||
'key_password'
|
||||
],
|
||||
title: 'SASL_SSL',
|
||||
required: ['mechanism', 'username', 'password'],
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['SASL_SSL'],
|
||||
type: 'string'
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
password: true
|
||||
},
|
||||
username: {
|
||||
type: 'string'
|
||||
},
|
||||
mechanism: {
|
||||
enum: ['PLAIN', 'SCRAM-SHA-256', 'SCRAM-SHA-512'],
|
||||
type: 'string',
|
||||
disableCreate: true
|
||||
},
|
||||
ca: {
|
||||
type: 'string',
|
||||
description: 'CA certificate to verify the server certificate, in PEM format'
|
||||
},
|
||||
certificate: {
|
||||
type: 'string',
|
||||
description: 'Client certificate for authentication to the server, in PEM format'
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Client key for authentication to the server, in PEM format'
|
||||
},
|
||||
key_password: {
|
||||
type: 'string',
|
||||
description: 'Password to decrypt the client key, if encrypted',
|
||||
password: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
required: ['brokers', 'security']
|
||||
interface Props {
|
||||
path: string
|
||||
kafkaCfgValid?: boolean
|
||||
kafkaResourcePath?: string
|
||||
kafkaCfg?: Record<string, any>
|
||||
can_write?: boolean
|
||||
showTestingBadge?: boolean
|
||||
}
|
||||
|
||||
const argsSchema = {
|
||||
let {
|
||||
path,
|
||||
kafkaCfgValid = $bindable(false),
|
||||
kafkaResourcePath = $bindable(''),
|
||||
kafkaCfg = $bindable({}),
|
||||
can_write = true,
|
||||
showTestingBadge = false
|
||||
}: Props = $props()
|
||||
|
||||
const kafkaConfigSchema = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -179,105 +48,39 @@
|
||||
required: ['topics', 'group_id']
|
||||
}
|
||||
|
||||
let isStaticConnectionValid = false
|
||||
let otherArgsValid = false
|
||||
|
||||
$: isConnectionValid =
|
||||
selected === 'resource'
|
||||
? !!args.kafka_resource_path
|
||||
: isStaticConnectionValid &&
|
||||
args.brokers &&
|
||||
args.brokers.length > 0 &&
|
||||
args.brokers.every((b) => b.length > 0)
|
||||
|
||||
$: isValid =
|
||||
isConnectionValid &&
|
||||
otherArgsValid &&
|
||||
args.topics &&
|
||||
args.topics.length > 0 &&
|
||||
args.topics.every((b) => /^[a-zA-Z0-9-_.]+$/.test(b))
|
||||
|
||||
$: usingResource = !!args.kafka_resource_path
|
||||
$: usingResource && (selected = 'resource')
|
||||
|
||||
function setGroupId() {
|
||||
if (!args.group_id) {
|
||||
args.group_id = `windmill_consumer-${$workspaceStore}-${path.replaceAll('/', '__')}`
|
||||
if (!kafkaCfg.group_id) {
|
||||
kafkaCfg.group_id = `windmill_consumer-${$workspaceStore}-${path.replaceAll('/', '__')}`
|
||||
}
|
||||
}
|
||||
$: path && setGroupId()
|
||||
|
||||
$effect(() => {
|
||||
path && setGroupId()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if showCapture && captureInfo}
|
||||
<CaptureSection
|
||||
captureType="kafka"
|
||||
disabled={!isValid}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
/>
|
||||
{/if}
|
||||
<Section label="Kafka" {headless}>
|
||||
<Section label="Kafka">
|
||||
{#snippet header()}
|
||||
{#if showTestingBadge}
|
||||
<TestingBadge />
|
||||
{/if}
|
||||
{/snippet}
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<div class="block grow w-full">
|
||||
<Subsection label="Connection">
|
||||
<svelte:fragment slot="header">
|
||||
{#if !staticInputDisabled}
|
||||
<ToggleButtonGroup
|
||||
bind:selected
|
||||
class="h-full"
|
||||
on:selected={(ev) => {
|
||||
if (ev.detail === 'static') {
|
||||
delete args.kafka_resource_path
|
||||
args.brokers = ['']
|
||||
args.security = {
|
||||
label: 'PLAINTEXT'
|
||||
}
|
||||
} else {
|
||||
delete args.brokers
|
||||
delete args.security
|
||||
}
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="static" label="Static" small={true} {item} />
|
||||
<ToggleButton value="resource" label="Resource" icon={Boxes} small={true} {item} />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if selected === 'resource'}
|
||||
<ResourcePicker
|
||||
resourceType="kafka"
|
||||
bind:value={args.kafka_resource_path}
|
||||
{defaultValues}
|
||||
/>
|
||||
{:else}
|
||||
<SchemaForm
|
||||
schema={connnectionSchema}
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:isValid={isStaticConnectionValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
{/if}
|
||||
{#if isConnectionValid}
|
||||
<ResourcePicker
|
||||
resourceType="kafka"
|
||||
bind:value={kafkaResourcePath}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{#if !!kafkaResourcePath}
|
||||
<TestTriggerConnection
|
||||
kind="kafka"
|
||||
args={{
|
||||
connection: args
|
||||
connection: {
|
||||
kafka_resource_path: kafkaResourcePath
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -287,18 +90,11 @@
|
||||
<div class="block grow w-full">
|
||||
<Subsection headless={true}>
|
||||
<SchemaForm
|
||||
schema={argsSchema}
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:isValid={otherArgsValid}
|
||||
schema={kafkaConfigSchema}
|
||||
bind:args={kafkaCfg}
|
||||
bind:isValid={kafkaCfgValid}
|
||||
lightHeader={true}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Subsection>
|
||||
</div>
|
||||
|
||||
@@ -1,137 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { KafkaTriggerService, type KafkaTrigger } from '$lib/gen'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import KafkaTriggerEditor from './KafkaTriggerEditor.svelte'
|
||||
import KafkaTriggerEditorInner from './KafkaTriggerEditorInner.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersEditorSection from '../TriggersEditorSection.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let newItem: boolean = false
|
||||
export let isEditor: boolean = false
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
isDeployed = false,
|
||||
defaultValues = undefined,
|
||||
newDraft = false,
|
||||
customLabel = undefined,
|
||||
...restProps
|
||||
} = $props()
|
||||
let kafkaTriggerEditor: KafkaTriggerEditorInner | undefined = $state(undefined)
|
||||
|
||||
let kafkaTriggerEditor: KafkaTriggerEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
|
||||
$: path && loadTriggers()
|
||||
|
||||
const { triggersCount, selectedTrigger, defaultValues } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
async function openKafkaTriggerEditor(isFlow: boolean, isDraft: boolean) {
|
||||
if (isDraft) {
|
||||
kafkaTriggerEditor?.openNew(isFlow, path, defaultValues)
|
||||
} else {
|
||||
kafkaTriggerEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (
|
||||
defaultValues &&
|
||||
$selectedTrigger === 'kafka' &&
|
||||
Object.keys($defaultValues ?? {}).length > 0
|
||||
) {
|
||||
kafkaTriggerEditor.openNew(isFlow, path, $defaultValues)
|
||||
defaultValues.set(undefined)
|
||||
}
|
||||
openKafkaTriggerEditor(isFlow, selectedTrigger.isDraft ?? false)
|
||||
})
|
||||
|
||||
let kafkaTriggers: (KafkaTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
export async function loadTriggers() {
|
||||
try {
|
||||
kafkaTriggers = (
|
||||
await KafkaTriggerService.listKafkaTriggers({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
isFlow
|
||||
})
|
||||
).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), kafka_count: kafkaTriggers?.length }
|
||||
openForm = kafkaTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load Kafka triggers', e)
|
||||
}
|
||||
}
|
||||
|
||||
let data = {
|
||||
kafkaTriggers: [],
|
||||
newItem
|
||||
}
|
||||
|
||||
function saveTrigger(path: string, args?: Record<string, any>) {
|
||||
kafkaTriggerEditor?.openNew(isFlow, path, args)
|
||||
}
|
||||
const cloudDisabled = $derived(isCloudHosted())
|
||||
</script>
|
||||
|
||||
<KafkaTriggerEditor
|
||||
on:update={() => {
|
||||
loadTriggers()
|
||||
}}
|
||||
bind:this={kafkaTriggerEditor}
|
||||
/>
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert title="EE Only" type="warning" size="xs">
|
||||
Kafka triggers are an enterprise only feature.
|
||||
</Alert>
|
||||
{:else if isCloudHosted()}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
Kafka triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/kafka_triggers">
|
||||
Kafka triggers execute scripts and flows in response to messages published to Kafka topics.
|
||||
</Description>
|
||||
{#if !newItem && kafkaTriggers && kafkaTriggers.length > 0}
|
||||
<Section label="Kafka triggers">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each kafkaTriggers as kafkaTrigger (kafkaTrigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{kafkaTrigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{kafkaTrigger.kafka_resource_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => kafkaTriggerEditor?.openEdit(kafkaTrigger.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if kafkaTrigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
<TriggersEditorSection
|
||||
on:saveTrigger={(e) => {
|
||||
saveTrigger(path, e.detail.config)
|
||||
}}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
cloudDisabled={false}
|
||||
triggerType="kafka"
|
||||
{isFlow}
|
||||
{data}
|
||||
{path}
|
||||
{isEditor}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
{openForm}
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
/>
|
||||
<KafkaTriggerEditorInner
|
||||
bind:this={kafkaTriggerEditor}
|
||||
useDrawer={false}
|
||||
hideTarget
|
||||
hideTooltips={!isDeployed || cloudDisabled}
|
||||
allowDraft={true}
|
||||
hasDraft={!!selectedTrigger.draftConfig}
|
||||
isDraftOnly={selectedTrigger.isDraft}
|
||||
{customLabel}
|
||||
{isDeployed}
|
||||
{cloudDisabled}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if cloudDisabled}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
Kafka triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/kafka_triggers">
|
||||
Kafka triggers execute scripts and flows in response to messages published to Kafka
|
||||
topics.
|
||||
</Description>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</KafkaTriggerEditorInner>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { KafkaTriggerService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
|
||||
export async function saveKafkaTriggerFromCfg(
|
||||
initialPath: string,
|
||||
cfg: Record<string, any>,
|
||||
edit: boolean,
|
||||
workspace: string,
|
||||
usedTriggerKinds: Writable<string[]>
|
||||
): Promise<boolean> {
|
||||
const requestBody = {
|
||||
path: cfg.path,
|
||||
script_path: cfg.script_path,
|
||||
is_flow: cfg.is_flow,
|
||||
kafka_resource_path: cfg.kafka_resource_path,
|
||||
group_id: cfg.group_id,
|
||||
topics: cfg.topics
|
||||
}
|
||||
try {
|
||||
if (edit) {
|
||||
await KafkaTriggerService.updateKafkaTrigger({
|
||||
workspace,
|
||||
path: initialPath,
|
||||
requestBody
|
||||
})
|
||||
sendUserToast(`Kafka trigger ${cfg.path} updated`)
|
||||
} else {
|
||||
await KafkaTriggerService.createKafkaTrigger({
|
||||
workspace,
|
||||
requestBody: { ...requestBody, enabled: true }
|
||||
})
|
||||
sendUserToast(`Kafka trigger ${cfg.path} created`)
|
||||
}
|
||||
if (!get(usedTriggerKinds).includes('kafka')) {
|
||||
usedTriggerKinds.update((t) => [...t, 'kafka'])
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
|
||||
interface Props {
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
isValid?: boolean | undefined
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
captureLoading?: boolean
|
||||
triggerDeployed?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
captureInfo = undefined,
|
||||
isValid = undefined,
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
captureLoading = false,
|
||||
triggerDeployed = false
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="mqtt"
|
||||
disabled={isValid === false}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
displayAlert={triggerDeployed}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
{#if captureInfo.connectionInfo?.connected}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Listening to MQTT messages...
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Connecting to mqtt...
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to MQTT messages.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import { Plus, X } from 'lucide-svelte'
|
||||
import Subsection from '$lib/components/Subsection.svelte'
|
||||
@@ -17,19 +15,18 @@
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
|
||||
export let can_write: boolean = false
|
||||
export let headless: boolean = false
|
||||
export let showCapture: boolean = false
|
||||
export let mqtt_resource_path: string = ''
|
||||
export let subscribe_topics: MqttSubscribeTopic[] = []
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let v3_config: MqttV3Config = DEFAULT_V3_CONFIG
|
||||
export let v5_config: MqttV5Config = DEFAULT_V5_CONFIG
|
||||
export let client_version: MqttClientVersion = 'v5'
|
||||
export let isValid: boolean = false
|
||||
export let client_id: string = ''
|
||||
export let showTestingBadge: boolean = false
|
||||
|
||||
const activateV5Options = {
|
||||
topic_alias: Boolean(v5_config.topic_alias),
|
||||
@@ -50,20 +47,12 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if showCapture && captureInfo}
|
||||
<CaptureSection
|
||||
captureType="mqtt"
|
||||
disabled={!isValid}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
/>
|
||||
{/if}
|
||||
<Section label="MQTT" {headless}>
|
||||
<svelte:fragment slot="header">
|
||||
{#if showTestingBadge}
|
||||
<TestingBadge />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<Subsection label="Connection setup">
|
||||
<ResourcePicker resourceType="mqtt" disabled={!can_write} bind:value={mqtt_resource_path} />
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { tick } from 'svelte'
|
||||
import MqttTriggerEditorInner from './MqttTriggerEditorInner.svelte'
|
||||
|
||||
let open = false
|
||||
let { onUpdate }: { onUpdate?: (path?: string) => void } = $props()
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
open = true
|
||||
await tick()
|
||||
@@ -19,9 +21,9 @@
|
||||
drawer?.openNew(is_flow, initial_script_path, defaultValues)
|
||||
}
|
||||
|
||||
let drawer: MqttTriggerEditorInner
|
||||
let drawer: MqttTriggerEditorInner | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<MqttTriggerEditorInner on:update bind:this={drawer} />
|
||||
<MqttTriggerEditorInner {onUpdate} bind:this={drawer} />
|
||||
{/if}
|
||||
|
||||
@@ -7,11 +7,9 @@
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Loader2, Save } from 'lucide-svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import {
|
||||
MqttTriggerService,
|
||||
type MqttClientVersion,
|
||||
@@ -20,33 +18,92 @@
|
||||
type MqttSubscribeTopic
|
||||
} from '$lib/gen'
|
||||
import MqttEditorConfigSection from './MqttEditorConfigSection.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import { saveMqttTriggerFromCfg } from './utils'
|
||||
import { handleConfigChange } from '../utils'
|
||||
|
||||
let mqtt_resource_path: string = ''
|
||||
let drawer: Drawer
|
||||
let is_flow: boolean = false
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
let itemKind: 'flow' | 'script' = 'script'
|
||||
let script_path = ''
|
||||
let initialScriptPath = ''
|
||||
let fixedScriptPath = ''
|
||||
let path: string = ''
|
||||
let pathError = ''
|
||||
let enabled = false
|
||||
let dirtyPath = false
|
||||
let can_write = true
|
||||
let drawerLoading = true
|
||||
let subscribe_topics: MqttSubscribeTopic[] = []
|
||||
let v3_config: MqttV3Config | undefined
|
||||
let v5_config: MqttV5Config | undefined
|
||||
let client_version: MqttClientVersion | undefined
|
||||
let client_id: string
|
||||
let isValid: boolean
|
||||
const dispatch = createEventDispatcher()
|
||||
interface Props {
|
||||
useDrawer?: boolean
|
||||
description?: Snippet | undefined
|
||||
hideTarget?: boolean
|
||||
hideTooltips?: boolean
|
||||
isEditor?: boolean
|
||||
allowDraft?: boolean
|
||||
hasDraft?: boolean
|
||||
isDraftOnly?: boolean
|
||||
customLabel?: Snippet
|
||||
isDeployed?: boolean
|
||||
cloudDisabled?: boolean
|
||||
onConfigChange?: (cfg: Record<string, any>, saveDisabled: boolean, updated: boolean) => void
|
||||
onCaptureConfigChange?: (cfg: Record<string, any>, isValid: boolean) => void
|
||||
onUpdate?: (path?: string) => void
|
||||
onDelete?: () => void
|
||||
onReset?: () => void
|
||||
}
|
||||
|
||||
$: is_flow = itemKind === 'flow'
|
||||
let {
|
||||
useDrawer = true,
|
||||
description = undefined,
|
||||
hideTarget = false,
|
||||
hideTooltips = false,
|
||||
isEditor = false,
|
||||
allowDraft = false,
|
||||
hasDraft = false,
|
||||
isDraftOnly = false,
|
||||
customLabel = undefined,
|
||||
isDeployed = false,
|
||||
onConfigChange = undefined,
|
||||
onCaptureConfigChange = undefined,
|
||||
onUpdate = undefined,
|
||||
onDelete = undefined,
|
||||
onReset = undefined,
|
||||
cloudDisabled = false
|
||||
}: Props = $props()
|
||||
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
let mqtt_resource_path: string = $state('')
|
||||
let drawer: Drawer | undefined = $state(undefined)
|
||||
let is_flow: boolean = $state(false)
|
||||
let initialPath = $state('')
|
||||
let edit = $state(true)
|
||||
let itemKind: 'flow' | 'script' = $state('script')
|
||||
let script_path = $state('')
|
||||
let initialScriptPath = $state('')
|
||||
let fixedScriptPath = $state('')
|
||||
let path: string = $state('')
|
||||
let pathError = $state('')
|
||||
let enabled = $state(false)
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let subscribe_topics: MqttSubscribeTopic[] = $state([])
|
||||
let v3_config: MqttV3Config | undefined = $state()
|
||||
let v5_config: MqttV5Config | undefined = $state()
|
||||
let client_version: MqttClientVersion | undefined = $state()
|
||||
let client_id: string | undefined = $state(undefined)
|
||||
let isValid: boolean | undefined = $state(undefined)
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
let deploymentLoading = $state(false)
|
||||
|
||||
const mqttConfig = $derived.by(getSaveCfg)
|
||||
const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({}))
|
||||
const saveDisabled = $derived(
|
||||
pathError != '' || emptyString(script_path) || !can_write || !isValid
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
is_flow = itemKind === 'flow'
|
||||
})
|
||||
|
||||
export async function openEdit(
|
||||
ePath: string,
|
||||
isFlow: boolean,
|
||||
defaultConfig?: Record<string, any>
|
||||
) {
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
@@ -54,11 +111,16 @@
|
||||
itemKind = isFlow ? 'flow' : 'script'
|
||||
edit = true
|
||||
dirtyPath = false
|
||||
await loadTrigger()
|
||||
await loadTrigger(defaultConfig)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load mqtt trigger: ${err.body}`, true)
|
||||
} finally {
|
||||
if (!defaultConfig) {
|
||||
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
}
|
||||
clearTimeout(loadingTimeout)
|
||||
drawerLoading = false
|
||||
showLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +129,12 @@
|
||||
fixedScriptPath_?: string,
|
||||
defaultValues?: Record<string, any>
|
||||
) {
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100)
|
||||
drawerLoading = true
|
||||
try {
|
||||
mqtt_resource_path = ''
|
||||
mqtt_resource_path = defaultValues?.mqtt_resource_path ?? ''
|
||||
drawer?.openDrawer()
|
||||
is_flow = nis_flow
|
||||
itemKind = nis_flow ? 'flow' : 'script'
|
||||
@@ -77,149 +142,212 @@
|
||||
fixedScriptPath = fixedScriptPath_ ?? ''
|
||||
script_path = fixedScriptPath
|
||||
subscribe_topics = defaultValues?.topics ?? []
|
||||
path = ''
|
||||
path = defaultValues?.path ?? ''
|
||||
initialPath = ''
|
||||
edit = false
|
||||
dirtyPath = false
|
||||
client_version = defaultValues?.client_version ?? 'v5'
|
||||
client_id = defaultValues?.client_id ?? ''
|
||||
enabled = defaultValues?.enabled ?? false
|
||||
} finally {
|
||||
clearTimeout(loadingTimeout)
|
||||
drawerLoading = false
|
||||
showLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrigger(): Promise<void> {
|
||||
async function loadTriggerConfig(cfg?: Record<string, any>): Promise<void> {
|
||||
try {
|
||||
const s = await MqttTriggerService.getMqttTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
mqtt_resource_path = s.mqtt_resource_path
|
||||
subscribe_topics = s.subscribe_topics
|
||||
script_path = s.script_path
|
||||
initialScriptPath = s.script_path
|
||||
is_flow = s.is_flow
|
||||
path = s.path
|
||||
enabled = s.enabled
|
||||
client_version = s.client_version
|
||||
v3_config = s.v3_config
|
||||
v5_config = s.v5_config
|
||||
client_id = s.client_id ?? ''
|
||||
can_write = canWrite(s.path, s.extra_perms, $userStore)
|
||||
mqtt_resource_path = cfg?.mqtt_resource_path
|
||||
subscribe_topics = cfg?.subscribe_topics
|
||||
script_path = cfg?.script_path
|
||||
initialScriptPath = cfg?.script_path
|
||||
is_flow = cfg?.is_flow
|
||||
path = cfg?.path
|
||||
enabled = cfg?.enabled
|
||||
client_version = cfg?.client_version
|
||||
v3_config = cfg?.v3_config
|
||||
v5_config = cfg?.v5_config
|
||||
client_id = cfg?.client_id ?? ''
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
} catch (error) {
|
||||
sendUserToast(`Could not load mqtt trigger config: ${error.body}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
|
||||
try {
|
||||
if (defaultConfig) {
|
||||
loadTriggerConfig(defaultConfig)
|
||||
return
|
||||
} else {
|
||||
const s = await MqttTriggerService.getMqttTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
loadTriggerConfig(s)
|
||||
}
|
||||
} catch (error) {
|
||||
sendUserToast(`Could not load mqtt trigger: ${error.body}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTrigger(): Promise<void> {
|
||||
if (edit) {
|
||||
await MqttTriggerService.updateMqttTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
client_id,
|
||||
client_version,
|
||||
v3_config,
|
||||
v5_config,
|
||||
mqtt_resource_path,
|
||||
subscribe_topics,
|
||||
path,
|
||||
script_path,
|
||||
enabled,
|
||||
is_flow
|
||||
}
|
||||
})
|
||||
sendUserToast(`MQTT trigger ${path} updated`)
|
||||
} else {
|
||||
await MqttTriggerService.createMqttTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
client_id,
|
||||
client_version,
|
||||
v3_config,
|
||||
v5_config,
|
||||
mqtt_resource_path,
|
||||
subscribe_topics,
|
||||
enabled: true,
|
||||
path,
|
||||
script_path,
|
||||
is_flow
|
||||
}
|
||||
})
|
||||
sendUserToast(`MQTT trigger ${path} created`)
|
||||
function getSaveCfg(): Record<string, any> {
|
||||
return {
|
||||
client_id,
|
||||
client_version,
|
||||
v3_config,
|
||||
v5_config,
|
||||
mqtt_resource_path,
|
||||
subscribe_topics,
|
||||
path,
|
||||
script_path,
|
||||
enabled,
|
||||
is_flow
|
||||
}
|
||||
|
||||
if (!$usedTriggerKinds.includes('mqtt')) {
|
||||
$usedTriggerKinds = [...$usedTriggerKinds, 'mqtt']
|
||||
}
|
||||
dispatch('update')
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
|
||||
function getCaptureConfig(): Record<string, any> {
|
||||
return {
|
||||
mqtt_resource_path,
|
||||
subscribe_topics,
|
||||
client_version,
|
||||
v3_config,
|
||||
v5_config,
|
||||
client_id,
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTrigger(): Promise<void> {
|
||||
deploymentLoading = true
|
||||
const cfg = getSaveCfg()
|
||||
const isSaved = await saveMqttTriggerFromCfg(
|
||||
initialPath,
|
||||
cfg,
|
||||
edit,
|
||||
$workspaceStore!,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
onUpdate?.(cfg.path)
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
deploymentLoading = false
|
||||
}
|
||||
|
||||
async function handleToggleEnabled(newEnabled: boolean) {
|
||||
enabled = newEnabled
|
||||
if (!isDraftOnly && !hasDraft) {
|
||||
await MqttTriggerService.setMqttTriggerEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: newEnabled }
|
||||
})
|
||||
sendUserToast(`${newEnabled ? 'enabled' : 'disabled'} MQTT trigger ${initialPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onCaptureConfigChange?.(captureConfig, isValid ?? false)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!drawerLoading) {
|
||||
handleConfigChange(mqttConfig, initialConfig, saveDisabled, edit, onConfigChange)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit MQTT trigger ${initialPath}`
|
||||
: `MQTT trigger ${initialPath}`
|
||||
: 'New MQTT trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !drawerLoading && can_write}
|
||||
{#if edit}
|
||||
<div class="mr-8 center-center -mt-1">
|
||||
<Toggle
|
||||
disabled={!can_write}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
on:change={async (e) => {
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} mqtt trigger ${initialPath}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={pathError != '' || emptyString(script_path) || !can_write || !isValid}
|
||||
on:click={updateTrigger}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{#if useDrawer}
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit MQTT trigger ${initialPath}`
|
||||
: `MQTT trigger ${initialPath}`
|
||||
: 'New MQTT trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{@render actions()}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else}
|
||||
<Section label={!customLabel ? 'MQTT trigger' : ''} headerClass="grow min-w-0 h-[30px]">
|
||||
<svelte:fragment slot="header">
|
||||
{#if customLabel}
|
||||
{@render customLabel()}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#if drawerLoading}
|
||||
<div class="flex flex-col items-center justify-center h-full w-full">
|
||||
<Loader2 size="50" class="animate-spin" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-5">
|
||||
<Alert title="Info" type="info">
|
||||
<svelte:fragment slot="action">
|
||||
{@render actions()}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
{#snippet actions()}
|
||||
{#if !drawerLoading}
|
||||
<TriggerEditorToolbar
|
||||
{isDraftOnly}
|
||||
{hasDraft}
|
||||
permissions={drawerLoading || !can_write ? 'none' : 'create'}
|
||||
{saveDisabled}
|
||||
{enabled}
|
||||
{allowDraft}
|
||||
{edit}
|
||||
isLoading={deploymentLoading}
|
||||
{isDeployed}
|
||||
onUpdate={updateTrigger}
|
||||
{onReset}
|
||||
{onDelete}
|
||||
onToggleEnabled={handleToggleEnabled}
|
||||
{cloudDisabled}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet config()}
|
||||
{#if drawerLoading}
|
||||
{#if showLoading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if description}
|
||||
{@render description()}
|
||||
{/if}
|
||||
{#if !hideTooltips}
|
||||
<Alert title="Info" type="info" size="xs">
|
||||
{#if edit}
|
||||
Changes can take up to 30 seconds to take effect.
|
||||
{:else}
|
||||
New MQTT triggers can take up to 30 seconds to start listening.
|
||||
{/if}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="mqtt_trigger"
|
||||
kind="mqtt_trigger"
|
||||
disabled={!can_write}
|
||||
disableEditing={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="mqtt_trigger"
|
||||
kind="mqtt_trigger"
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{#if !hideTarget}
|
||||
<Section label="Runnable">
|
||||
<p class="text-xs mb-1 text-tertiary">
|
||||
Pick a script or flow to be triggered<Required required={true} />
|
||||
@@ -232,32 +360,37 @@
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
/>
|
||||
{#if emptyString(script_path)}
|
||||
<Button
|
||||
btnClasses="ml-4 mt-2"
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={!can_write}
|
||||
href={itemKind === 'flow' ? '/flows/add?hub=61' : '/scripts/add?hub=hub%2F19655'}
|
||||
target="_blank">Create from template</Button
|
||||
target="_blank"
|
||||
>
|
||||
Create from template
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<MqttEditorConfigSection
|
||||
bind:mqtt_resource_path
|
||||
bind:subscribe_topics
|
||||
bind:can_write
|
||||
bind:client_version
|
||||
bind:v3_config
|
||||
bind:v5_config
|
||||
bind:isValid
|
||||
bind:client_id
|
||||
headless={true}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<MqttEditorConfigSection
|
||||
bind:mqtt_resource_path
|
||||
bind:subscribe_topics
|
||||
{can_write}
|
||||
bind:client_version
|
||||
bind:v3_config
|
||||
bind:v5_config
|
||||
bind:isValid
|
||||
bind:client_id
|
||||
headless={true}
|
||||
showTestingBadge={isEditor}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -1,126 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import MqttTriggerEditorInner from './MqttTriggerEditorInner.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersEditorSection from '../TriggersEditorSection.svelte'
|
||||
import MqttTriggerEditor from './MqttTriggerEditor.svelte'
|
||||
import { MqttTriggerService, type MqttTrigger } from '$lib/gen'
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let newItem: boolean = false
|
||||
export let isEditor: boolean = false
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let mqttTriggerEditor: MqttTriggerEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
isDeployed = false,
|
||||
defaultValues = undefined,
|
||||
newDraft = false,
|
||||
customLabel = undefined,
|
||||
...props
|
||||
} = $props()
|
||||
let mqttTriggerEditor: MqttTriggerEditorInner | undefined = $state(undefined)
|
||||
|
||||
$: path && loadTriggers()
|
||||
|
||||
const { triggersCount, selectedTrigger, defaultValues } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
onMount(() => {
|
||||
if (
|
||||
defaultValues &&
|
||||
$selectedTrigger === 'mqtt' &&
|
||||
Object.keys($defaultValues ?? {}).length > 0
|
||||
) {
|
||||
mqttTriggerEditor.openNew(isFlow, path, $defaultValues)
|
||||
defaultValues.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
let mqttTriggers: (MqttTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
export async function loadTriggers() {
|
||||
try {
|
||||
mqttTriggers = (
|
||||
await MqttTriggerService.listMqttTriggers({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
isFlow
|
||||
})
|
||||
).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), mqtt_count: mqttTriggers?.length }
|
||||
openForm = mqttTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load mqtt triggers', e)
|
||||
async function openMqttTriggerEditor(isFlow: boolean, isDraft: boolean) {
|
||||
if (isDraft) {
|
||||
mqttTriggerEditor?.openNew(isFlow, path, defaultValues)
|
||||
} else {
|
||||
mqttTriggerEditor?.openEdit(selectedTrigger.path, isFlow, selectedTrigger.draftConfig)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mqttTriggerEditor && openMqttTriggerEditor(isFlow, selectedTrigger.isDraft ?? false)
|
||||
})
|
||||
|
||||
const cloudDisabled = $derived(isCloudHosted())
|
||||
</script>
|
||||
|
||||
<MqttTriggerEditor
|
||||
on:update={() => {
|
||||
loadTriggers()
|
||||
}}
|
||||
bind:this={mqttTriggerEditor}
|
||||
/>
|
||||
|
||||
{#if isCloudHosted()}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
MQTT triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<Description link="https://mqtt.org/">
|
||||
Windmill can connect to an MQTT broker and subscribes to specific topics thus allowing the
|
||||
execution of script/flows based on the event triggered by those subscribed topics
|
||||
</Description>
|
||||
|
||||
{#if !newItem && mqttTriggers && mqttTriggers.length > 0}
|
||||
<Section label="MQTT">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each mqttTriggers as mqttTriggers (mqttTriggers.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{mqttTriggers.path}</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => mqttTriggerEditor?.openEdit(mqttTriggers.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if mqttTriggers.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<TriggersEditorSection
|
||||
on:applyArgs
|
||||
on:saveTrigger={(e) => {
|
||||
mqttTriggerEditor?.openNew(isFlow, path, e.detail.config)
|
||||
}}
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
cloudDisabled={false}
|
||||
triggerType="mqtt"
|
||||
{isFlow}
|
||||
{path}
|
||||
{isEditor}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
{openForm}
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
<MqttTriggerEditorInner
|
||||
bind:this={mqttTriggerEditor}
|
||||
useDrawer={false}
|
||||
hideTarget
|
||||
hideTooltips={!isDeployed || cloudDisabled}
|
||||
allowDraft={true}
|
||||
hasDraft={!!selectedTrigger.draftConfig}
|
||||
isDraftOnly={selectedTrigger.isDraft}
|
||||
{customLabel}
|
||||
{isDeployed}
|
||||
{cloudDisabled}
|
||||
{...props}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if cloudDisabled}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
MQTT triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/mqtt_triggers">
|
||||
MQTT triggers allow you to execute scripts and flows in response to MQTT messages. They
|
||||
can be configured to subscribe to specific topics with different QoS levels.
|
||||
</Description>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</MqttTriggerEditorInner>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { MqttTriggerService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
|
||||
export async function saveMqttTriggerFromCfg(
|
||||
initialPath: string,
|
||||
cfg: Record<string, any>,
|
||||
edit: boolean,
|
||||
workspace: string,
|
||||
usedTriggerKinds: Writable<string[]>
|
||||
): Promise<boolean> {
|
||||
const requestBody = {
|
||||
client_id: cfg.client_id,
|
||||
client_version: cfg.client_version,
|
||||
v3_config: cfg.v3_config,
|
||||
v5_config: cfg.v5_config,
|
||||
mqtt_resource_path: cfg.mqtt_resource_path,
|
||||
subscribe_topics: cfg.subscribe_topics,
|
||||
path: cfg.path,
|
||||
script_path: cfg.script_path,
|
||||
enabled: cfg.enabled,
|
||||
is_flow: cfg.is_flow
|
||||
}
|
||||
try {
|
||||
if (edit) {
|
||||
await MqttTriggerService.updateMqttTrigger({
|
||||
workspace,
|
||||
path: initialPath,
|
||||
requestBody
|
||||
})
|
||||
sendUserToast(`MQTT trigger ${cfg.path} updated`)
|
||||
} else {
|
||||
await MqttTriggerService.createMqttTrigger({
|
||||
workspace,
|
||||
requestBody: { ...requestBody, enabled: true }
|
||||
})
|
||||
sendUserToast(`MQTT trigger ${cfg.path} created`)
|
||||
}
|
||||
|
||||
if (!get(usedTriggerKinds).includes('mqtt')) {
|
||||
usedTriggerKinds.update((t) => [...t, 'mqtt'])
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
|
||||
interface Props {
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
isValid?: boolean | undefined
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
captureLoading?: boolean
|
||||
triggerDeployed?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
captureInfo = undefined,
|
||||
isValid = undefined,
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
captureLoading = false,
|
||||
triggerDeployed = false
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="nats"
|
||||
disabled={isValid === false}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
displayAlert={triggerDeployed}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
{#if captureInfo.connectionInfo?.connected}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Listening to NATS messages...
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Connecting to nats...
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to NATS messages.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -2,7 +2,9 @@
|
||||
import { tick } from 'svelte'
|
||||
import NatsTriggerEditorInner from './NatsTriggerEditorInner.svelte'
|
||||
|
||||
let open = false
|
||||
let { onUpdate } = $props()
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
open = true
|
||||
await tick()
|
||||
@@ -19,9 +21,9 @@
|
||||
drawer?.openNew(is_flow, initial_script_path, defaultValues)
|
||||
}
|
||||
|
||||
let drawer: NatsTriggerEditorInner
|
||||
let drawer: NatsTriggerEditorInner | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<NatsTriggerEditorInner on:update bind:this={drawer} />
|
||||
<NatsTriggerEditorInner {onUpdate} bind:this={drawer} />
|
||||
{/if}
|
||||
|
||||
@@ -8,35 +8,97 @@
|
||||
import { NatsTriggerService } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Loader2, Save } from 'lucide-svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import NatsTriggersConfigSection from './NatsTriggersConfigSection.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import { saveNatsTriggerFromCfg } from './utils'
|
||||
import { handleConfigChange } from '../utils'
|
||||
|
||||
let drawer: Drawer
|
||||
let is_flow: boolean = false
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
let itemKind: 'flow' | 'script' = 'script'
|
||||
let script_path = ''
|
||||
let initialScriptPath = ''
|
||||
let fixedScriptPath = ''
|
||||
let path: string = ''
|
||||
let pathError = ''
|
||||
let enabled = false
|
||||
let dirtyPath = false
|
||||
let can_write = true
|
||||
let drawerLoading = true
|
||||
let defaultValues: Record<string, any> | undefined = undefined
|
||||
let args: Record<string, any> = {}
|
||||
interface Props {
|
||||
useDrawer?: boolean
|
||||
description?: Snippet | undefined
|
||||
hideTarget?: boolean
|
||||
hideTooltips?: boolean
|
||||
useEditButton?: boolean
|
||||
isEditor?: boolean
|
||||
allowDraft?: boolean
|
||||
hasDraft?: boolean
|
||||
isDraftOnly?: boolean
|
||||
isDeployed?: boolean
|
||||
cloudDisabled?: boolean
|
||||
customLabel?: Snippet
|
||||
onConfigChange?: (cfg: Record<string, any>, saveDisabled: boolean, updated: boolean) => void
|
||||
onCaptureConfigChange?: (cfg: Record<string, any>, isValid: boolean) => void
|
||||
onUpdate?: (path?: string) => void
|
||||
onDelete?: () => void
|
||||
onReset?: () => void
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let {
|
||||
useDrawer = true,
|
||||
description = undefined,
|
||||
hideTarget = false,
|
||||
hideTooltips = false,
|
||||
isEditor = false,
|
||||
allowDraft = false,
|
||||
hasDraft = false,
|
||||
isDraftOnly = false,
|
||||
isDeployed = false,
|
||||
cloudDisabled = false,
|
||||
customLabel = undefined,
|
||||
onConfigChange = undefined,
|
||||
onCaptureConfigChange = undefined,
|
||||
onUpdate = undefined,
|
||||
onDelete = undefined,
|
||||
onReset = undefined
|
||||
}: Props = $props()
|
||||
|
||||
$: is_flow = itemKind === 'flow'
|
||||
let drawer: Drawer | undefined = $state(undefined)
|
||||
let is_flow: boolean = $state(false)
|
||||
let initialPath = $state('')
|
||||
let edit = $state(true)
|
||||
let itemKind: 'flow' | 'script' = $state('script')
|
||||
let script_path = $state('')
|
||||
let initialScriptPath = $state('')
|
||||
let fixedScriptPath = $state('')
|
||||
let path: string = $state('')
|
||||
let pathError = $state('')
|
||||
let enabled = $state(false)
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let defaultValues: Record<string, any> | undefined = $state(undefined)
|
||||
let natsResourcePath = $state('')
|
||||
let subjects = $state([''])
|
||||
let useJetstream = $state(false)
|
||||
let streamName = $state('')
|
||||
let consumerName = $state('')
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
let deploymentLoading = $state(false)
|
||||
let isValid = $state(false)
|
||||
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
const saveDisabled = $derived(
|
||||
pathError != '' || emptyString(script_path) || !can_write || !isValid
|
||||
)
|
||||
const natsConfig = $derived.by(getSaveCfg)
|
||||
const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({}))
|
||||
|
||||
$effect(() => {
|
||||
is_flow = itemKind === 'flow'
|
||||
})
|
||||
|
||||
export async function openEdit(
|
||||
ePath: string,
|
||||
isFlow: boolean,
|
||||
defaultConfig?: Record<string, any>
|
||||
) {
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
@@ -44,11 +106,16 @@
|
||||
itemKind = isFlow ? 'flow' : 'script'
|
||||
edit = true
|
||||
dirtyPath = false
|
||||
await loadTrigger()
|
||||
await loadTrigger(defaultConfig)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load nats trigger: ${err}`, true)
|
||||
} finally {
|
||||
clearTimeout(loadingTimeout)
|
||||
drawerLoading = false
|
||||
showLoading = false
|
||||
if (!defaultConfig) {
|
||||
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,92 +124,95 @@
|
||||
fixedScriptPath_?: string,
|
||||
nDefaultValues?: Record<string, any>
|
||||
) {
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100)
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
is_flow = nis_flow
|
||||
edit = false
|
||||
itemKind = nis_flow ? 'flow' : 'script'
|
||||
args.nats_resource_path = nDefaultValues?.nats_resource_path ?? ''
|
||||
args.subjects = nDefaultValues?.subjects ?? ['']
|
||||
args.use_jetstream = nDefaultValues?.use_jetstream ?? false
|
||||
args.stream_name = args.use_jetstream ? (nDefaultValues?.stream_name ?? '') : undefined
|
||||
args.consumer_name = args.use_jetstream ? (nDefaultValues?.consumer_name ?? '') : undefined
|
||||
natsResourcePath = nDefaultValues?.nats_resource_path ?? ''
|
||||
subjects = nDefaultValues?.subjects ?? ['']
|
||||
useJetstream = nDefaultValues?.use_jetstream ?? false
|
||||
streamName = useJetstream ? (nDefaultValues?.stream_name ?? '') : undefined
|
||||
consumerName = useJetstream ? (nDefaultValues?.consumer_name ?? '') : undefined
|
||||
initialScriptPath = ''
|
||||
fixedScriptPath = fixedScriptPath_ ?? ''
|
||||
script_path = fixedScriptPath
|
||||
path = ''
|
||||
path = nDefaultValues?.path ?? ''
|
||||
initialPath = ''
|
||||
dirtyPath = false
|
||||
defaultValues = nDefaultValues
|
||||
enabled = nDefaultValues?.enabled ?? false
|
||||
} finally {
|
||||
clearTimeout(loadingTimeout)
|
||||
drawerLoading = false
|
||||
showLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrigger(): Promise<void> {
|
||||
const s = await NatsTriggerService.getNatsTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
script_path = s.script_path
|
||||
initialScriptPath = s.script_path
|
||||
async function loadTriggerConfig(cfg?: Record<string, any>): Promise<void> {
|
||||
script_path = cfg?.script_path
|
||||
initialScriptPath = cfg?.script_path
|
||||
is_flow = cfg?.is_flow
|
||||
path = cfg?.path
|
||||
natsResourcePath = cfg?.nats_resource_path
|
||||
streamName = cfg?.stream_name
|
||||
consumerName = cfg?.consumer_name
|
||||
subjects = cfg?.subjects || ['']
|
||||
useJetstream = cfg?.use_jetstream || false
|
||||
enabled = cfg?.enabled
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
}
|
||||
|
||||
is_flow = s.is_flow
|
||||
path = s.path
|
||||
args.nats_resource_path = s.nats_resource_path
|
||||
args.stream_name = s.stream_name
|
||||
args.consumer_name = s.consumer_name
|
||||
args.subjects = s.subjects
|
||||
args.use_jetstream = s.use_jetstream
|
||||
enabled = s.enabled
|
||||
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
|
||||
if (defaultConfig) {
|
||||
loadTriggerConfig(defaultConfig)
|
||||
return
|
||||
} else {
|
||||
const s = await NatsTriggerService.getNatsTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
loadTriggerConfig(s)
|
||||
}
|
||||
}
|
||||
|
||||
can_write = canWrite(s.path, s.extra_perms, $userStore)
|
||||
function getSaveCfg() {
|
||||
return {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
enabled,
|
||||
nats_resource_path: natsResourcePath,
|
||||
stream_name: streamName,
|
||||
consumer_name: consumerName,
|
||||
subjects,
|
||||
use_jetstream: useJetstream
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTrigger(): Promise<void> {
|
||||
if (edit) {
|
||||
await NatsTriggerService.updateNatsTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
nats_resource_path: args.nats_resource_path,
|
||||
stream_name: args.stream_name,
|
||||
consumer_name: args.consumer_name,
|
||||
subjects: args.subjects,
|
||||
use_jetstream: args.use_jetstream
|
||||
}
|
||||
})
|
||||
sendUserToast(`Nats trigger ${path} updated`)
|
||||
} else {
|
||||
await NatsTriggerService.createNatsTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
enabled: true,
|
||||
nats_resource_path: args.nats_resource_path,
|
||||
stream_name: args.stream_name,
|
||||
consumer_name: args.consumer_name,
|
||||
subjects: args.subjects,
|
||||
use_jetstream: args.use_jetstream
|
||||
}
|
||||
})
|
||||
sendUserToast(`Nats trigger ${path} created`)
|
||||
deploymentLoading = true
|
||||
const cfg = natsConfig
|
||||
const isSaved = await saveNatsTriggerFromCfg(
|
||||
initialPath,
|
||||
cfg,
|
||||
edit,
|
||||
$workspaceStore!,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
onUpdate?.(cfg.path)
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
if (!$usedTriggerKinds.includes('nats')) {
|
||||
$usedTriggerKinds = [...$usedTriggerKinds, 'nats']
|
||||
}
|
||||
dispatch('update')
|
||||
drawer.closeDrawer()
|
||||
deploymentLoading = false
|
||||
}
|
||||
|
||||
function useDefaultValues() {
|
||||
if (args.nats_resource_path && args.nats_resource_path != '') {
|
||||
if (natsResourcePath && natsResourcePath != '') {
|
||||
return false
|
||||
}
|
||||
if (!defaultValues) {
|
||||
@@ -155,73 +225,120 @@
|
||||
)
|
||||
}
|
||||
|
||||
let isValid = false
|
||||
async function handleToggleEnabled(toggleEnabled: boolean) {
|
||||
enabled = toggleEnabled
|
||||
if (!isDraftOnly && !hasDraft) {
|
||||
await NatsTriggerService.setNatsTriggerEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: toggleEnabled }
|
||||
})
|
||||
sendUserToast(`${toggleEnabled ? 'enabled' : 'disabled'} NATS trigger ${initialPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
function getCaptureConfig() {
|
||||
const { nats_resource_path, subjects, stream_name, consumer_name, use_jetstream } = natsConfig
|
||||
return { nats_resource_path, subjects, stream_name, consumer_name, use_jetstream }
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onCaptureConfigChange?.(captureConfig, isValid)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
!drawerLoading &&
|
||||
handleConfigChange(natsConfig, initialConfig, saveDisabled, edit, onConfigChange)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit NATS trigger ${initialPath}`
|
||||
: `NATS trigger ${initialPath}`
|
||||
: 'New NATS trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !drawerLoading}
|
||||
{#if edit}
|
||||
<div class="mr-8 center-center -mt-1">
|
||||
<Toggle
|
||||
disabled={!can_write}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
on:change={async (e) => {
|
||||
await NatsTriggerService.setNatsTriggerEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} NATS trigger ${initialPath}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if can_write}
|
||||
<Button
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={pathError != '' || emptyString(script_path) || !can_write || !isValid}
|
||||
on:click={updateTrigger}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{/if}
|
||||
{#if useDrawer}
|
||||
<Drawer size="800px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit NATS trigger ${initialPath}`
|
||||
: `NATS trigger ${initialPath}`
|
||||
: 'New NATS trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{@render actions()}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else}
|
||||
<Section label={!customLabel ? 'NATS trigger' : ''} headerClass="grow min-w-0 h-[30px]">
|
||||
<svelte:fragment slot="header">
|
||||
{#if customLabel}
|
||||
{@render customLabel()}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#if drawerLoading}
|
||||
<svelte:fragment slot="action">
|
||||
{@render actions()}
|
||||
</svelte:fragment>
|
||||
{@render config()}
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
{#snippet actions()}
|
||||
{#if !drawerLoading}
|
||||
<TriggerEditorToolbar
|
||||
{isDraftOnly}
|
||||
{hasDraft}
|
||||
permissions={drawerLoading || !can_write ? 'none' : 'create'}
|
||||
{saveDisabled}
|
||||
{enabled}
|
||||
{allowDraft}
|
||||
{edit}
|
||||
isLoading={deploymentLoading}
|
||||
{isDeployed}
|
||||
onUpdate={updateTrigger}
|
||||
{onReset}
|
||||
{onDelete}
|
||||
onToggleEnabled={handleToggleEnabled}
|
||||
{cloudDisabled}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet config()}
|
||||
{#if drawerLoading}
|
||||
{#if showLoading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
<Alert title="Info" type="info">
|
||||
{#if edit}
|
||||
Changes can take up to 30 seconds to take effect.
|
||||
{:else}
|
||||
NATS consumers can take up to 30 seconds to start.
|
||||
{/if}
|
||||
</Alert>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="nats_trigger"
|
||||
kind="nats_trigger"
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if description}
|
||||
{@render description()}
|
||||
{/if}
|
||||
{#if !hideTooltips}
|
||||
<Alert title="Info" type="info" size="xs">
|
||||
{#if edit}
|
||||
Changes can take up to 30 seconds to take effect.
|
||||
{:else}
|
||||
NATS consumers can take up to 30 seconds to start.
|
||||
{/if}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-12 mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="nats_trigger"
|
||||
kind="nats_trigger"
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{#if !hideTarget}
|
||||
<Section label="Runnable">
|
||||
<p class="text-xs mb-1 text-tertiary">
|
||||
Pick a script or flow to be triggered<Required required={true} />
|
||||
@@ -243,19 +360,29 @@
|
||||
color="dark"
|
||||
size="xs"
|
||||
href={itemKind === 'flow' ? '/flows/add?hub=66' : '/scripts/add?hub=hub%2F19663'}
|
||||
target="_blank">Create from template</Button
|
||||
target="_blank"
|
||||
>
|
||||
Create from template
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<NatsTriggersConfigSection
|
||||
{path}
|
||||
bind:args
|
||||
bind:isValid
|
||||
defaultValues={useDefaultValues() ? defaultValues : undefined}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<NatsTriggersConfigSection
|
||||
{path}
|
||||
bind:natsResourcePath
|
||||
bind:subjects
|
||||
bind:useJetstream
|
||||
bind:streamName
|
||||
bind:consumerName
|
||||
on:valid-config={({ detail }) => {
|
||||
isValid = detail
|
||||
}}
|
||||
defaultValues={useDefaultValues() ? defaultValues : undefined}
|
||||
{can_write}
|
||||
showTestingBadge={isEditor}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -1,135 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { Boxes } from 'lucide-svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Subsection from '$lib/components/Subsection.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
export let defaultValues: Record<string, any> | undefined = undefined
|
||||
export let headless: boolean = false
|
||||
export let args: Record<string, any> = {}
|
||||
export let staticInputDisabled: boolean = true
|
||||
export let showCapture: boolean = false
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
export let isValid: boolean = false
|
||||
export let path: string
|
||||
let selected: 'resource' | 'static' = staticInputDisabled ? 'resource' : 'static'
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const connnectionSchema = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
servers: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
title: 'Servers',
|
||||
default: ['']
|
||||
},
|
||||
require_tls: {
|
||||
type: 'boolean',
|
||||
title: 'Require TLS',
|
||||
default: false
|
||||
},
|
||||
auth: {
|
||||
type: 'object',
|
||||
title: 'Authentication',
|
||||
default: {
|
||||
label: 'NO_AUTH'
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
title: 'NO_AUTH',
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['NO_AUTH'],
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
order: ['token'],
|
||||
title: 'TOKEN',
|
||||
required: ['token'],
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['TOKEN'],
|
||||
type: 'string'
|
||||
},
|
||||
token: {
|
||||
type: 'string',
|
||||
password: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
title: 'USER_PASSWORD',
|
||||
required: ['user', 'password'],
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['USER_PASSWORD'],
|
||||
type: 'string'
|
||||
},
|
||||
user: {
|
||||
type: 'string'
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
password: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
order: ['nkey'],
|
||||
title: 'NKEY',
|
||||
required: ['nkey'],
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['NKEY'],
|
||||
type: 'string'
|
||||
},
|
||||
seed: {
|
||||
type: 'string',
|
||||
password: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
title: 'JWT',
|
||||
required: ['jwt', 'seed'],
|
||||
properties: {
|
||||
label: {
|
||||
enum: ['JWT'],
|
||||
type: 'string'
|
||||
},
|
||||
jwt: {
|
||||
type: 'string',
|
||||
password: true
|
||||
},
|
||||
seed: {
|
||||
type: 'string',
|
||||
password: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
required: ['servers', 'auth', 'require_tls']
|
||||
interface Props {
|
||||
defaultValues?: Record<string, any> | undefined
|
||||
headless?: boolean
|
||||
natsResourcePath: string
|
||||
subjects: string[]
|
||||
useJetstream: boolean
|
||||
streamName: string
|
||||
consumerName: string
|
||||
path: string
|
||||
can_write?: boolean
|
||||
showTestingBadge?: boolean
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let {
|
||||
defaultValues = undefined,
|
||||
headless = false,
|
||||
natsResourcePath = $bindable(),
|
||||
subjects = $bindable(),
|
||||
useJetstream = $bindable(),
|
||||
streamName = $bindable(),
|
||||
consumerName = $bindable(),
|
||||
path,
|
||||
can_write = true,
|
||||
showTestingBadge = false
|
||||
}: Props = $props()
|
||||
|
||||
let otherArgsValid = $state(false)
|
||||
let globalError = $derived(
|
||||
!useJetstream && subjects && subjects.length > 1
|
||||
? 'Only one subject is supported if not using JetStream.'
|
||||
: ''
|
||||
)
|
||||
let isConnectionValid = $derived(!!natsResourcePath)
|
||||
|
||||
const argsSchema = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
type: 'object',
|
||||
@@ -173,116 +87,71 @@
|
||||
required: ['subjects', 'use_jetstream', 'stream_name', 'consumer_name']
|
||||
}
|
||||
|
||||
let isStaticConnectionValid = false
|
||||
let otherArgsValid = false
|
||||
let globalError = ''
|
||||
|
||||
$: globalError =
|
||||
!args.use_jetstream && args.subjects && args.subjects.length > 1
|
||||
? 'Only one subject is supported if not using JetStream.'
|
||||
: ''
|
||||
|
||||
$: isConnectionValid =
|
||||
selected === 'resource'
|
||||
? !!args.nats_resource_path
|
||||
: isStaticConnectionValid &&
|
||||
args.servers &&
|
||||
args.servers.length > 0 &&
|
||||
args.servers.every((b) => b.length > 0) &&
|
||||
args.require_tls !== undefined &&
|
||||
args.require_tls !== null
|
||||
|
||||
$: isValid =
|
||||
isConnectionValid &&
|
||||
otherArgsValid &&
|
||||
args.subjects &&
|
||||
args.subjects.length > 0 &&
|
||||
args.subjects.every((b) => /^[a-zA-Z0-9-_.*>]+$/.test(b)) &&
|
||||
globalError === ''
|
||||
|
||||
$: usingResource = !!args.nats_resource_path
|
||||
$: usingResource && (selected = 'resource')
|
||||
$effect(() => {
|
||||
const valid =
|
||||
isConnectionValid &&
|
||||
otherArgsValid &&
|
||||
!!subjects &&
|
||||
subjects.length > 0 &&
|
||||
subjects.every((b) => /^[a-zA-Z0-9-_.*>]+$/.test(b)) &&
|
||||
globalError === ''
|
||||
dispatch('valid-config', valid)
|
||||
})
|
||||
|
||||
function setStreamAndConsumerNames() {
|
||||
if (!args.stream_name) {
|
||||
args.stream_name = `windmill_stream-${$workspaceStore}-${path.replaceAll('/', '__')}`
|
||||
if (!streamName) {
|
||||
streamName = `windmill_stream-${$workspaceStore}-${path.replaceAll('/', '__')}`
|
||||
}
|
||||
if (!args.consumer_name) {
|
||||
args.consumer_name = `windmill_consumer-${$workspaceStore}-${path.replaceAll('/', '__')}`
|
||||
if (!consumerName) {
|
||||
consumerName = `windmill_consumer-${$workspaceStore}-${path.replaceAll('/', '__')}`
|
||||
}
|
||||
}
|
||||
|
||||
function setNewArgs(args: Record<string, any>) {
|
||||
subjects = args.subjects
|
||||
useJetstream = args.use_jetstream
|
||||
streamName = args.stream_name
|
||||
consumerName = args.consumer_name
|
||||
if (args.use_jetstream) {
|
||||
setStreamAndConsumerNames()
|
||||
}
|
||||
}
|
||||
|
||||
function getNatsArgsCfg() {
|
||||
return {
|
||||
subjects,
|
||||
use_jetstream: useJetstream,
|
||||
stream_name: streamName,
|
||||
consumer_name: consumerName
|
||||
}
|
||||
}
|
||||
$: useJetStream = args.use_jetstream
|
||||
$: useJetStream && setStreamAndConsumerNames()
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if showCapture && captureInfo}
|
||||
<CaptureSection
|
||||
captureType="nats"
|
||||
disabled={!isValid}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
bind:captureTable
|
||||
/>
|
||||
{/if}
|
||||
<Section label="NATS" {headless}>
|
||||
{#snippet badge()}
|
||||
{#if showTestingBadge}
|
||||
<TestingBadge />
|
||||
{/if}
|
||||
{/snippet}
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<div class="block grow w-full">
|
||||
<Subsection label="Connection">
|
||||
<svelte:fragment slot="header">
|
||||
{#if !staticInputDisabled}
|
||||
<ToggleButtonGroup
|
||||
bind:selected
|
||||
class="h-full"
|
||||
on:selected={(ev) => {
|
||||
if (ev.detail === 'static') {
|
||||
delete args.nats_resource_path
|
||||
args.require_tls = false
|
||||
args.servers = ['']
|
||||
args.auth = {
|
||||
label: 'NO_AUTH'
|
||||
}
|
||||
} else {
|
||||
delete args.servers
|
||||
delete args.auth
|
||||
delete args.require_tls
|
||||
}
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="static" label="Static" small={true} {item} />
|
||||
<ToggleButton value="resource" label="Resource" icon={Boxes} small={true} {item} />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if selected === 'resource'}
|
||||
<ResourcePicker
|
||||
resourceType="nats"
|
||||
bind:value={args.nats_resource_path}
|
||||
{defaultValues}
|
||||
/>
|
||||
{:else}
|
||||
<SchemaForm
|
||||
schema={connnectionSchema}
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:isValid={isStaticConnectionValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
{/if}
|
||||
<ResourcePicker
|
||||
resourceType="nats"
|
||||
bind:value={natsResourcePath}
|
||||
{defaultValues}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{#if isConnectionValid}
|
||||
<TestTriggerConnection kind="nats" args={{ connection: args }} />
|
||||
<TestTriggerConnection
|
||||
kind="nats"
|
||||
args={{
|
||||
connection: {
|
||||
nats_resource_path: natsResourcePath
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Subsection>
|
||||
</div>
|
||||
@@ -291,17 +160,10 @@
|
||||
<Subsection headless={true}>
|
||||
<SchemaForm
|
||||
schema={argsSchema}
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:args={getNatsArgsCfg, (args) => setNewArgs(args)}
|
||||
bind:isValid={otherArgsValid}
|
||||
lightHeader={true}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
</Subsection>
|
||||
<span class="text-xs text-red-500">{globalError}</span>
|
||||
|
||||
@@ -1,137 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { NatsTriggerService, type NatsTrigger } from '$lib/gen'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import NatsTriggerEditor from './NatsTriggerEditor.svelte'
|
||||
import NatsTriggerEditorInner from './NatsTriggerEditorInner.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersEditorSection from '../TriggersEditorSection.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let newItem: boolean = false
|
||||
export let isEditor: boolean = false
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
isDeployed = false,
|
||||
defaultValues = undefined,
|
||||
customLabel = undefined,
|
||||
...props
|
||||
} = $props()
|
||||
let natsTriggerEditor: NatsTriggerEditorInner | undefined = $state(undefined)
|
||||
|
||||
let natsTriggerEditor: NatsTriggerEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
|
||||
$: path && loadTriggers()
|
||||
|
||||
const { triggersCount, selectedTrigger, defaultValues } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
async function openNatsTriggerEditor(isFlow: boolean, isDraft: boolean) {
|
||||
if (isDraft) {
|
||||
natsTriggerEditor?.openNew(isFlow, path, defaultValues)
|
||||
} else {
|
||||
natsTriggerEditor?.openEdit(selectedTrigger.path, isFlow, selectedTrigger.draftConfig)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (
|
||||
defaultValues &&
|
||||
$selectedTrigger === 'nats' &&
|
||||
Object.keys($defaultValues ?? {}).length > 0
|
||||
) {
|
||||
natsTriggerEditor.openNew(isFlow, path, $defaultValues)
|
||||
defaultValues.set(undefined)
|
||||
}
|
||||
natsTriggerEditor && openNatsTriggerEditor(isFlow, selectedTrigger.isDraft ?? false)
|
||||
})
|
||||
|
||||
let natsTriggers: (NatsTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
export async function loadTriggers() {
|
||||
try {
|
||||
natsTriggers = (
|
||||
await NatsTriggerService.listNatsTriggers({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
isFlow
|
||||
})
|
||||
).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), nats_count: natsTriggers?.length }
|
||||
openForm = natsTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load nats triggers', e)
|
||||
}
|
||||
}
|
||||
|
||||
let data = {
|
||||
natsTriggers: [],
|
||||
newItem
|
||||
}
|
||||
|
||||
function saveTrigger(path: string, args?: Record<string, any>) {
|
||||
natsTriggerEditor?.openNew(isFlow, path, args)
|
||||
}
|
||||
const cloudDisabled = $derived(isCloudHosted())
|
||||
</script>
|
||||
|
||||
<NatsTriggerEditor
|
||||
on:update={() => {
|
||||
loadTriggers()
|
||||
}}
|
||||
bind:this={natsTriggerEditor}
|
||||
/>
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert title="EE Only" type="warning" size="xs">
|
||||
Nats triggers are an enterprise only feature.
|
||||
</Alert>
|
||||
{:else if isCloudHosted()}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
Nats triggers are disabled in the multi-tenant cloud.
|
||||
NATS triggers are an enterprise only feature.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/nats_triggers">
|
||||
NATS triggers execute scripts and flows in response to messages published to NATS subjects.
|
||||
</Description>
|
||||
|
||||
{#if !newItem && natsTriggers && natsTriggers.length > 0}
|
||||
<Section label="NATS Triggers">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each natsTriggers as natsTrigger (natsTrigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{natsTrigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{natsTrigger.nats_resource_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => natsTriggerEditor?.openEdit(natsTrigger.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if natsTrigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<TriggersEditorSection
|
||||
on:saveTrigger={(e) => {
|
||||
saveTrigger(path, e.detail.config)
|
||||
}}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
cloudDisabled={false}
|
||||
triggerType="nats"
|
||||
{isFlow}
|
||||
{data}
|
||||
{path}
|
||||
{isEditor}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
{openForm}
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
/>
|
||||
<NatsTriggerEditorInner
|
||||
bind:this={natsTriggerEditor}
|
||||
useDrawer={false}
|
||||
hideTarget
|
||||
hideTooltips={!isDeployed}
|
||||
useEditButton
|
||||
allowDraft={true}
|
||||
hasDraft={!!selectedTrigger.draftConfig}
|
||||
isDraftOnly={selectedTrigger.isDraft}
|
||||
{customLabel}
|
||||
{isDeployed}
|
||||
{cloudDisabled}
|
||||
{...props}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if cloudDisabled}
|
||||
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
|
||||
NATS triggers are disabled in the multi-tenant cloud.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/nats_triggers">
|
||||
NATS triggers allow you to execute scripts and flows in response to NATS messages. They
|
||||
can be configured to listen to specific subjects and to use JetStream or not.
|
||||
</Description>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</NatsTriggerEditorInner>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NatsTriggerService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
|
||||
export async function saveNatsTriggerFromCfg(
|
||||
initialPath: string,
|
||||
cfg: Record<string, any>,
|
||||
edit: boolean,
|
||||
workspace: string,
|
||||
usedTriggerKinds: Writable<string[]>
|
||||
): Promise<boolean> {
|
||||
const requestBody = {
|
||||
path: cfg.path,
|
||||
script_path: cfg.script_path,
|
||||
is_flow: cfg.is_flow,
|
||||
nats_resource_path: cfg.nats_resource_path,
|
||||
stream_name: cfg.stream_name,
|
||||
consumer_name: cfg.consumer_name,
|
||||
subjects: cfg.subjects,
|
||||
use_jetstream: cfg.use_jetstream
|
||||
}
|
||||
try {
|
||||
if (edit) {
|
||||
await NatsTriggerService.updateNatsTrigger({
|
||||
workspace,
|
||||
path: initialPath,
|
||||
requestBody
|
||||
})
|
||||
sendUserToast(`Nats trigger ${cfg.path} updated`)
|
||||
} else {
|
||||
await NatsTriggerService.createNatsTrigger({
|
||||
workspace,
|
||||
requestBody: {
|
||||
...requestBody,
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
sendUserToast(`Nats trigger ${cfg.path} created`)
|
||||
}
|
||||
if (!get(usedTriggerKinds).includes('nats')) {
|
||||
usedTriggerKinds.update((t) => [...t, 'nats'])
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
|
||||
interface Props {
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
postgres_resource_path?: string
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
captureLoading?: boolean
|
||||
triggerDeployed?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
captureInfo = undefined,
|
||||
postgres_resource_path = '',
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
captureLoading = false,
|
||||
triggerDeployed = false
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="postgres"
|
||||
disabled={!postgres_resource_path}
|
||||
{captureInfo}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
displayAlert={triggerDeployed}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
{#if captureInfo.connectionInfo?.connected}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Listening to Postgres database transactions...
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Connecting to Postgres database...
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to Postgres database transactions.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -1,141 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import MultiSelect from 'svelte-multiselect'
|
||||
import RelationPicker from './RelationPicker.svelte'
|
||||
import type { PublicationData } from '$lib/gen'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import CheckPostgresRequirement from './CheckPostgresRequirement.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { X } from 'lucide-svelte'
|
||||
const DEFAULT_PUBLICATION: PublicationData = {
|
||||
transaction_to_track: ['Insert', 'Update', 'Delete'],
|
||||
table_to_track: [
|
||||
{
|
||||
schema_name: 'public',
|
||||
table_to_track: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let transactionType: string[] = ['Insert', 'Update', 'Delete']
|
||||
let isValid: boolean = false
|
||||
|
||||
export let headless: boolean = false
|
||||
export let can_write: boolean = false
|
||||
export let showCapture: boolean = false
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let postgres_resource_path: string = ''
|
||||
export let publication: PublicationData = DEFAULT_PUBLICATION
|
||||
|
||||
function updateValidity(publication: PublicationData) {
|
||||
isValid =
|
||||
!emptyString(postgres_resource_path) &&
|
||||
(!publication.table_to_track || publication.table_to_track.length !== 0)
|
||||
}
|
||||
|
||||
$: updateValidity(publication)
|
||||
|
||||
let testTriggerConnection: TestTriggerConnection | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="h-full">
|
||||
{#if showCapture && captureInfo}
|
||||
<CaptureSection
|
||||
disabled={!isValid}
|
||||
on:captureToggle
|
||||
captureType="postgres"
|
||||
{captureInfo}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
/>
|
||||
{/if}
|
||||
<Section label="Postgres config" {headless}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="mb-2">
|
||||
<p class="text-xs mb-1 text-tertiary">
|
||||
Pick a database to connect to <Required required={true} />
|
||||
</p>
|
||||
<ResourcePicker
|
||||
disabled={!can_write}
|
||||
bind:value={postgres_resource_path}
|
||||
resourceType={'postgresql'}
|
||||
on:change={() => {
|
||||
if (emptyString(postgres_resource_path)) {
|
||||
publication = { ...DEFAULT_PUBLICATION }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if postgres_resource_path}
|
||||
<TestTriggerConnection
|
||||
kind="postgres"
|
||||
args={{ postgres_resource_path }}
|
||||
noButton
|
||||
bind:this={testTriggerConnection}
|
||||
/>
|
||||
<CheckPostgresRequirement
|
||||
bind:postgres_resource_path
|
||||
bind:can_write
|
||||
checkConnection={testTriggerConnection?.testTriggerConnection}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if postgres_resource_path}
|
||||
<Label label="Transactions">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip small>
|
||||
Choose the types of database transactions that should trigger a script or flow. You
|
||||
can select from <strong>Insert</strong>, <strong>Update</strong>,
|
||||
<strong>Delete</strong>, or any combination of these operations to define when the
|
||||
trigger should activate.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<MultiSelect
|
||||
noMatchingOptionsMsg=""
|
||||
createOptionMsg={null}
|
||||
duplicates={false}
|
||||
options={transactionType}
|
||||
allowUserOptions="append"
|
||||
bind:selected={publication.transaction_to_track}
|
||||
ulOptionsClass={'!bg-surface !text-sm'}
|
||||
ulSelectedClass="!text-sm"
|
||||
outerDivClass="!bg-surface !min-h-[38px] !border-[#d1d5db]"
|
||||
placeholder="Select transactions"
|
||||
--sms-options-margin="4px"
|
||||
>
|
||||
<svelte:fragment slot="remove-icon">
|
||||
<div class="hover:text-primary p-0.5">
|
||||
<X size={12} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</MultiSelect>
|
||||
</Label>
|
||||
<Label label="Table tracking">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/postgres_triggers#define-what-to-track"
|
||||
small
|
||||
>
|
||||
Select the tables to track. You can choose to track
|
||||
<strong>all tables in your database</strong>,
|
||||
<strong>all tables within a specific schema</strong>,
|
||||
<strong>specific tables in a schema</strong>, or even
|
||||
<strong>specific columns of a table</strong>. Additionally, you can apply a
|
||||
<strong>filter</strong> to retrieve only rows that do not match the specified criteria.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<RelationPicker bind:relations={publication.table_to_track} />
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
@@ -2,22 +2,28 @@
|
||||
import { tick } from 'svelte'
|
||||
import PostgresTriggerEditorInner from './PostgresTriggerEditorInner.svelte'
|
||||
|
||||
let open = false
|
||||
let { onUpdate } = $props()
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
open = true
|
||||
await tick()
|
||||
drawer?.openEdit(ePath, isFlow)
|
||||
}
|
||||
|
||||
export async function openNew(is_flow: boolean, initial_script_path?: string, defaultValues?: Record<string, any>) {
|
||||
export async function openNew(
|
||||
is_flow: boolean,
|
||||
initial_script_path?: string,
|
||||
defaultValues?: Record<string, any>
|
||||
) {
|
||||
open = true
|
||||
await tick()
|
||||
drawer?.openNew(is_flow, initial_script_path, defaultValues)
|
||||
}
|
||||
|
||||
let drawer: PostgresTriggerEditorInner
|
||||
let drawer: PostgresTriggerEditorInner | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<PostgresTriggerEditorInner on:update bind:this={drawer} />
|
||||
<PostgresTriggerEditorInner {onUpdate} bind:this={drawer} />
|
||||
{/if}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user