mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
major schedules rewrite
This commit is contained in:
@@ -20,7 +20,6 @@
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import autosize from 'svelte-autosize'
|
||||
import * as autosizeLib from 'autosize'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Password from './Password.svelte'
|
||||
import type VariableEditor from './VariableEditor.svelte'
|
||||
@@ -296,6 +295,7 @@
|
||||
{#if properties && Object.keys(properties).length > 0}
|
||||
<div class="p-4 pl-8 border rounded w-full">
|
||||
<SchemaForm
|
||||
{disabled}
|
||||
schema={{ properties, $schema: '', required: [], type: 'object' }}
|
||||
bind:args={value}
|
||||
/>
|
||||
|
||||
@@ -11,11 +11,15 @@
|
||||
faUser,
|
||||
faBarsStaggered
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import ScheduleEditor from './ScheduleEditor.svelte'
|
||||
|
||||
export let job: Job
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
let scheduleEditor: ScheduleEditor
|
||||
</script>
|
||||
|
||||
<ScheduleEditor bind:this={scheduleEditor} />
|
||||
|
||||
<div class="rounded-md p-3 bg-gray-50 shadow-sm sm:text-sm md:text-base" style="min-height: 150px;">
|
||||
<JobStatus {job} />
|
||||
<div>
|
||||
@@ -46,9 +50,10 @@
|
||||
{:else if job && job.schedule_path}
|
||||
<Icon class="text-gray-700" data={faCalendar} scale={SMALL_ICON_SCALE} />
|
||||
<span
|
||||
>Triggered by the schedule: <a
|
||||
href={`/schedule/add?edit=${job.schedule_path}&isFlow=${job.job_kind == 'flow'}`}
|
||||
>{job.schedule_path}</a
|
||||
>Triggered by the schedule: <button
|
||||
class="break-words text-sm text-blue-600 font-normal"
|
||||
on:click={() => scheduleEditor?.openEdit(job.schedule_path ?? '', job.job_kind == 'flow')}
|
||||
>{job.schedule_path}</button
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
} from '$lib/gen'
|
||||
import { GroupService } from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Required from './Required.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
@@ -28,6 +27,7 @@
|
||||
export let path = ''
|
||||
export let error = ''
|
||||
export let disabled = false
|
||||
export let checkInitialPathExistence = false
|
||||
|
||||
export let kind: PathKind
|
||||
|
||||
@@ -105,7 +105,10 @@
|
||||
clearTimeout(validateTimeout)
|
||||
}
|
||||
validateTimeout = setTimeout(async () => {
|
||||
if ((path == '' || path != initialPath) && (await pathExists(path, kind))) {
|
||||
if (
|
||||
(path == '' || checkInitialPathExistence || path != initialPath) &&
|
||||
(await pathExists(path, kind))
|
||||
) {
|
||||
error = 'path already used'
|
||||
} else if (meta && validateName(meta)) {
|
||||
error = ''
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export let label = ''
|
||||
export let options: [string | { title: string; desc: string }, any][]
|
||||
export let value: any
|
||||
export let disabled = false
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
@@ -19,6 +20,7 @@
|
||||
{val === value ? '!bg-blue-50 !border-blue-500' : ''}"
|
||||
>
|
||||
<input
|
||||
{disabled}
|
||||
type="radio"
|
||||
value={val}
|
||||
class="sr-only"
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<script context="module">
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'New Schedule' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { sendUserToast, formatCron, canWrite } from '$lib/utils'
|
||||
import { ScriptService, Script, ScheduleService, type Flow, FlowService } from '$lib/gen'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { Alert, Badge, Button } from '$lib/components/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import CronInput, { OFFSET } from '$lib/components/CronInput.svelte'
|
||||
import { faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
let schedule: string = '0 0 12 * *'
|
||||
|
||||
let itemKind: 'flow' | 'script' = 'script'
|
||||
|
||||
let script_path = ''
|
||||
let initialScriptPath = ''
|
||||
|
||||
export function openEdit(ePath: string, isFlow: boolean) {
|
||||
is_flow = isFlow
|
||||
initialPath = ePath
|
||||
itemKind = is_flow ? 'flow' : 'script'
|
||||
path = ePath
|
||||
edit = true
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
export function openNew(is_flow: boolean, initial_script_path?: string) {
|
||||
edit = false
|
||||
itemKind = is_flow ? 'flow' : 'script'
|
||||
initialScriptPath = initial_script_path ?? ''
|
||||
path = initialScriptPath
|
||||
initialPath = initialScriptPath
|
||||
script_path = initialScriptPath
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
$: is_flow = itemKind == 'flow'
|
||||
|
||||
let runnable: Script | Flow | undefined
|
||||
let args: Record<string, any> = {}
|
||||
|
||||
let isValid = true
|
||||
|
||||
let path: string = ''
|
||||
let enabled: boolean = false
|
||||
let pathError = ''
|
||||
|
||||
let validCRON = true
|
||||
$: allowSchedule = isValid && validCRON && script_path != ''
|
||||
|
||||
$: script_path != '' && loadScript(script_path)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function loadScript(p: string | undefined): Promise<void> {
|
||||
if (p) {
|
||||
if (is_flow) {
|
||||
runnable = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path: p })
|
||||
} else {
|
||||
runnable = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: p })
|
||||
}
|
||||
} else {
|
||||
runnable = undefined
|
||||
}
|
||||
}
|
||||
|
||||
let can_write = true
|
||||
async function loadSchedule(): Promise<void> {
|
||||
try {
|
||||
const s = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
enabled = s.enabled
|
||||
schedule = s.schedule
|
||||
script_path = s.script_path ?? ''
|
||||
is_flow = s.is_flow
|
||||
args = s.args ?? {}
|
||||
can_write = canWrite(s.path, s.extra_perms, $userStore)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load schedule: ${err}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function scheduleScript(): Promise<void> {
|
||||
if (edit) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatCron(schedule),
|
||||
args
|
||||
}
|
||||
})
|
||||
sendUserToast(`Schedule ${path} updated`)
|
||||
} else {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
schedule: formatCron(schedule),
|
||||
offset: OFFSET,
|
||||
script_path,
|
||||
is_flow,
|
||||
args,
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
sendUserToast(`Schedule ${path} created`)
|
||||
}
|
||||
dispatch('update')
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
if (edit && path != '') {
|
||||
loadSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let drawer: Drawer
|
||||
</script>
|
||||
|
||||
<Drawer size="900px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit ? `Edit ${initialPath}` : 'New schedule'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if edit}
|
||||
<div class="mr-8">
|
||||
<Toggle
|
||||
disabled={!can_write}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
on:change={async (e) => {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
startIcon={{ icon: faSave }}
|
||||
disabled={!allowSchedule || pathError != ''}
|
||||
on:click={scheduleScript}
|
||||
>
|
||||
{edit ? 'Save' : 'Schedule'}
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div>
|
||||
{#if !edit}
|
||||
<span class="font-semibold text-gray-700">Path</span>
|
||||
<Path
|
||||
checkInitialPathExistence
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
namePlaceholder={'my_schedule'}
|
||||
kind="schedule"
|
||||
/>
|
||||
<div class="mb-8" />
|
||||
{/if}
|
||||
|
||||
<h2 class="border-b pb-1 mb-2">
|
||||
<span class="mr-1">Schedule</span>
|
||||
<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
|
||||
</h2>
|
||||
<CronInput disabled={!can_write} bind:schedule bind:validCRON />
|
||||
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Runnable</h2>
|
||||
{#if !edit}
|
||||
<p class="text-xs mb-1 text-gray-600">
|
||||
Pick a script or flow to be triggered by the schedule<Required required={true} />
|
||||
</p>
|
||||
<ScriptPicker
|
||||
disabled={initialScriptPath != '' || !can_write}
|
||||
initialPath={initialScriptPath}
|
||||
kind={Script.kind.SCRIPT}
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
/>
|
||||
{:else}
|
||||
<Alert type="info" title="Runnable path cannot be edited">
|
||||
Once a schedule is created, the runnable path cannot be changed. However, when renaming a
|
||||
script or a flow, the runnable path will automatically update itself.
|
||||
</Alert>
|
||||
<div class="my-2" />
|
||||
<ScriptPicker
|
||||
disabled
|
||||
initialPath={script_path}
|
||||
scriptPath={script_path}
|
||||
allowFlow={true}
|
||||
{itemKind}
|
||||
/>
|
||||
{/if}
|
||||
<div class="mt-6">
|
||||
<span class="font-semibold text-gray-700">Arguments</span>
|
||||
|
||||
{#if runnable}
|
||||
{#if runnable?.schema && runnable.schema.properties && Object.keys(runnable.schema.properties).length > 0}
|
||||
<SchemaForm disabled={!can_write} schema={runnable.schema} bind:isValid bind:args />
|
||||
{:else}
|
||||
<div class="text-xs texg-gray-700">
|
||||
This {is_flow ? 'flow' : 'script'} takes no argument
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-xs texg-gray-700 my-2">
|
||||
Pick a {is_flow ? 'flow' : 'script'} and fill its argument here
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -10,6 +10,7 @@
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import HighlightCode from './HighlightCode.svelte'
|
||||
import FlowPathViewer from './flows/content/FlowPathViewer.svelte'
|
||||
|
||||
export let initialPath: string | undefined = undefined
|
||||
export let scriptPath: string | undefined = undefined
|
||||
@@ -17,9 +18,11 @@
|
||||
export let allowHub = false
|
||||
export let itemKind: 'hub' | 'script' | 'flow' = allowHub ? 'hub' : 'script'
|
||||
export let kind: Script.kind = Script.kind.SCRIPT
|
||||
export let disabled = false
|
||||
|
||||
let items: { value: string; label: string }[] = []
|
||||
let drawerViewer: Drawer
|
||||
let drawerFlowViewer: Drawer
|
||||
let code: string = ''
|
||||
let lang: 'deno' | 'python3' | 'go' | 'bash' | undefined
|
||||
|
||||
@@ -53,41 +56,64 @@
|
||||
$: itemKind && $workspaceStore && loadItems()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row items-center gap-4 w-full">
|
||||
{#if options.length > 1}
|
||||
<div class="w-80 mt-1">
|
||||
<RadioButton bind:value={itemKind} {options} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Select
|
||||
value={items.find((x) => x.value == initialPath)}
|
||||
class="grow"
|
||||
on:change={() => {
|
||||
dispatch('select', { path: scriptPath })
|
||||
}}
|
||||
bind:justValue={scriptPath}
|
||||
{items}
|
||||
placeholder="Pick a {itemKind}"
|
||||
/>
|
||||
{#if scriptPath !== undefined && scriptPath !== ''}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
const { language, content } = await getScriptByPath(scriptPath ?? '')
|
||||
code = content
|
||||
lang = language
|
||||
drawerViewer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Show code
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={drawerViewer}>
|
||||
<Drawer bind:this={drawerViewer} size="900px">
|
||||
<DrawerContent title="Script {scriptPath}" on:close={drawerViewer.closeDrawer}>
|
||||
<HighlightCode {code} language={lang} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={drawerFlowViewer} size="900px">
|
||||
<DrawerContent title="Flow {scriptPath}" on:close={drawerFlowViewer.closeDrawer}>
|
||||
<FlowPathViewer path={scriptPath ?? ''} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-row items-center gap-4 w-full">
|
||||
{#if options.length > 1}
|
||||
<div class="w-80 mt-1">
|
||||
<RadioButton {disabled} bind:value={itemKind} {options} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if disabled}
|
||||
<input type="text" value={scriptPath} disabled />
|
||||
{:else}
|
||||
<Select
|
||||
value={items.find((x) => x.value == initialPath)}
|
||||
class="grow"
|
||||
on:change={() => {
|
||||
dispatch('select', { path: scriptPath })
|
||||
}}
|
||||
bind:justValue={scriptPath}
|
||||
{items}
|
||||
placeholder="Pick a {itemKind}"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if scriptPath !== undefined && scriptPath !== ''}
|
||||
{#if itemKind == 'flow'}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
drawerFlowViewer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Show flow
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
const { language, content } = await getScriptByPath(scriptPath ?? '')
|
||||
code = content
|
||||
lang = language
|
||||
drawerViewer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Show code
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mb-1 font-semibold text-gray-700">General</div>
|
||||
<span class="font-semibold text-gray-700">Path</span>
|
||||
<Path
|
||||
disabled={!can_write}
|
||||
bind:error={pathError}
|
||||
@@ -153,6 +153,7 @@
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Toggle bind:checked={variable.is_secret} options={{ right: 'Secret' }} />
|
||||
<div class="mb-2" />
|
||||
{#if variable.is_secret}
|
||||
<Alert type="warning" title="Audit log for each access">
|
||||
Every secret is encrypted at rest and in transit with a key specific to this
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import type ShareModal from '$lib/components/ShareModal.svelte'
|
||||
|
||||
@@ -39,8 +41,10 @@
|
||||
sendUserToast(`Could not archive this flow ${err.body}`, true)
|
||||
}
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
<Row
|
||||
href={`/flows/run/${path}`}
|
||||
kind="flow"
|
||||
@@ -132,7 +136,9 @@
|
||||
{
|
||||
displayName: 'Schedule',
|
||||
icon: faCalendarAlt,
|
||||
href: `/schedule/add?path=${path}&isFlow=true`
|
||||
action: () => {
|
||||
scheduleEditor.openNew(true, path)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Share',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import type ShareModal from '$lib/components/ShareModal.svelte'
|
||||
|
||||
@@ -46,8 +48,11 @@
|
||||
dispatch('change')
|
||||
sendUserToast(`Archived script ${path}`)
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
|
||||
<Row
|
||||
href={`/scripts/run/${hash}`}
|
||||
kind="script"
|
||||
@@ -156,7 +161,9 @@
|
||||
{
|
||||
displayName: 'Schedule',
|
||||
icon: faCalendarAlt,
|
||||
href: `/schedule/add?path=${path}`
|
||||
action: () => {
|
||||
scheduleEditor.openNew(false, path)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Share',
|
||||
|
||||
@@ -25,12 +25,16 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import { check } from 'svelte-awesome/icons'
|
||||
import { Badge } from '../common'
|
||||
import ScheduleEditor from '../ScheduleEditor.svelte'
|
||||
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
export let job: Job
|
||||
let scheduleEditor: ScheduleEditor
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
|
||||
<div class="border border-gray-400 rounded py-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 w-full gap-4">
|
||||
<div class="flex-col">
|
||||
@@ -159,9 +163,11 @@
|
||||
{:else if job && job.schedule_path}
|
||||
<Icon class="text-gray-700" data={faCalendar} scale={SMALL_ICON_SCALE} />
|
||||
<span class="mx-2"
|
||||
>Triggered by the schedule: <a
|
||||
href={`/schedule/add?edit=${job.schedule_path}&isFlow=${job.job_kind == 'flow'}`}
|
||||
>{job.schedule_path}</a
|
||||
>Triggered by the schedule: <button
|
||||
class="break-words text-sm text-blue-600 font-normal"
|
||||
on:click={() =>
|
||||
scheduleEditor?.openEdit(job.schedule_path ?? '', job.job_kind == 'flow')}
|
||||
>{job.schedule_path}</button
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
@@ -115,8 +116,11 @@
|
||||
})
|
||||
await goto('/run/' + run + '?workspace=' + $workspaceStore)
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => loadSchedule()} bind:this={scheduleEditor} />
|
||||
|
||||
<UserSettings bind:this={userSettings} />
|
||||
|
||||
<Skeleton
|
||||
@@ -148,7 +152,7 @@
|
||||
Publish to Hub
|
||||
</Button>
|
||||
<Button
|
||||
href="/schedule/add?path={flow.path}&isFlow=true"
|
||||
on:click={() => scheduleEditor?.openNew(true, flow?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
@@ -305,7 +309,7 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button size="xs" href="/schedule/add?edit={flow.path}&isFlow=true"
|
||||
<Button size="xs" on:click={() => scheduleEditor?.openEdit(flow?.path ?? '', true)}
|
||||
>Edit schedule</Button
|
||||
>
|
||||
</h2>
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
<script context="module">
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'New Schedule' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast, formatCron } from '$lib/utils'
|
||||
import { ScriptService, Script, ScheduleService, type Flow, FlowService } from '$lib/gen'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { Alert, Badge, Button } from '$lib/components/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import CronInput, { OFFSET } from '$lib/components/CronInput.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import { faList, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
let initialPath = $page.url.searchParams.get('edit') || ''
|
||||
let edit = initialPath === '' ? false : true
|
||||
let schedule: string = '0 0 12 * *'
|
||||
|
||||
let script_path = $page.url.searchParams.get('path') || ''
|
||||
let initialScriptPath = script_path
|
||||
let is_flow = $page.url.searchParams.get('isFlow') == 'true'
|
||||
let itemKind: 'flow' | 'script' = is_flow ? 'flow' : 'script'
|
||||
|
||||
$: is_flow = itemKind == 'flow'
|
||||
|
||||
let runnable: Script | Flow | undefined
|
||||
let args: Record<string, any> = {}
|
||||
|
||||
let isValid = true
|
||||
|
||||
let path: string = ''
|
||||
let enabled: boolean = false
|
||||
let pathError = ''
|
||||
|
||||
let validCRON = true
|
||||
$: allowSchedule = isValid && validCRON && script_path != ''
|
||||
|
||||
$: script_path && loadScript(script_path)
|
||||
|
||||
async function loadScript(p: string | undefined): Promise<void> {
|
||||
if (p) {
|
||||
if (is_flow) {
|
||||
runnable = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path: p })
|
||||
} else {
|
||||
runnable = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: p })
|
||||
}
|
||||
} else {
|
||||
runnable = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSchedule(): Promise<void> {
|
||||
try {
|
||||
const s = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
enabled = s.enabled
|
||||
schedule = s.schedule
|
||||
script_path = s.script_path ?? ''
|
||||
is_flow = s.is_flow
|
||||
initialScriptPath = script_path
|
||||
args = s.args ?? {}
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load schedule: ${err}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function scheduleScript(): Promise<void> {
|
||||
if (edit) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatCron(schedule),
|
||||
args
|
||||
}
|
||||
})
|
||||
goto('/schedules')
|
||||
} else {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
schedule: formatCron(schedule),
|
||||
offset: OFFSET,
|
||||
script_path,
|
||||
is_flow,
|
||||
args,
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
goto('/schedules')
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
if (edit) {
|
||||
loadSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader title={edit ? 'Edit schedule ' + initialPath : 'New schedule'}>
|
||||
<Button
|
||||
startIcon={{ icon: faSave }}
|
||||
disabled={!allowSchedule || pathError != ''}
|
||||
on:click={scheduleScript}
|
||||
>
|
||||
{edit ? 'Save' : 'Schedule'}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<div>
|
||||
{#if !edit}
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Save schedule as</h2>
|
||||
|
||||
<Path
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
namePlaceholder={'my_schedule'}
|
||||
kind="schedule"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Runnable</h2>
|
||||
{#if !edit}
|
||||
<p class="text-xs mb-1 text-gray-600">
|
||||
Pick a script or flow to be triggered by the schedule<Required required={true} />
|
||||
</p>
|
||||
<ScriptPicker
|
||||
initialPath={initialScriptPath}
|
||||
kind={Script.kind.SCRIPT}
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
/>
|
||||
{:else}
|
||||
<Alert type="info" title="Runnable path cannot be edited">
|
||||
Once a schedule is created, the runnable path cannot be changed. However, when renaming a
|
||||
script or a flow, the runnable path will automatically update itself. To edit the runnable
|
||||
path, you can always delete the schedule and create a new on.
|
||||
</Alert>
|
||||
<div class="mt-4 flex items-center gap-2 max-w-xl">
|
||||
<div> <Badge large color="blue">{itemKind}</Badge></div><input
|
||||
type="text"
|
||||
disabled
|
||||
value={script_path}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
href="/runs/{script_path}"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faList }}
|
||||
>
|
||||
View runs
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={edit ? '' : 'mt-2 md:mt-6'}>
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Arguments</h2>
|
||||
{#if runnable}
|
||||
{#if runnable?.schema && runnable.schema.properties && Object.keys(runnable.schema.properties).length > 0}
|
||||
<SchemaForm schema={runnable.schema} bind:isValid bind:args />
|
||||
{:else}
|
||||
<div class="text-xs texg-gray-700">
|
||||
This {is_flow ? 'flow' : 'script'} takes no argument
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-xs texg-gray-700 my-2">
|
||||
Pick a {is_flow ? 'flow' : 'script'} and fill its argument here
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">
|
||||
<span class="mr-1">Schedule</span>
|
||||
<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
|
||||
</h2>
|
||||
<CronInput bind:schedule bind:validCRON />
|
||||
{#if edit}
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Enable</h2>
|
||||
<Toggle
|
||||
checked={enabled}
|
||||
on:change={async (e) => {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
@@ -13,7 +13,6 @@
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import {
|
||||
@@ -31,6 +30,7 @@
|
||||
import { Badge, Button, Skeleton } from '$lib/components/common'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
|
||||
type ScheduleW = Schedule & { canWrite: boolean }
|
||||
|
||||
@@ -64,11 +64,15 @@
|
||||
loadSchedules()
|
||||
}
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={loadSchedules} bind:this={scheduleEditor} />
|
||||
<CenteredPage>
|
||||
<PageHeader title="Schedules" tooltip="Trigger Scripts and Flows according to a cron schedule">
|
||||
<Button size="md" startIcon={{ icon: faPlus }} href="/schedule/add">New schedule</Button>
|
||||
<Button size="md" startIcon={{ icon: faPlus }} on:click={() => scheduleEditor.openNew(false)}
|
||||
>New schedule</Button
|
||||
>
|
||||
</PageHeader>
|
||||
<div class="mt-10 mb-40">
|
||||
{#if loading}
|
||||
@@ -92,8 +96,9 @@
|
||||
{#each schedules as { path, error, edited_by, edited_at, schedule, offset_, enabled, script_path, is_flow, extra_perms, canWrite }}
|
||||
<tr class={enabled ? '' : 'bg-gray-50'}>
|
||||
<td class="max-w-sm"
|
||||
><a class="break-words text-sm" href="/schedule/add?edit={path}&isFlow={is_flow}"
|
||||
>{path}</a
|
||||
><button
|
||||
class="break-words text-sm text-blue-600 font-normal"
|
||||
on:click={() => scheduleEditor?.openEdit(path, is_flow)}>{path}</button
|
||||
>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</td>
|
||||
@@ -176,7 +181,7 @@
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
goto(`/schedule/add?edit=${path}&isFlow=${is_flow}`)
|
||||
scheduleEditor?.openEdit(path, is_flow)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { onDestroy } from 'svelte'
|
||||
@@ -57,6 +56,7 @@
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
let script: Script | undefined
|
||||
@@ -163,8 +163,11 @@
|
||||
sendUserToast(`Could not create job: ${err.body}`, true)
|
||||
}
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
</script>
|
||||
|
||||
<ScheduleEditor bind:this={scheduleEditor} />
|
||||
|
||||
{#if script}
|
||||
<CenteredPage>
|
||||
<Skeleton {loading} layout={[[{ h: 1.5, w: 40 }], 1, [{ h: 1, w: 30 }]]} />
|
||||
@@ -279,7 +282,7 @@
|
||||
Share
|
||||
</Button>
|
||||
<Button
|
||||
href="/schedule/add?path={script.path}"
|
||||
on:click={() => scheduleEditor?.openNew(false, script?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
|
||||
Reference in New Issue
Block a user