mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
feat: add schedule settable from pull flows
This commit is contained in:
@@ -3793,6 +3793,7 @@ components:
|
||||
- offset_
|
||||
- extra_perms
|
||||
- is_flow
|
||||
- enabled
|
||||
|
||||
NewSchedule:
|
||||
type: object
|
||||
@@ -3809,6 +3810,8 @@ components:
|
||||
type: boolean
|
||||
args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- schedule
|
||||
|
||||
+10
-3
@@ -64,6 +64,7 @@ pub struct NewSchedule {
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn push_scheduled_job<'c>(
|
||||
@@ -133,7 +134,7 @@ async fn create_schedule(
|
||||
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
|
||||
|
||||
let schedule = sqlx::query_as!(Schedule,
|
||||
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *",
|
||||
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *",
|
||||
w_id,
|
||||
ns.path,
|
||||
ns.schedule,
|
||||
@@ -141,7 +142,8 @@ async fn create_schedule(
|
||||
&authed.username,
|
||||
ns.script_path,
|
||||
ns.is_flow,
|
||||
ns.args
|
||||
ns.args,
|
||||
ns.enabled
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
@@ -165,8 +167,13 @@ async fn create_schedule(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tx = push_scheduled_job(tx, schedule).await?;
|
||||
let tx = if ns.enabled.unwrap_or(true) {
|
||||
push_scheduled_job(tx, schedule).await?
|
||||
} else {
|
||||
tx
|
||||
};
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(ns.path.to_string())
|
||||
}
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
{disabled}
|
||||
class="default-button-secondary items-center leading-4 py-0 my-px px-1 float-right"
|
||||
on:click={() => (value = undefined)}
|
||||
>Reset<Tooltip>Reset to default value</Tooltip></button
|
||||
>Reset <Tooltip>Reset to default value</Tooltip></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
let preview: string[] = []
|
||||
let cronError = ''
|
||||
export let schedule: string = '0 0 12 * *'
|
||||
let limit = 3
|
||||
|
||||
$: handleScheduleInput(schedule)
|
||||
|
||||
@@ -69,11 +70,17 @@
|
||||
<CollapseLink text="preview next runs" open={true}>
|
||||
{#if preview && preview.length > 0}
|
||||
<div class="text-sm text-gray-700 border p-2 rounded-md">
|
||||
<div class="flex flex-row justify-between">The next 10 runs will be scheduled at:</div>
|
||||
<div class="flex flex-row justify-between">The next runs will be scheduled at:</div>
|
||||
<ul class="list-disc mx-12">
|
||||
{#each preview as p}
|
||||
{#each preview.slice(0, limit) as p}
|
||||
<li class="mx-2 text-gray-700 text-sm">{displayDate(p)}</li>
|
||||
{/each}
|
||||
<li class="text-sm mx-2">...</li>
|
||||
{#if limit != 10}
|
||||
<button class="underline text-gray-400" on:click={() => (limit = 10)}>Load more</button>
|
||||
{:else}
|
||||
<button class="underline text-gray-400" on:click={() => (limit = 3)}>Load less</button>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -252,9 +252,7 @@
|
||||
(lastWsAttempt.getTime() - new Date().getTime() > 60000 && nbWsAttempt < 2)
|
||||
) {
|
||||
if (!websocketAlive.black && !websocketAlive.deno && !websocketAlive.pyright) {
|
||||
sendUserToast(
|
||||
'Smart assistant got disconnected. Reconnecting to windmill language server for smart assistance'
|
||||
)
|
||||
console.log('reconnecting to language servers')
|
||||
lastWsAttempt = new Date()
|
||||
nbWsAttempt++
|
||||
reloadWebsocket()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { FlowService, type Flow } from '$lib/gen'
|
||||
import { FlowService, ScheduleService, ScriptService, type Flow } from '$lib/gen'
|
||||
import { clearPreviewResults, hubScripts, workspaceStore } from '$lib/stores'
|
||||
import { loadHubScripts, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
|
||||
import { formatCron, loadHubScripts, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
|
||||
import { onMount } from 'svelte'
|
||||
import { OFFSET } from './CronInput.svelte'
|
||||
import FlowEditor from './FlowEditor.svelte'
|
||||
import { flowStore, type FlowMode } from './flows/flowStore'
|
||||
import { flowStore, mode } from './flows/flowStore'
|
||||
import { flowToMode } from './flows/utils'
|
||||
|
||||
import ScriptSchema from './ScriptSchema.svelte'
|
||||
@@ -14,36 +15,90 @@
|
||||
export let initialPath: string = ''
|
||||
let pathError = ''
|
||||
|
||||
let mode: FlowMode
|
||||
let scheduleArgs: Record<string, any>
|
||||
let scheduleEnabled
|
||||
let scheduleCron: string
|
||||
|
||||
$: step = Number($page.url.searchParams.get('step')) || 1
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: path,
|
||||
schedule: formatCron(scheduleCron),
|
||||
offset: OFFSET,
|
||||
script_path: path,
|
||||
is_flow: true,
|
||||
args: scheduleArgs,
|
||||
enabled: scheduleEnabled
|
||||
}
|
||||
})
|
||||
}
|
||||
async function saveFlow(): Promise<void> {
|
||||
const newFlow = flowToMode($flowStore, mode)
|
||||
const flow = flowToMode($flowStore, $mode)
|
||||
|
||||
if (initialPath === '') {
|
||||
await FlowService.createFlow({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: newFlow.path,
|
||||
summary: newFlow.summary,
|
||||
description: newFlow.description ?? '',
|
||||
value: newFlow.value,
|
||||
schema: newFlow.schema
|
||||
path: flow.path,
|
||||
summary: flow.summary,
|
||||
description: flow.description ?? '',
|
||||
value: flow.value,
|
||||
schema: flow.schema
|
||||
}
|
||||
})
|
||||
if ($mode == 'pull') {
|
||||
await createSchedule(flow.path)
|
||||
}
|
||||
} else {
|
||||
await FlowService.updateFlow({
|
||||
workspace: $workspaceStore!,
|
||||
path: newFlow.path,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
path: newFlow.path,
|
||||
summary: newFlow.summary,
|
||||
description: newFlow.description ?? '',
|
||||
value: newFlow.value,
|
||||
schema: newFlow.schema
|
||||
path: flow.path,
|
||||
summary: flow.summary,
|
||||
description: flow.description ?? '',
|
||||
value: flow.value,
|
||||
schema: flow.schema
|
||||
}
|
||||
})
|
||||
const scheduleExists = await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
})
|
||||
if (scheduleExists) {
|
||||
const schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
})
|
||||
if (
|
||||
schedule.path != flow.path ||
|
||||
JSON.stringify(schedule.args) != JSON.stringify(scheduleArgs) ||
|
||||
schedule.schedule != scheduleCron
|
||||
) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatCron(scheduleCron),
|
||||
script_path: flow.path,
|
||||
is_flow: true,
|
||||
args: scheduleArgs
|
||||
}
|
||||
})
|
||||
}
|
||||
if (scheduleEnabled != schedule.enabled) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: flow.path,
|
||||
requestBody: { enabled: scheduleEnabled }
|
||||
})
|
||||
}
|
||||
} else {
|
||||
await createSchedule(flow.path)
|
||||
}
|
||||
}
|
||||
sendUserToast(`Success! flow saved at ${$flowStore.path}`)
|
||||
goto(`/flows/get/${$flowStore.path}`)
|
||||
@@ -54,7 +109,7 @@
|
||||
}
|
||||
|
||||
flowStore.subscribe((flow: Flow) => {
|
||||
setQueryWithoutLoad($page.url, 'state', btoa(JSON.stringify(flowToMode(flow, mode))))
|
||||
setQueryWithoutLoad($page.url, 'state', btoa(JSON.stringify(flowToMode(flow, $mode))))
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -121,7 +176,13 @@
|
||||
<!-- metadata -->
|
||||
|
||||
{#if step === 1}
|
||||
<FlowEditor bind:mode bind:pathError bind:initialPath />
|
||||
<FlowEditor
|
||||
bind:pathError
|
||||
bind:initialPath
|
||||
bind:scheduleEnabled
|
||||
bind:scheduleCron
|
||||
bind:scheduleArgs
|
||||
/>
|
||||
{:else if step === 2}
|
||||
<ScriptSchema
|
||||
synchronizedHeader={false}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { FlowModuleValue } from '$lib/gen'
|
||||
import { ScheduleService } from '$lib/gen'
|
||||
|
||||
import { faFileExport, faFileImport, faGlobe, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import FlowPreview from './FlowPreview.svelte'
|
||||
import CopyFirstStepSchema from './flows/CopyFirstStepSchema.svelte'
|
||||
import { addModule, flowStore, initFlow, type FlowMode } from './flows/flowStore'
|
||||
import { addModule, flowStore, initFlow, mode } from './flows/flowStore'
|
||||
import ModuleStep from './ModuleStep.svelte'
|
||||
import Path from './Path.svelte'
|
||||
import RadioButtonV2 from './RadioButtonV2.svelte'
|
||||
@@ -20,21 +20,41 @@
|
||||
import CronInput from './CronInput.svelte'
|
||||
import CollapseLink from './CollapseLink.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { loadSchema } from '$lib/scripts'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let pathError = ''
|
||||
export let initialPath: string = ''
|
||||
export let mode: FlowMode =
|
||||
$flowStore?.value.modules[1]?.value.type == FlowModuleValue.type.FORLOOPFLOW ? 'pull' : 'push'
|
||||
|
||||
let allowSchedule = false
|
||||
let cronSchedule: string | undefined
|
||||
export let scheduleArgs: Record<string, any> = {}
|
||||
export let scheduleEnabled = false
|
||||
export let scheduleCron: string = '0 */5 * * *'
|
||||
|
||||
let jsonSetter: Modal
|
||||
let jsonViewer: Modal
|
||||
|
||||
let jsonValue: string = ''
|
||||
|
||||
async function loadSchedule() {
|
||||
try {
|
||||
const schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
})
|
||||
scheduleEnabled = schedule.enabled
|
||||
scheduleCron = schedule.schedule
|
||||
scheduleArgs = scheduleArgs
|
||||
console.log(schedule.enabled, schedule.schedule)
|
||||
} catch (e) {
|
||||
console.log(`no primary schedule found for ${initialPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($workspaceStore && initialPath != '') {
|
||||
loadSchedule()
|
||||
}
|
||||
|
||||
let open = 0
|
||||
let args: Record<string, any> = {}
|
||||
</script>
|
||||
@@ -63,7 +83,7 @@
|
||||
<Modal bind:this={jsonViewer}>
|
||||
<div slot="title">See JSON</div>
|
||||
<div slot="content" class="h-full">
|
||||
<FlowViewer flow={flowToMode($flowStore, mode)} tab="json" />
|
||||
<FlowViewer flow={flowToMode($flowStore, $mode)} tab="json" />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -106,7 +126,7 @@
|
||||
}
|
||||
url.searchParams.append(
|
||||
'flow',
|
||||
btoa(JSON.stringify(flowToMode(openFlow, mode)))
|
||||
btoa(JSON.stringify(flowToMode(openFlow, $mode)))
|
||||
)
|
||||
window.open(url, '_blank')?.focus()
|
||||
}
|
||||
@@ -154,35 +174,48 @@
|
||||
options={[
|
||||
[
|
||||
{
|
||||
title: 'Push',
|
||||
title: 'UI or webhook triggered',
|
||||
desc: 'Trigger this flow through the generated UI, a manual schedule or by calling the associated webhook'
|
||||
},
|
||||
'push'
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Pull',
|
||||
title: 'Watching changes regularly',
|
||||
desc: 'The first module of this flow is a trigger script whose purpose is to pull data from an external source and return all new items since last run. This flow is meant to be scheduled very regularly to reduce latency to react to new events. It will trigger the rest of the flow once per item. If no new items, the flow will be skipped.'
|
||||
},
|
||||
'pull'
|
||||
]
|
||||
]}
|
||||
bind:value={mode}
|
||||
bind:value={$mode}
|
||||
/>
|
||||
<div class="p-4 hidden">
|
||||
<CollapseLink text="set primary schedule" open={mode == 'pull'}>
|
||||
<Toggle
|
||||
bind:value={allowSchedule}
|
||||
options={{
|
||||
left: { label: 'disabled', value: false },
|
||||
right: { label: 'enabled', value: true }
|
||||
}}
|
||||
/>
|
||||
<div class="p-2 mt-2 rounded" class:bg-gray-300={!allowSchedule}>
|
||||
<CronInput schedule={cronSchedule} />
|
||||
</div>
|
||||
</CollapseLink>
|
||||
</div>
|
||||
{#if $mode == 'pull'}
|
||||
<div class="p-4">
|
||||
<CollapseLink text="set primary schedule" open={true}>
|
||||
<Tooltip
|
||||
>The primary schedule of a flow is simply a schedule that has the same name as a
|
||||
flow. It can be set and enabled directly within the flow editor. "Watching for
|
||||
new changes" flows are meant to be watching regularly for new items in an
|
||||
external systems. The primary schedule purpose is there to set the periodicity
|
||||
at which you want this watcher to operate.
|
||||
</Tooltip>
|
||||
<Toggle
|
||||
bind:checked={scheduleEnabled}
|
||||
options={{
|
||||
left: { label: 'disabled', value: false },
|
||||
right: { label: 'enabled', value: true }
|
||||
}}
|
||||
/>
|
||||
<div class="p-2 mt-2 rounded" class:bg-gray-300={!scheduleEnabled}>
|
||||
{#if !scheduleEnabled}
|
||||
<span class="font-black">No next scheduled run when disabled</span>
|
||||
{/if}
|
||||
<CronInput bind:schedule={scheduleCron} />
|
||||
</div>
|
||||
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
|
||||
</CollapseLink>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
|
||||
@@ -194,10 +227,15 @@
|
||||
<CopyFirstStepSchema />
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<SchemaEditor schema={$flowStore.schema} />
|
||||
<SchemaEditor
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
schema={$flowStore.schema}
|
||||
/>
|
||||
<div class="my-4" />
|
||||
<FlowPreview
|
||||
{mode}
|
||||
mode={$mode}
|
||||
flow={$flowStore}
|
||||
i={$flowStore?.value.modules.length}
|
||||
bind:args
|
||||
@@ -219,7 +257,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<ModuleStep bind:open bind:mod bind:args {i} {mode} />
|
||||
<ModuleStep bind:open bind:mod bind:args {i} mode={$mode} />
|
||||
{/each}
|
||||
<li class="relative m-20 ">
|
||||
<div class="relative flex justify-center">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
class="flex flex-col default-secondary-button-v2 mb-2 grow"
|
||||
class:selected={value == val}
|
||||
>
|
||||
<h2 class="mb-2 whitespace-nowrap">{label.title} <Tooltip>{label.desc}</Tooltip></h2>
|
||||
<h2 class="mb-2">{label.title} <Tooltip>{label.desc}</Tooltip></h2>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let schema: Schema = emptySchema()
|
||||
|
||||
@@ -63,6 +66,7 @@
|
||||
}
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
function startEditArgument(argName: string): void {
|
||||
@@ -87,6 +91,7 @@
|
||||
delete schema.properties[argName]
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
dispatch('change', schema)
|
||||
} else {
|
||||
throw Error('Argument not found!')
|
||||
}
|
||||
|
||||
@@ -46,10 +46,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="mr-1">
|
||||
{#if kind == 'read'}
|
||||
<Badge tooltip={reason}>shared to you (read-only)</Badge>
|
||||
{:else if kind == 'write'}
|
||||
<Badge tooltip={reason}>shared to you</Badge>
|
||||
{/if}
|
||||
</span>
|
||||
{#if kind == 'read' || kind == 'write'}
|
||||
<span class="mr-1">
|
||||
{#if kind == 'read'}
|
||||
<Badge tooltip={reason}>shared to you (read-only)</Badge>
|
||||
{:else if kind == 'write'}
|
||||
<Badge tooltip={reason}>shared to you</Badge>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -7,10 +7,14 @@ import { createInlineScriptModuleFromPath, getFirstStepSchema, loadSchemaFromMod
|
||||
|
||||
export type FlowMode = 'push' | 'pull'
|
||||
|
||||
export const mode = writable<FlowMode>('push')
|
||||
export const flowStore = writable<Flow>(undefined)
|
||||
export const schemasStore = writable<Schema[]>([])
|
||||
|
||||
export function initFlow(flow: Flow) {
|
||||
const newMode = flow.value.modules[1]?.value.type === FlowModuleValue.type.FORLOOPFLOW ? 'pull' : 'push'
|
||||
mode.set(newMode)
|
||||
flow = flattenForloopFlows(flow, newMode)
|
||||
schemasStore.set([])
|
||||
flowStore.set(flow)
|
||||
// For each module in flow, we should load the corresponding schema
|
||||
@@ -19,6 +23,16 @@ export function initFlow(flow: Flow) {
|
||||
})
|
||||
}
|
||||
|
||||
export function flattenForloopFlows(flow: Flow, mode: FlowMode): Flow {
|
||||
let newFlow: Flow = JSON.parse(JSON.stringify(flow))
|
||||
if (mode == 'pull') {
|
||||
const oldModules = newFlow.value.modules[1].value.value?.modules ?? []
|
||||
newFlow.value.modules = newFlow.value.modules.slice(0, 1)
|
||||
newFlow.value.modules.push(...oldModules)
|
||||
}
|
||||
return newFlow
|
||||
}
|
||||
|
||||
export const isCopyFirstStepSchemaDisabled = derived(flowStore, (flow: Flow | undefined) => {
|
||||
if (flow) {
|
||||
const modules = flow.value.modules
|
||||
|
||||
@@ -35,15 +35,6 @@ export function flowToMode(flow: Flow | any, mode: FlowMode): Flow {
|
||||
return flow
|
||||
}
|
||||
|
||||
export function flattenForloopFlows(flow: Flow): Flow {
|
||||
let newFlow: Flow = JSON.parse(JSON.stringify(flow))
|
||||
if (newFlow.value.modules[1]?.value.type == FlowModuleValue.type.FORLOOPFLOW) {
|
||||
const oldModules = newFlow.value.modules[1].value.value?.modules ?? []
|
||||
newFlow.value.modules = newFlow.value.modules.slice(0, 1)
|
||||
newFlow.value.modules.push(...oldModules)
|
||||
}
|
||||
return newFlow
|
||||
}
|
||||
|
||||
export function getTypeAsString(arg: any): string {
|
||||
if (arg === null) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let level = 0
|
||||
export let isLast = true
|
||||
export let currentPath: string = ''
|
||||
export let pureViewer = false
|
||||
|
||||
const collapsedSymbol = '...'
|
||||
let keys: string | any[]
|
||||
@@ -67,6 +68,7 @@
|
||||
level={level + 1}
|
||||
isLast={index === keys.length - 1}
|
||||
currentPath={computeKey(key)}
|
||||
{pureViewer}
|
||||
on:select
|
||||
/>
|
||||
{:else}
|
||||
@@ -75,7 +77,9 @@
|
||||
<WarningMessage />
|
||||
{:else}
|
||||
<span> {JSON.stringify(json[key])}</span>
|
||||
<button class="ml-2 default-button-secondary py-0"> Select </button>
|
||||
{#if !pureViewer}
|
||||
<button class="ml-2 default-button-secondary py-0"> Select </button>
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -176,6 +176,10 @@ export function canWrite(
|
||||
return false
|
||||
}
|
||||
|
||||
export function defaultIfEmptyString(str: string | undefined, dflt: string): string {
|
||||
return str == undefined || str == '' ? dflt : str
|
||||
}
|
||||
|
||||
export function removeKeysWithEmptyValues(obj: any): any {
|
||||
Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {}))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { page } from '$app/stores'
|
||||
|
||||
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
|
||||
import { initFlow } from '$lib/components/flows/flowStore'
|
||||
import { initFlow, mode } from '$lib/components/flows/flowStore'
|
||||
import { FlowService, type Flow } from '$lib/gen'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
$page.url.searchParams.delete('hub')
|
||||
sendUserToast(`Flow has been loaded from hub flow id ${hubId}.`)
|
||||
}
|
||||
$mode = 'push'
|
||||
initFlow(flow)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { FlowService, type Flow } from '$lib/gen'
|
||||
import { FlowModuleValue, FlowService, type Flow } from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { flattenForloopFlows } from '$lib/components/flows/utils'
|
||||
import { initFlow } from '$lib/components/flows/flowStore'
|
||||
|
||||
const initialState = $page.url.searchParams.get('state')
|
||||
@@ -42,7 +41,6 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: flow.path
|
||||
})
|
||||
flow = flattenForloopFlows(flow)
|
||||
initialPath = flow.path
|
||||
initFlow(flow)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { FlowService, type Flow } from '$lib/gen'
|
||||
import { displayDaysAgo, canWrite } from '$lib/utils'
|
||||
import { FlowService, ScheduleService, type Flow, type Schedule } from '$lib/gen'
|
||||
import { displayDaysAgo, canWrite, sendUserToast, defaultIfEmptyString } from '$lib/utils'
|
||||
import Icon from 'svelte-awesome'
|
||||
import {
|
||||
faPlay,
|
||||
@@ -22,14 +22,17 @@
|
||||
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import FlowViewer from '$lib/components/FlowViewer.svelte'
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
|
||||
let flow: Flow | undefined
|
||||
let schedule: Schedule | undefined
|
||||
let can_write = false
|
||||
|
||||
let path = $page.params.path
|
||||
@@ -37,16 +40,41 @@
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadFlow(path)
|
||||
loadFlow()
|
||||
loadSchedule()
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveFlow(hash: string): Promise<void> {
|
||||
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
|
||||
loadFlow(path)
|
||||
async function loadSchedule() {
|
||||
try {
|
||||
schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('no primary schedule')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFlow(hash: string): Promise<void> {
|
||||
async function archiveFlow(): Promise<void> {
|
||||
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
|
||||
loadFlow()
|
||||
}
|
||||
|
||||
async function setScheduleEnabled(path: string, enabled: boolean): Promise<void> {
|
||||
try {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
path,
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
loadSchedule()
|
||||
} catch (err) {
|
||||
sendUserToast(`Cannot ` + enabled ? 'disable' : 'enable' + ` schedule: ${err}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFlow(): Promise<void> {
|
||||
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
|
||||
can_write = canWrite(flow.path, flow.extra_perms!, $userStore)
|
||||
}
|
||||
@@ -87,7 +115,7 @@
|
||||
icon: faArchive,
|
||||
type: 'delete',
|
||||
action: () => {
|
||||
flow?.path && archiveFlow(flow.path)
|
||||
flow?.path && archiveFlow()
|
||||
},
|
||||
disabled: flow.archived || !can_write
|
||||
}
|
||||
@@ -137,31 +165,46 @@
|
||||
{#if flow === undefined}
|
||||
<p>loading</p>
|
||||
{:else}
|
||||
<h2>{flow.summary}</h2>
|
||||
<p>Edited at {displayDaysAgo(flow.edited_at ?? '')} by {flow.edited_by}</p>
|
||||
|
||||
<div class="prose">
|
||||
<SvelteMarkdown source={defaultIfEmptyString(flow.description, 'No description')} />
|
||||
</div>
|
||||
{#if schedule}
|
||||
<div>
|
||||
<h2 class="text-gray-700 pb-1 mb-3 border-b">Primary Schedule</h2>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Enabled</h3>
|
||||
<Toggle
|
||||
checked={schedule.enabled}
|
||||
on:change={(e) => {
|
||||
if (can_write) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class:bg-gray-300={!schedule.enabled}>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Schedule</h3>
|
||||
{schedule.schedule}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Args</h3>
|
||||
<ObjectViewer json={schedule.args ?? {}} pureViewer={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if flow.archived}
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-orange-700 p-4" role="alert">
|
||||
<p class="font-bold">Archived</p>
|
||||
<p>This version was archived</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Edited at</h3>
|
||||
{displayDaysAgo(flow.edited_at ?? '')}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Last editor</h3>
|
||||
{flow.edited_by}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Summary</h3>
|
||||
{flow.summary}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Description</h3>
|
||||
<div class="prose mt-5">
|
||||
<SvelteMarkdown source={flow.description ?? ''} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Webhook to run this flow:</span>
|
||||
<Tooltip
|
||||
@@ -176,7 +219,7 @@
|
||||
></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 pb-1 mb-3 border-b">Flow</h3>
|
||||
<h2 class="text-gray-700 pb-1 mb-3 border-b">Flow</h2>
|
||||
<FlowViewer {flow} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
type ScheduleW = Schedule & { canWrite: boolean }
|
||||
|
||||
@@ -79,36 +80,31 @@
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each schedules as { path, edited_by, edited_at, schedule, offset_, enabled, script_path, is_flow, extra_perms, canWrite }}
|
||||
<tr class={enabled ? '' : 'bg-gray-200'}>
|
||||
<tr class={enabled ? '' : 'bg-gray-100'}>
|
||||
<td
|
||||
><a href="/schedule/add?edit={path}&isFlow={is_flow}" style="cursor: pointer;"
|
||||
>{path}</a
|
||||
>
|
||||
<div>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</td>
|
||||
<td
|
||||
>{script_path}<span class="text-2xs text-gray-500 bg-gray-100 font-mono ml-2"
|
||||
<td class="whitespace-nowrap"
|
||||
><a href="{is_flow ? '/flows/get' : '/scripts/get'}/{script_path}">{script_path}</a
|
||||
><span class="text-2xs text-gray-500 bg-gray-100 font-mono ml-2"
|
||||
>{is_flow ? 'flow' : 'script'}</span
|
||||
></td
|
||||
>
|
||||
<td>{schedule}</td>
|
||||
<td
|
||||
><a
|
||||
id="toggle-{path}"
|
||||
style="cursor: pointer;"
|
||||
on:click={() => {
|
||||
<td>
|
||||
<Toggle
|
||||
checked={enabled}
|
||||
on:change={(e) => {
|
||||
if (canWrite) {
|
||||
setScheduleEnabled(path, enabled ? false : true)
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
><span class="m-auto block text-center">
|
||||
<Icon data={enabled ? faToggleOn : faToggleOff} scale={1.5} />
|
||||
</span></a
|
||||
></td
|
||||
/></td
|
||||
>
|
||||
<td>{offset_ < 0 ? '+' : ''}{(offset_ / 60) * -1}</td>
|
||||
<td class="text-2xs">By {edited_by} <br />at {displayDate(edited_at)}</td>
|
||||
@@ -171,4 +167,7 @@
|
||||
/>
|
||||
|
||||
<style>
|
||||
td {
|
||||
@apply px-2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { ScriptService, type Script } from '$lib/gen'
|
||||
import { truncateHash, sendUserToast, displayDaysAgo, canWrite } from '$lib/utils'
|
||||
import {
|
||||
truncateHash,
|
||||
sendUserToast,
|
||||
displayDaysAgo,
|
||||
canWrite,
|
||||
defaultIfEmptyString
|
||||
} from '$lib/utils'
|
||||
import Icon from 'svelte-awesome'
|
||||
import {
|
||||
faPlay,
|
||||
@@ -230,6 +236,13 @@
|
||||
{#if script === undefined}
|
||||
<p>loading</p>
|
||||
{:else}
|
||||
<h2>{script.summary}</h2>
|
||||
<p>Edited at {displayDaysAgo(script.created_at ?? '')} by {script.created_by}</p>
|
||||
|
||||
<div class="prose">
|
||||
<SvelteMarkdown source={defaultIfEmptyString(script.description, 'No description')} />
|
||||
</div>
|
||||
|
||||
{#if script.lock_error_logs}
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
|
||||
<p class="font-bold">Error deploying this script</p>
|
||||
@@ -259,24 +272,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Edited at</h3>
|
||||
{displayDaysAgo(script.created_at ?? '')}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Last editor</h3>
|
||||
{script.created_by}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Summary</h3>
|
||||
{script.summary}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700 ">Description</h3>
|
||||
<div class="prose mt-5">
|
||||
<SvelteMarkdown source={script.description ?? ''} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>
|
||||
Current hash <Tooltip
|
||||
@@ -288,32 +283,38 @@
|
||||
<p class="text-gray-700">
|
||||
<a href="/scripts/get/{script?.hash}">{script?.hash}</a>
|
||||
</p>
|
||||
<span>Webhook to run this script and get job's uuid as response:</span>
|
||||
<Tooltip
|
||||
>Send a POST http request with a token as bearer token and the args respecting the
|
||||
corresponding jsonschema as payload. To create a permanent token, go to your user setting
|
||||
by clicking your username on the top-left. For more info about openapi, see <a
|
||||
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
|
||||
></Tooltip
|
||||
>
|
||||
<h3 class="whitespace-nowrap mt-2">
|
||||
Webhook to run this script and get job's uuid as response
|
||||
<Tooltip
|
||||
>Send a POST http request with a token as bearer token and the args respecting the
|
||||
corresponding jsonschema as payload. To create a permanent token, go to your user
|
||||
setting by clicking your username on the top-left. For more info about openapi, see <a
|
||||
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
|
||||
></Tooltip
|
||||
>
|
||||
</h3>
|
||||
|
||||
<pre><code
|
||||
><a href="/api/w/{$workspaceStore}/jobs/run/h/{script?.hash}"
|
||||
>By hash: <a href="/api/w/{$workspaceStore}/jobs/run/h/{script?.hash}"
|
||||
>/api/w/{$workspaceStore}/jobs/run/h/{script?.hash}</a
|
||||
></code
|
||||
></pre>
|
||||
<pre><code
|
||||
><a href="/api/w/{$workspaceStore}/jobs/run/p/{script?.path}"
|
||||
>By path: <a href="/api/w/{$workspaceStore}/jobs/run/p/{script?.path}"
|
||||
>/api/w/{$workspaceStore}/jobs/run/p/{script?.path}</a
|
||||
></code
|
||||
></pre>
|
||||
<span>Endpoint to run this script and get job's result as response:</span>
|
||||
<Tooltip
|
||||
>Send a POST http request with a token as bearer token and the args respecting the
|
||||
corresponding jsonschema as payload. To create a permanent token, go to your user setting
|
||||
by clicking your username on the top-left. For more info about openapi, see <a
|
||||
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
|
||||
></Tooltip
|
||||
>
|
||||
<h3 class="whitespace-nowrap mt-2">
|
||||
Endpoint to run this script and get job's result as response
|
||||
<Tooltip
|
||||
>Send a POST http request with a token as bearer token and the args respecting the
|
||||
corresponding jsonschema as payload. To create a permanent token, go to your user
|
||||
setting by clicking your username on the top-left. For more info about openapi, see <a
|
||||
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
|
||||
></Tooltip
|
||||
>
|
||||
</h3>
|
||||
|
||||
<pre><code
|
||||
><a href="/api/w/{$workspaceStore}/jobs/run_wait_result/p/{script?.path}"
|
||||
>/api/w/{$workspaceStore}/jobs/run_wait_result/p/{script?.path}</a
|
||||
@@ -321,8 +322,8 @@
|
||||
></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-gray-700">
|
||||
Previous versions of this hash<Tooltip
|
||||
<h3>
|
||||
Previous versions of this hash <Tooltip
|
||||
>When you edit a script, a new hash is created and old versions are archived</Tooltip
|
||||
>
|
||||
</h3>
|
||||
@@ -361,3 +362,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
@apply text-lg mb-2 mt-4 text-gray-600;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user