mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
283 lines
7.3 KiB
Svelte
283 lines
7.3 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation'
|
|
import { page } from '$app/stores'
|
|
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
|
|
import { userStore, workspaceStore } from '$lib/stores'
|
|
import {
|
|
encodeState,
|
|
formatCron,
|
|
loadHubScripts,
|
|
sendUserToast,
|
|
setQueryWithoutLoad
|
|
} from '$lib/utils'
|
|
import { faCalendarAlt, faEye, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
|
import { setContext } from 'svelte'
|
|
import { Icon } from 'svelte-awesome'
|
|
import { writable } from 'svelte/store'
|
|
import CenteredPage from './CenteredPage.svelte'
|
|
import { Badge, Button, Drawer, DrawerContent } from './common'
|
|
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
|
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
|
import { OFFSET } from './CronInput.svelte'
|
|
import FlowGraphViewer from './FlowGraphViewer.svelte'
|
|
import FlowEditor from './flows/FlowEditor.svelte'
|
|
import { flowStateStore } from './flows/flowState'
|
|
import { flowStore } from './flows/flowStore'
|
|
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
|
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
|
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
|
|
import type { FlowEditorContext } from './flows/types'
|
|
import { cleanInputs } from './flows/utils'
|
|
|
|
export let initialPath: string = ''
|
|
export let selectedId: string | undefined
|
|
export let initialArgs: Record<string, any> = {}
|
|
export let loading = false
|
|
|
|
let pathError = ''
|
|
|
|
async function createSchedule(path: string) {
|
|
const { cron, args, enabled } = $scheduleStore
|
|
|
|
try {
|
|
await ScheduleService.createSchedule({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
path: path,
|
|
schedule: formatCron(cron),
|
|
offset: OFFSET,
|
|
script_path: path,
|
|
is_flow: true,
|
|
args,
|
|
enabled
|
|
}
|
|
})
|
|
} catch (err) {
|
|
sendUserToast(`The primary schedule could not be created: ${err}`, true)
|
|
}
|
|
}
|
|
|
|
async function saveFlow(): Promise<void> {
|
|
const flow = cleanInputs($flowStore)
|
|
const { cron, args, enabled } = $scheduleStore
|
|
$dirtyStore = false
|
|
|
|
if (initialPath === '') {
|
|
await FlowService.createFlow({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
path: flow.path,
|
|
summary: flow.summary,
|
|
description: flow.description ?? '',
|
|
value: flow.value,
|
|
schema: flow.schema
|
|
}
|
|
})
|
|
if (enabled) {
|
|
await createSchedule(flow.path)
|
|
}
|
|
} else {
|
|
await FlowService.updateFlow({
|
|
workspace: $workspaceStore!,
|
|
path: initialPath,
|
|
requestBody: {
|
|
path: flow.path,
|
|
summary: flow.summary,
|
|
description: flow.description ?? '',
|
|
value: flow.value,
|
|
schema: flow.schema
|
|
}
|
|
})
|
|
const scheduleExists = await ScheduleService.existsSchedule({
|
|
workspace: $workspaceStore ?? '',
|
|
path: flow.path
|
|
})
|
|
if (scheduleExists) {
|
|
const schedule = await ScheduleService.getSchedule({
|
|
workspace: $workspaceStore ?? '',
|
|
path: flow.path
|
|
})
|
|
if (JSON.stringify(schedule.args) != JSON.stringify(args) || schedule.schedule != cron) {
|
|
await ScheduleService.updateSchedule({
|
|
workspace: $workspaceStore ?? '',
|
|
path: flow.path,
|
|
requestBody: {
|
|
schedule: formatCron(cron),
|
|
args
|
|
}
|
|
})
|
|
}
|
|
if (enabled != schedule.enabled) {
|
|
await ScheduleService.setScheduleEnabled({
|
|
workspace: $workspaceStore ?? '',
|
|
path: flow.path,
|
|
requestBody: { enabled }
|
|
})
|
|
}
|
|
} else if (enabled) {
|
|
await createSchedule(flow.path)
|
|
}
|
|
}
|
|
sendUserToast(`Success! flow saved at ${$flowStore.path}`)
|
|
goto(`/flows/get/${$flowStore.path}`)
|
|
}
|
|
|
|
let timeout: NodeJS.Timeout | undefined = undefined
|
|
|
|
$: {
|
|
if ($flowStore && $flowStateStore) {
|
|
setUrl()
|
|
}
|
|
}
|
|
|
|
function setUrl() {
|
|
timeout && clearTimeout(timeout)
|
|
timeout = setTimeout(
|
|
() =>
|
|
setQueryWithoutLoad(
|
|
$page.url,
|
|
'state',
|
|
encodeState({
|
|
flow: $flowStore,
|
|
selectedId: $selectedIdStore
|
|
})
|
|
),
|
|
500
|
|
)
|
|
}
|
|
|
|
const selectedIdStore = writable<string>(selectedId)
|
|
|
|
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
|
|
const previewArgsStore = writable<Record<string, any>>(initialArgs)
|
|
|
|
function select(selectedId: string) {
|
|
selectedIdStore.set(selectedId)
|
|
}
|
|
|
|
setContext<FlowEditorContext>('FlowEditorContext', {
|
|
selectedId: selectedIdStore,
|
|
schedule: scheduleStore,
|
|
select,
|
|
previewArgs: previewArgsStore
|
|
})
|
|
|
|
async function loadSchedule() {
|
|
loadFlowSchedule(initialPath, $workspaceStore)
|
|
.then((schedule: Schedule) => {
|
|
scheduleStore.set(schedule)
|
|
})
|
|
.catch(() => {
|
|
scheduleStore.set({
|
|
cron: '0 */5 * * *',
|
|
args: {},
|
|
enabled: false
|
|
})
|
|
})
|
|
}
|
|
|
|
$: selectedId && select(selectedId)
|
|
|
|
$: initialPath && $workspaceStore && loadSchedule()
|
|
|
|
loadHubScripts()
|
|
|
|
let flowViewer: Drawer
|
|
</script>
|
|
|
|
{#if !$userStore?.operator}
|
|
<UnsavedConfirmationModal />
|
|
|
|
<Drawer bind:this={flowViewer} size="75%">
|
|
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
|
|
<div class="overflow-hidden h-full w-full">
|
|
<FlowGraphViewer flow={$flowStore} />
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<div class="flex flex-col flex-1 h-screen">
|
|
<!-- Nav between steps-->
|
|
<div
|
|
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
|
|
>
|
|
<div class="flex flex-row">
|
|
<FlowImportExportMenu />
|
|
<Button
|
|
btnClasses="inline-flex"
|
|
startIcon={{ icon: faEye }}
|
|
variant="border"
|
|
color="light"
|
|
size="sm"
|
|
on:click={flowViewer.openDrawer}
|
|
>
|
|
Graph
|
|
</Button>
|
|
</div>
|
|
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
|
|
{#if $scheduleStore.enabled}
|
|
<Button
|
|
btnClasses="hidden lg:inline-flex"
|
|
startIcon={{ icon: faCalendarAlt }}
|
|
variant="contained"
|
|
color="light"
|
|
size="xs"
|
|
on:click={async () => {
|
|
select('settings-schedule')
|
|
}}
|
|
>
|
|
{$scheduleStore.cron ?? ''}
|
|
</Button>
|
|
{/if}
|
|
<Button
|
|
btnClasses="hidden lg:inline-flex"
|
|
startIcon={{ icon: faPen }}
|
|
variant="contained"
|
|
color="light"
|
|
size="xs"
|
|
on:click={async () => {
|
|
select('settings-metadata')
|
|
document.getElementById('path')?.focus()
|
|
}}
|
|
>
|
|
{$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
|
|
</Button>
|
|
<Button
|
|
startIcon={{ icon: faPen }}
|
|
variant="contained"
|
|
color="light"
|
|
size="xs"
|
|
on:click={async () => {
|
|
select('settings-metadata')
|
|
document.getElementById('flow-summary')?.focus()
|
|
}}
|
|
>
|
|
<div class="max-w-[10em] !truncate">
|
|
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
|
|
</div>
|
|
</Button>
|
|
</div>
|
|
<div class="flex flex-row space-x-2">
|
|
<FlowPreviewButtons />
|
|
<div class="center-center">
|
|
<Button
|
|
disabled={pathError != ''}
|
|
startIcon={{ icon: faSave }}
|
|
size="sm"
|
|
on:click={saveFlow}>Save</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- metadata -->
|
|
{#if $flowStateStore}
|
|
<FlowEditor {initialPath} {loading} />
|
|
{:else}
|
|
<CenteredPage>Loading...</CenteredPage>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
Flow Builder not available to operators
|
|
{/if}
|