mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
fix: guard v1 weekday crons, refresh home activity on reload, stable trigger keys
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f38ff60f54
commit
d2b92f1b39
@@ -33369,6 +33369,11 @@ components:
|
||||
timezone:
|
||||
description: the schedule's timezone, for a schedule only
|
||||
type: string
|
||||
cron_version:
|
||||
description: >-
|
||||
the schedule's cron version, for a schedule only; v1 numbers
|
||||
weekdays from 1 = Sunday
|
||||
type: string
|
||||
|
||||
TriggersCount:
|
||||
type: object
|
||||
|
||||
@@ -1130,11 +1130,14 @@ struct AttachedTrigger {
|
||||
path: String,
|
||||
/// `enabled` | `disabled` | `suspended`
|
||||
mode: String,
|
||||
/// A schedule's cron expression and timezone; absent for every other kind.
|
||||
/// A schedule's cron expression, timezone and cron version (`v1` numbers
|
||||
/// weekdays from 1 = Sunday); absent for every other kind.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
schedule: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
timezone: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cron_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
@@ -1194,7 +1197,8 @@ async fn get_runnables_activity(
|
||||
ORDER BY j.created_at DESC"
|
||||
);
|
||||
|
||||
const NO_CRON: &str = "NULL::text AS schedule, NULL::text AS timezone";
|
||||
const NO_CRON: &str =
|
||||
"NULL::text AS schedule, NULL::text AS timezone, NULL::text AS cron_version";
|
||||
let branch = |table: &str, kind: &str, mode: &str, cron: &str| {
|
||||
format!(
|
||||
"SELECT '{kind}' AS kind, path, script_path, is_flow, {mode} AS mode, {cron} FROM {table} \
|
||||
@@ -1204,7 +1208,12 @@ async fn get_runnables_activity(
|
||||
let trigger_mode = "mode::text";
|
||||
let bool_mode = "CASE WHEN enabled THEN 'enabled' ELSE 'disabled' END";
|
||||
let branches = [
|
||||
branch("schedule", "schedule", bool_mode, "schedule, timezone"),
|
||||
branch(
|
||||
"schedule",
|
||||
"schedule",
|
||||
bool_mode,
|
||||
"schedule, timezone, cron_version::text",
|
||||
),
|
||||
branch("http_trigger", "http", trigger_mode, NO_CRON),
|
||||
branch("websocket_trigger", "websocket", trigger_mode, NO_CRON),
|
||||
branch("kafka_trigger", "kafka", trigger_mode, NO_CRON),
|
||||
|
||||
@@ -837,6 +837,8 @@
|
||||
// was on screen so the selection can drop what this reload removes instead of
|
||||
// keeping a dead path. `tick` lets the reloaded rows re-register first.
|
||||
const renderedBefore = homeSelection.renderedKeys
|
||||
// A row action can add, toggle or remove a trigger, or start a run.
|
||||
homeActivity.reset()
|
||||
void ownerCountsRes.refetch()
|
||||
await reloadItems()
|
||||
await tick()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import { Tooltip } from '$lib/components/meltComponents'
|
||||
import { triggerDisplayNamesMap, triggerIconMapMono } from '$lib/components/triggers/utils'
|
||||
import { displayDate } from '$lib/utils'
|
||||
import { displayDate, formatCron } from '$lib/utils'
|
||||
import { describeSchedule } from '$lib/utils/describeCron'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getHomeActivity, type ActivityKind } from './homeActivity.svelte'
|
||||
@@ -41,10 +41,26 @@
|
||||
)
|
||||
let loneScheduleText = $derived(
|
||||
loneSchedule?.schedule
|
||||
? (describeSchedule(loneSchedule.schedule, loneSchedule.timezone) ?? loneSchedule.schedule)
|
||||
? (describeLoneSchedule(
|
||||
loneSchedule.schedule,
|
||||
loneSchedule.timezone,
|
||||
loneSchedule.cron_version
|
||||
) ?? loneSchedule.schedule)
|
||||
: undefined
|
||||
)
|
||||
|
||||
// `describeCron` reads weekdays the v2 way (0 = Sunday); a v1 cron numbers them
|
||||
// from 1 = Sunday, so one that names weekdays is shown as the raw expression.
|
||||
function describeLoneSchedule(
|
||||
cron: string,
|
||||
timezone: string | undefined,
|
||||
cronVersion: string | undefined
|
||||
): string | undefined {
|
||||
const dow = formatCron(cron.trim()).split(/\s+/)[5]
|
||||
if (cronVersion === 'v1' && dow !== undefined && dow !== '*' && dow !== '?') return undefined
|
||||
return describeSchedule(cron, timezone)
|
||||
}
|
||||
|
||||
type RunStatus = (typeof runs)[number]['status']
|
||||
|
||||
const statusClass: Record<RunStatus, string> = {
|
||||
@@ -111,7 +127,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="relative z-[1] hidden lg:flex items-center justify-start gap-0.5 w-44 min-w-0 shrink-0">
|
||||
<div
|
||||
class="relative z-[1] hidden lg:flex items-center justify-start gap-0.5 w-44 min-w-0 shrink-0"
|
||||
>
|
||||
{#if loneSchedule}
|
||||
<Tooltip class="min-w-0">
|
||||
<div
|
||||
@@ -128,7 +146,7 @@
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{#each visibleTriggers as trigger (trigger.kind + trigger.path)}
|
||||
{#each visibleTriggers as trigger, i (i)}
|
||||
<Tooltip>
|
||||
{@render triggerChip(trigger.kind, trigger.mode)}
|
||||
{#snippet text()}
|
||||
@@ -146,7 +164,7 @@
|
||||
{#snippet text()}
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-semibold">{triggers.length} triggers</span>
|
||||
{#each triggers as trigger (trigger.kind + trigger.path)}
|
||||
{#each triggers as trigger, i (i)}
|
||||
{@render triggerLine(trigger)}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describeCron, describeSchedule } from './describeCron'
|
||||
|
||||
describe('describeCron', () => {
|
||||
it.each([
|
||||
['*/30 * * * * *', 'Every 30 seconds'],
|
||||
['0 * * * * *', 'Every minute'],
|
||||
['0 */15 * * * *', 'Every 15 minutes'],
|
||||
['0 0 * * * *', 'Every hour'],
|
||||
['0 30 * * * *', 'Every hour at :30'],
|
||||
['0 0 */6 * * *', 'Every 6 hours'],
|
||||
['0 0 4 * * *', 'Every day at 4:00'],
|
||||
// five fields are padded at the end, like the schedule editor saves them
|
||||
['0 0 12 * *', 'Every day at 12:00'],
|
||||
['0 30 9 * * 1-5', 'Every weekday at 9:30'],
|
||||
['0 0 9 * * MON', 'Every Monday at 9:00'],
|
||||
['0 0 18 * * 1,3,5', 'Every Monday, Wednesday and Friday at 18:00'],
|
||||
['0 0 8 1 * *', 'Every month on the 1st at 8:00'],
|
||||
['0 0 8 22 * *', 'Every month on the 22nd at 8:00']
|
||||
])('%s → %s', (cron, text) => {
|
||||
expect(describeCron(cron)).toBe(text)
|
||||
})
|
||||
|
||||
it.each(['0 0 8 1 1 *', '15 0 4 * * *', '0 0 4-6 * * *', 'not a cron'])(
|
||||
'leaves %s to be shown as is',
|
||||
(cron) => {
|
||||
expect(describeCron(cron)).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('describeSchedule', () => {
|
||||
it('names the zone of a wall-clock time only', () => {
|
||||
expect(describeSchedule('0 0 12 * * *', 'UTC')).toBe('Every day at 12:00 UTC')
|
||||
expect(describeSchedule('0 0 * * * *', 'UTC')).toBe('Every hour')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user