mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
primary schedule in flow UI wip
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import Icon from 'svelte-awesome'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
export let open = false
|
||||
export let text: string
|
||||
</script>
|
||||
|
||||
<button class="text-gray-600 underline" on:click={() => (open = !open)}
|
||||
>{text} <Icon data={open ? faChevronUp : faChevronDown} scale={0.5} /></button
|
||||
>
|
||||
{#if open}
|
||||
<div transition:slide><slot /></div>
|
||||
{/if}
|
||||
@@ -0,0 +1,80 @@
|
||||
<script context="module">
|
||||
export const OFFSET = new Date().getTimezoneOffset()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ScheduleService } from '$lib/gen'
|
||||
import { displayDate, formatCron, sendUserToast } from '$lib/utils'
|
||||
import CollapseLink from './CollapseLink.svelte'
|
||||
|
||||
export let validCRON = true
|
||||
|
||||
let preview: string[] = []
|
||||
let cronError = ''
|
||||
export let schedule: string = '0 0 12 * *'
|
||||
|
||||
$: handleScheduleInput(schedule)
|
||||
|
||||
async function handleScheduleInput(input: string): Promise<void> {
|
||||
try {
|
||||
preview = await ScheduleService.previewSchedule({
|
||||
requestBody: { schedule: formatCron(input), offset: OFFSET }
|
||||
})
|
||||
cronError = ''
|
||||
validCRON = true
|
||||
} catch (err) {
|
||||
if (err.status == 400 && err.body.includes('cron')) {
|
||||
cronError = `Invalid cron expression`
|
||||
validCRON = false
|
||||
} else {
|
||||
sendUserToast(`Cannot preview: ${err}`, true)
|
||||
validCRON = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="text-purple-500 text-2xs grow">{cronError}</div>
|
||||
<div class="flex flex-row items-end max-w-5xl">
|
||||
<label class="text-xs min-w-max mr-2 self-center" for="cron-schedule">CRON expression</label>
|
||||
<input
|
||||
class="inline-block"
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
bind:value={schedule}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row text-xs text-blue-500 gap-3 pl-28">
|
||||
<button
|
||||
on:click={() => {
|
||||
schedule = '0 */15 * * *'
|
||||
cronError = ''
|
||||
}}>every 15 min</button
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
schedule = '0 0 * * * *'
|
||||
cronError = ''
|
||||
}}>every hour</button
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
schedule = '0 0 8 * * *'
|
||||
cronError = ''
|
||||
}}>once a day at 8AM</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<ul class="list-disc mx-12">
|
||||
{#each preview as p}
|
||||
<li class="mx-2 text-gray-700 text-sm">{displayDate(p)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</CollapseLink>
|
||||
@@ -17,9 +17,17 @@
|
||||
import Modal from './Modal.svelte'
|
||||
import FlowViewer from './FlowViewer.svelte'
|
||||
import { flowToMode } from './flows/utils'
|
||||
import CronInput from './CronInput.svelte'
|
||||
import CollapseLink from './CollapseLink.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
|
||||
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
|
||||
|
||||
let jsonSetter: Modal
|
||||
let jsonViewer: Modal
|
||||
@@ -28,8 +36,7 @@
|
||||
|
||||
let open = 0
|
||||
let args: Record<string, any> = {}
|
||||
export let mode: FlowMode =
|
||||
$flowStore?.value.modules[1]?.value.type == FlowModuleValue.type.FORLOOPFLOW ? 'pull' : 'push'
|
||||
|
||||
$: numberOfSteps = $flowStore?.value.modules.length - 1
|
||||
</script>
|
||||
|
||||
@@ -163,6 +170,20 @@
|
||||
]}
|
||||
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>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
export let value: any
|
||||
export let options: {
|
||||
left: { label?: string; value: any }
|
||||
right: { label?: string; value: any }
|
||||
}
|
||||
let checked = options.right.value === value
|
||||
export let value: any = undefined
|
||||
export let options:
|
||||
| {
|
||||
left: { label?: string; value: any }
|
||||
right: { label?: string; value: any }
|
||||
}
|
||||
| undefined = undefined
|
||||
export let checked = options?.right.value === value
|
||||
|
||||
const id = Date.now().toString(36)
|
||||
|
||||
@@ -14,8 +16,8 @@
|
||||
|
||||
<span>
|
||||
<label for={id} class="inline-flex items-center cursor-pointer mt-2">
|
||||
{#if Boolean(options.left.label)}
|
||||
<span class="mr-2 text-sm font-medium text-gray-900">{options.left.label}</span>
|
||||
{#if Boolean(options?.left.label)}
|
||||
<span class="mr-2 text-sm font-medium text-gray-900">{options?.left.label}</span>
|
||||
{/if}
|
||||
<div class="relative">
|
||||
<input
|
||||
@@ -25,16 +27,16 @@
|
||||
class="sr-only peer"
|
||||
bind:checked
|
||||
on:change={() => {
|
||||
value = checked ? options.right.value : options.left.value
|
||||
dispatch('change', value)
|
||||
value = options ? (checked ? options?.right.value : options?.left.value) : checked
|
||||
dispatch('change', value ?? checked)
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"
|
||||
/>
|
||||
</div>
|
||||
{#if Boolean(options.right.label)}
|
||||
<span class="ml-2 text-sm font-medium text-gray-900">{options.right.label}</span>
|
||||
{#if Boolean(options?.right.label)}
|
||||
<span class="ml-2 text-sm font-medium text-gray-900">{options?.right.label}</span>
|
||||
{/if}
|
||||
</label>
|
||||
</span>
|
||||
|
||||
@@ -456,3 +456,14 @@ export async function loadHubFlows() {
|
||||
const processed = flows.sort((a, b) => b.votes - a.votes)
|
||||
return processed
|
||||
}
|
||||
|
||||
export function formatCron(inp: string): string {
|
||||
// Allow for cron expressions inputted by the user to omit month and year
|
||||
let splitted = inp.split(' ')
|
||||
splitted = splitted.filter(String) //remove empty string elements
|
||||
if (6 - splitted.length > 0) {
|
||||
return splitted.concat(Array(6 - splitted.length).fill('*')).join(' ')
|
||||
} else {
|
||||
return inp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast, displayDate } from '$lib/utils'
|
||||
import { sendUserToast, displayDate, formatCron } from '$lib/utils'
|
||||
import { ScriptService, type Script, ScheduleService, type Flow, FlowService } from '$lib/gen'
|
||||
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
@@ -21,11 +21,11 @@
|
||||
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'
|
||||
|
||||
let initialPath = $page.url.searchParams.get('edit') || ''
|
||||
let edit = initialPath === '' ? false : true
|
||||
let scheduleInput: string = '0 0 12 * *'
|
||||
let cronError = ''
|
||||
let schedule: string = '0 0 12 * *'
|
||||
|
||||
let script_path = $page.url.searchParams.get('path') || ''
|
||||
let is_flow = $page.url.searchParams.get('isFlow') == 'true'
|
||||
@@ -36,20 +36,14 @@
|
||||
let runnable: Script | Flow | undefined
|
||||
let args: Record<string, any> = {}
|
||||
|
||||
let validCRON = true
|
||||
let allowSchedule: boolean
|
||||
let isValid = true
|
||||
let preview: string[] = []
|
||||
|
||||
let path: string = ''
|
||||
let pathError = ''
|
||||
|
||||
const offset = new Date().getTimezoneOffset()
|
||||
|
||||
let validCRON = true
|
||||
$: allowSchedule = isValid && validCRON && script_path != ''
|
||||
|
||||
$: handleScheduleInput(scheduleInput)
|
||||
|
||||
$: script_path && loadScript(script_path)
|
||||
|
||||
async function loadScript(p: string | undefined): Promise<void> {
|
||||
@@ -70,7 +64,7 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
scheduleInput = s.schedule
|
||||
schedule = s.schedule
|
||||
script_path = s.script_path ?? ''
|
||||
args = s.args ?? {}
|
||||
} catch (err) {
|
||||
@@ -84,7 +78,7 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatInput(scheduleInput),
|
||||
schedule: formatCron(schedule),
|
||||
script_path: script_path,
|
||||
is_flow: is_flow,
|
||||
args
|
||||
@@ -96,8 +90,8 @@
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
schedule: formatInput(scheduleInput),
|
||||
offset,
|
||||
schedule: formatCron(schedule),
|
||||
offset: OFFSET,
|
||||
script_path,
|
||||
is_flow,
|
||||
args
|
||||
@@ -107,35 +101,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatInput(input: string): string {
|
||||
// Allow for cron expressions inputted by the user to omit month and year
|
||||
let splitted = input.split(' ')
|
||||
splitted = splitted.filter(String) //remove empty string elements
|
||||
if (6 - splitted.length > 0) {
|
||||
return splitted.concat(Array(6 - splitted.length).fill('*')).join(' ')
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScheduleInput(input: string): Promise<void> {
|
||||
try {
|
||||
preview = await ScheduleService.previewSchedule({
|
||||
requestBody: { schedule: formatInput(input), offset }
|
||||
})
|
||||
cronError = ''
|
||||
validCRON = true
|
||||
} catch (err) {
|
||||
if (err.status == 400 && err.body.includes('cron')) {
|
||||
cronError = `Invalid cron expression`
|
||||
validCRON = false
|
||||
} else {
|
||||
sendUserToast(`Cannot preview: ${err}`, true)
|
||||
validCRON = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
if (edit) {
|
||||
@@ -191,39 +156,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
<h2 class="mt-2 md:mt-6">
|
||||
Schedule<Tooltip>Schedules use CRON syntax. Milliseconds are mandatory.</Tooltip>
|
||||
Schedule<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
|
||||
</h2>
|
||||
<div class="text-purple-500 text-2xs grow">{cronError}</div>
|
||||
<div class="flex flex-row items-end max-w-5xl">
|
||||
<label class="text-xs min-w-max mr-2 self-center" for="cron-schedule">CRON expression</label>
|
||||
<input
|
||||
class="inline-block"
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
bind:value={scheduleInput}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row text-xs text-blue-500 gap-3 pl-28">
|
||||
<button
|
||||
on:click={() => {
|
||||
scheduleInput = '0 */15 * * *'
|
||||
cronError = ''
|
||||
}}>every 15 min</button
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
scheduleInput = '0 0 * * * *'
|
||||
cronError = ''
|
||||
}}>every hour</button
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
scheduleInput = '0 0 8 * * *'
|
||||
cronError = ''
|
||||
}}>once a day at 8AM</button
|
||||
>
|
||||
</div>
|
||||
<CronInput bind:schedule bind:validCRON />
|
||||
<div class="flex flex-row-reverse mt-2 ">
|
||||
<div>
|
||||
<button
|
||||
@@ -236,44 +171,5 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if preview && preview.length > 0}
|
||||
<div class="text-sm text-gray-700 border mt-6 p-2 rounded-md">
|
||||
<div class="flex flex-row justify-between">
|
||||
The next 10 runs of this script will be:
|
||||
<button
|
||||
on:click={() => {
|
||||
preview = []
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="list-disc mx-12">
|
||||
{#each preview as p}
|
||||
<li class="mx-2 text-gray-700 text-sm">{displayDate(p)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
<style>
|
||||
.selected:hover {
|
||||
@apply border border-gray-400 rounded-md border-opacity-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user