feat: add schedule page to script settings

This commit is contained in:
Ruben Fiszel
2024-03-01 20:27:27 +01:00
parent 644df205c3
commit 67cf82f130
9 changed files with 407 additions and 17 deletions
+16 -7
View File
@@ -88,7 +88,7 @@
const dispatch = createEventDispatcher()
async function createSchedule(path: string) {
const { cron, timezone, args, enabled } = $scheduleStore
const { cron, timezone, args, enabled, summary } = $scheduleStore
try {
await ScheduleService.createSchedule({
@@ -100,7 +100,8 @@
script_path: path,
is_flow: true,
args,
enabled
enabled,
summary
}
})
} catch (err) {
@@ -215,7 +216,7 @@
// console.log('flow', computeUnlockedSteps(flow)) // del
// loadingSave = false // del
// return
const { cron, timezone, args, enabled } = $scheduleStore
const { cron, timezone, args, enabled, summary } = $scheduleStore
if (newFlow) {
try {
localStorage.removeItem('flow')
@@ -256,14 +257,20 @@
workspace: $workspaceStore ?? '',
path: initialPath
})
if (JSON.stringify(schedule.args) != JSON.stringify(args) || schedule.schedule != cron) {
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
args,
summary
}
})
}
@@ -338,6 +345,7 @@
}
const scheduleStore = writable<Schedule>({
summary: undefined,
args: {},
cron: '',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -373,14 +381,15 @@
})
async function loadSchedule() {
loadFlowSchedule(initialPath, $workspaceStore)
loadFlowSchedule(initialPath, $workspaceStore!)
.then((schedule: Schedule) => {
scheduleStore.set(schedule)
})
.catch(() => {
scheduleStore.set({
summary: undefined,
cron: '0 */5 * * *',
timezone: 'UTC',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
args: {},
enabled: false
})
@@ -72,9 +72,10 @@
drawer?.openDrawer()
}
export async function openNew(is_flow: boolean, initial_script_path?: string) {
export async function openNew(nis_flow: boolean, initial_script_path?: string) {
args = {}
runnable = undefined
is_flow = nis_flow
let defaultErrorHandlerMaybe = undefined
let defaultRecoveryHandlerMaybe = undefined
if ($workspaceStore) {
@@ -87,7 +88,7 @@
}
edit = false
itemKind = is_flow ? 'flow' : 'script'
itemKind = nis_flow ? 'flow' : 'script'
initialScriptPath = initial_script_path ?? ''
summary = ''
no_flow_overlap = false
@@ -1,5 +1,12 @@
<script lang="ts">
import { DraftService, NewScript, Script, ScriptService, type NewScriptWithDraft } from '$lib/gen'
import {
DraftService,
NewScript,
Script,
ScriptService,
type NewScriptWithDraft,
ScheduleService
} from '$lib/gen'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { inferArgs } from '$lib/infer'
@@ -10,6 +17,7 @@
emptySchema,
emptyString,
encodeState,
formatCron,
orderedJsonStringify
} from '$lib/utils'
import Path from './Path.svelte'
@@ -24,6 +32,7 @@
import ErrorHandlerToggleButton from '$lib/components/details/ErrorHandlerToggleButton.svelte'
import {
Bug,
Calendar,
CheckCircle,
Code,
DiffIcon,
@@ -50,6 +59,9 @@
import type Editor from './Editor.svelte'
import WorkerTagPicker from './WorkerTagPicker.svelte'
import MetadataGen from './copilot/MetadataGen.svelte'
import ScriptSchedules from './ScriptSchedules.svelte'
import { writable } from 'svelte/store'
import { type ScriptSchedule, loadScriptSchedule } from '$lib/scripts'
export let script: NewScript
export let initialPath: string = ''
@@ -69,6 +81,26 @@
let editor: Editor | undefined = undefined
let scriptEditor: ScriptEditor | undefined = undefined
let scheduleStore = writable<ScriptSchedule>({
summary: '',
cron: '0 */5 * * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
args: {},
enabled: false
})
async function loadSchedule() {
const scheduleRes = await loadScriptSchedule(initialPath, $workspaceStore!)
if (scheduleRes) {
scheduleStore.set(scheduleRes)
}
}
$: {
if (initialPath != '') {
loadSchedule()
}
}
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql']
export function setCode(code: string): void {
@@ -161,6 +193,28 @@
}
}
async function createSchedule(path: string) {
const { cron, timezone, args, enabled, summary } = $scheduleStore
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 editScript(stay: boolean): Promise<void> {
loadingSave = true
try {
@@ -204,6 +258,49 @@
concurrency_key: emptyString(script.concurrency_key) ? undefined : script.concurrency_key
}
})
const { enabled, timezone, args, cron, summary } = $scheduleStore
const scheduleExists =
initialPath &&
initialPath != '' &&
(await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path: initialPath
}))
if (scheduleExists) {
const schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path: initialPath
})
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 (enabled) {
await createSchedule(initialPath)
}
savedScript = cloneDeep(script) as NewScriptWithDraft
history.replaceState(history.state, '', `/scripts/edit/${script.path}`)
if (stay) {
@@ -365,7 +462,7 @@
let path: Path | undefined = undefined
let dirtyPath = false
let selectedTab: 'metadata' | 'runtime' | 'ui' = 'metadata'
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'schedule' = 'metadata'
</script>
<svelte:window on:keydown={onKeyDown} />
@@ -387,6 +484,7 @@
cannot be inferred from the type directly.
</Tooltip>
</Tab>
<Tab value="schedule" active={$scheduleStore.enabled}>Schedule</Tab>
<svelte:fragment slot="content">
<div class="p-4">
<TabContent value="metadata">
@@ -858,6 +956,9 @@
<div class="mt-4" />
<ScriptSchema bind:schema={script.schema} />
</TabContent>
<TabContent value="schedule">
<ScriptSchedules {initialPath} schema={script.schema} schedule={scheduleStore} />
</TabContent>
</div>
</svelte:fragment>
</Tabs>
@@ -888,6 +989,21 @@
</div>
<div class="gap-4 flex">
{#if $scheduleStore.enabled}
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: Calendar }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
metadataOpen = true
selectedTab = 'schedule'
}}
>
{$scheduleStore.cron ?? ''}
</Button>
{/if}
<div class="flex justify-start w-full border rounded-md overflow-hidden">
<div>
<button
@@ -0,0 +1,107 @@
<script lang="ts">
import CronInput from '$lib/components/CronInput.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { emptyString } from '$lib/utils'
import { Alert, Button, Skeleton } from '$lib/components/common'
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
import { ScheduleService, type Schedule } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Calendar } from 'lucide-svelte'
import type { Writable } from 'svelte/store'
import type { ScriptSchedule } from '$lib/scripts'
export let initialPath: string
export let schema: Record<string, any> | undefined
export let schedule: Writable<ScriptSchedule>
// const { schedule, flowStore, pathStore, initialPath } =
// getContext<FlowEditorContext>('FlowEditorContext')
let schedules: Schedule[] | undefined = undefined
async function loadSchedules() {
try {
schedules = (
await ScheduleService.listSchedules({
workspace: $workspaceStore ?? '',
path: initialPath,
isFlow: false
})
).filter((s) => s.path != initialPath)
} catch (e) {
console.error('impossible to load schedules')
}
}
let scheduleEditor: ScheduleEditor
$: initialPath && loadSchedules()
</script>
<div class="w-full py-2">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
type="text"
placeholder="Schedule summary"
class="text-sm w-full font-semibold mb-4"
bind:value={$schedule.summary}
/>
</div>
<CronInput bind:schedule={$schedule.cron} bind:timezone={$schedule.timezone} />
<div class="mt-10" />
<SchemaForm {schema} bind:args={$schedule.args} />
{#if emptyString($schedule.cron)}
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
{/if}
<div class="mt-10" />
<Toggle
disabled={emptyString($schedule.cron)}
bind:checked={$schedule.enabled}
options={{
right: 'Schedule enabled'
}}
/>
<Alert bgClass="my-4" type="warning" title="Changes only applied upon deploy">
Changes to the primary schedule are only applied upon deploy. Other schedules' changes are applied
immediately.
</Alert>
{#if initialPath != ''}
<ScheduleEditor
on:update={() => {
loadSchedules()
}}
bind:this={scheduleEditor}
/>
<h2 class="pt-7">Other schedules</h2>
<div class="py-4 flex">
<Button
on:click={() => scheduleEditor?.openNew(false, initialPath)}
variant="border"
color="light"
size="xs"
startIcon={{ icon: Calendar }}
>
New Schedule
</Button>
</div>
{#if schedules}
{#if schedules.length == 0}
<div class="text-xs text-secondary px-2"> No other schedules </div>
{:else}
<div class="flex flex-col divide-y px-2 pt-2 max-w-lg">
{#each schedules as schedule (schedule.path)}
<div class="grid grid-cols-6 text-2xs items-center py-2"
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
<div>{schedule.enabled ? 'on' : 'off'}</div>
<button on:click={() => scheduleEditor?.openEdit(schedule.path, false)}>Edit</button>
</div>
{/each}
</div>
{/if}
{:else}
<Skeleton layout={[[8]]} />
{/if}
{/if}
@@ -5,10 +5,45 @@
import { emptyString } from '$lib/utils'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import { Alert, Button, Skeleton } from '$lib/components/common'
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
import { ScheduleService, type Schedule } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Calendar } from 'lucide-svelte'
const { schedule, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
const { schedule, flowStore, initialPath } = getContext<FlowEditorContext>('FlowEditorContext')
let schedules: Schedule[] | undefined = undefined
async function loadSchedules() {
try {
schedules = (
await ScheduleService.listSchedules({
workspace: $workspaceStore ?? '',
path: initialPath,
isFlow: true
})
).filter((s) => s.path != initialPath)
} catch (e) {
console.error('impossible to load schedules')
}
}
let scheduleEditor: ScheduleEditor
$: initialPath && loadSchedules()
</script>
<div class="w-full py-2">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
type="text"
placeholder="Schedule summary"
class="text-sm w-full font-semibold mb-4"
bind:value={$schedule.summary}
/>
</div>
<CronInput bind:schedule={$schedule.cron} bind:timezone={$schedule.timezone} />
<div class="mt-10" />
<SchemaForm schema={$flowStore.schema} bind:args={$schedule.args} />
@@ -23,3 +58,46 @@
right: 'Schedule enabled'
}}
/>
<Alert bgClass="my-4" type="warning" title="Changes only applied upon deploy">
Changes to the primary schedule are only applied upon deploy. Other schedules' changes are applied
immediately.
</Alert>
{#if initialPath != ''}
<ScheduleEditor
on:update={() => {
loadSchedules()
}}
bind:this={scheduleEditor}
/>
<h2 class="pt-7">Other schedules</h2>
<div class="py-4 flex">
<Button
on:click={() => scheduleEditor?.openNew(true, initialPath)}
variant="border"
color="light"
size="xs"
startIcon={{ icon: Calendar }}
>
New Schedule
</Button>
</div>
{#if schedules}
{#if schedules.length == 0}
<div class="text-xs text-secondary px-2"> No other schedules </div>
{:else}
<div class="flex flex-col divide-y px-2 pt-2 max-w-lg">
{#each schedules as schedule (schedule.path)}
<div class="grid grid-cols-6 text-2xs items-center py-2"
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
<div>{schedule.enabled ? 'on' : 'off'}</div>
<button on:click={() => scheduleEditor?.openEdit(schedule.path, true)}>Edit</button>
</div>
{/each}
</div>
{/if}
{:else}
<Skeleton layout={[[8]]} />
{/if}
{/if}
@@ -1,6 +1,7 @@
import { ScheduleService } from '$lib/gen'
export type Schedule = {
summary: string | undefined
args: Record<string, any>
cron: string
timezone: string
@@ -8,7 +9,7 @@ export type Schedule = {
}
// Load the schedule of a flow given its path and the workspace
export async function loadFlowSchedule(path: string, workspace: string = ''): Promise<Schedule> {
export async function loadFlowSchedule(path: string, workspace: string): Promise<Schedule> {
const existsSchedule = await ScheduleService.existsSchedule({
workspace,
path
@@ -24,6 +25,7 @@ export async function loadFlowSchedule(path: string, workspace: string = ''): Pr
})
return {
summary: schedule.summary,
enabled: schedule.enabled,
cron: schedule.schedule,
timezone: schedule.timezone,
+37 -1
View File
@@ -1,6 +1,6 @@
import { get } from 'svelte/store'
import type { Schema, SupportedLanguage } from './common'
import { FlowService, Script, ScriptService } from './gen'
import { FlowService, Script, ScriptService, ScheduleService } from './gen'
import { workspaceStore } from './stores'
export function scriptLangToEditorLang(lang: Script.language) {
@@ -35,6 +35,42 @@ export function scriptLangToEditorLang(lang: Script.language) {
}
}
export type ScriptSchedule = {
summary: string | undefined
args: Record<string, any>
cron: string
timezone: string
enabled: boolean
}
// Load the schedule of a flow given its path and the workspace
export async function loadScriptSchedule(
path: string,
workspace: string
): Promise<ScriptSchedule | undefined> {
const existsSchedule = await ScheduleService.existsSchedule({
workspace,
path
})
if (!existsSchedule) {
return undefined
}
const schedule = await ScheduleService.getSchedule({
workspace,
path
})
return {
summary: schedule.summary,
enabled: schedule.enabled,
cron: schedule.schedule,
timezone: schedule.timezone,
args: schedule.args ?? {}
}
}
export async function loadSchemaFlow(path: string): Promise<Schema> {
const flow = await FlowService.getFlowByPath({
workspace: get(workspaceStore)!,
@@ -6,7 +6,7 @@
import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte'
import { goto } from '$app/navigation'
import { Alert, Badge as HeaderBadge, Skeleton } from '$lib/components/common'
import { Alert, Button, Badge as HeaderBadge, Skeleton } from '$lib/components/common'
import MoveDrawer from '$lib/components/MoveDrawer.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
@@ -27,7 +27,8 @@
History,
Columns,
Pen,
Eye
Eye,
Calendar
} from 'lucide-svelte'
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
@@ -42,6 +43,7 @@
import TimeAgo from '$lib/components/TimeAgo.svelte'
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
import FlowGraphViewerStep from '$lib/components/FlowGraphViewerStep.svelte'
import { loadFlowSchedule, type Schedule } from '$lib/components/flows/scheduleUtils'
let flow: Flow | undefined
let can_write = false
@@ -76,9 +78,13 @@
goto('/')
}
let schedule: Schedule | undefined = undefined
async function loadFlow(): Promise<void> {
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
can_write = canWrite(flow.path, flow.extra_perms!, $userStore)
try {
schedule = await loadFlowSchedule(path, $workspaceStore!)
} catch {}
}
$: urlAsync = `${$page.url.origin}/api/w/${$workspaceStore}/jobs/run/f/${flow?.path}`
@@ -307,6 +313,21 @@
</HeaderBadge>
</div>
{/if}
{#if schedule?.enabled}
<Button
btnClasses="inline-flex"
startIcon={{ icon: Calendar }}
variant="contained"
color="light"
size="xs"
on:click={() => {
detailSelected = 'details'
triggerSelected = 'schedule'
}}
>
{schedule.cron ?? ''}
</Button>
{/if}
</DetailPageHeader>
</svelte:fragment>
<svelte:fragment slot="form">
@@ -21,7 +21,8 @@
Badge,
Alert,
DrawerContent,
Drawer
Drawer,
Button
} from '$lib/components/common'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import RunForm from '$lib/components/RunForm.svelte'
@@ -41,6 +42,7 @@
Activity,
Archive,
ArchiveRestore,
Calendar,
Eye,
FolderOpen,
GitFork,
@@ -64,6 +66,7 @@
import TimeAgo from '$lib/components/TimeAgo.svelte'
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
import PersistentScriptDrawer from '$lib/components/PersistentScriptDrawer.svelte'
import { loadScriptSchedule, type ScriptSchedule } from '$lib/scripts'
let script: Script | undefined
let topHash: string | undefined
@@ -139,6 +142,7 @@
}
}
}
let schedule: ScriptSchedule | undefined = undefined
async function loadScript(hash: string): Promise<void> {
try {
@@ -147,6 +151,7 @@
script = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: hash })
hash = script.hash
}
schedule = await loadScriptSchedule(script.path, $workspaceStore!)
can_write =
script.workspace_id == $workspaceStore &&
canWrite(script.path, script.extra_perms!, $userStore)
@@ -488,6 +493,21 @@
</Badge>
</div>
{/if}
{#if schedule?.enabled}
<Button
btnClasses="inline-flex"
startIcon={{ icon: Calendar }}
variant="contained"
color="light"
size="xs"
on:click={() => {
detailSelected = 'details'
triggerSelected = 'schedule'
}}
>
{schedule.cron ?? ''}
</Button>
{/if}
</DetailPageHeader>
</svelte:fragment>
<svelte:fragment slot="form">