From 17176bb8d112b35228ce9183f4b2f81abe9e5b6e Mon Sep 17 00:00:00 2001 From: Oliver Veal Date: Wed, 5 Apr 2023 15:37:24 +0100 Subject: [PATCH] feat: improved cron/schedule editor (#1362) * basic cron schedule editing ui * schedules run in a user-specified timezone * fix other uses of CronInput component * use now() from database to schedule next job * offset -> IANA timezone conversion on db migration * sqlx ci --------- Co-authored-by: Ruben Fiszel --- backend/Cargo.lock | 33 ++ backend/Cargo.toml | 1 + .../20230405071524_schedule_timezone.down.sql | 2 + .../20230405071524_schedule_timezone.up.sql | 26 + backend/sqlx-data.json | 561 +++++++++--------- backend/windmill-api/Cargo.toml | 1 + backend/windmill-api/openapi.yaml | 70 +-- backend/windmill-api/src/schedule.rs | 48 +- backend/windmill-common/src/schedule.rs | 2 +- backend/windmill-queue/Cargo.toml | 1 + backend/windmill-queue/src/schedule.rs | 25 +- backend/windmill-worker/src/jobs.rs | 2 +- frontend/package-lock.json | 53 ++ frontend/package.json | 2 + frontend/src/lib/components/CronInput.svelte | 374 ++++++++++-- .../src/lib/components/FlowBuilder.svelte | 13 +- .../src/lib/components/ScheduleEditor.svelte | 17 +- .../flows/content/FlowSchedules.svelte | 2 +- .../src/lib/components/flows/scheduleUtils.ts | 4 +- frontend/src/lib/utils.ts | 13 +- .../(logged)/flows/get/[...path]/+page.svelte | 7 +- .../(root)/(logged)/schedules/+page.svelte | 57 +- 22 files changed, 847 insertions(+), 467 deletions(-) create mode 100644 backend/migrations/20230405071524_schedule_timezone.down.sql create mode 100644 backend/migrations/20230405071524_schedule_timezone.up.sql diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 8cdf57b764..8bad21a2b2 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -537,6 +537,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-tz" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa48fa079165080f11d7753fd0bc175b7d391f276b965fe4b55bfad67856e463" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "cipher" version = "0.3.0" @@ -2318,6 +2340,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -4817,6 +4848,7 @@ dependencies = [ "axum", "base64 0.21.0", "chrono", + "chrono-tz", "cookie", "cron", "futures", @@ -4984,6 +5016,7 @@ version = "1.85.0" dependencies = [ "anyhow", "chrono", + "chrono-tz", "cron", "hex", "hmac", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index ff1f88e12d..b2ebfdb5d9 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -84,6 +84,7 @@ uuid = { version = "^1", features = ["serde", "v4"] } thiserror = "^1" anyhow = "^1" chrono = { version = "^0", features = ["serde"] } +chrono-tz = "^0" tracing = "^0" tracing-subscriber = { version = "^0", features = ["env-filter", "json"] } prometheus = { version = "^0", default-features = false } diff --git a/backend/migrations/20230405071524_schedule_timezone.down.sql b/backend/migrations/20230405071524_schedule_timezone.down.sql new file mode 100644 index 0000000000..fb093e4572 --- /dev/null +++ b/backend/migrations/20230405071524_schedule_timezone.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE schedule DROP COLUMN timezone; +ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/backend/migrations/20230405071524_schedule_timezone.up.sql b/backend/migrations/20230405071524_schedule_timezone.up.sql new file mode 100644 index 0000000000..954302eb38 --- /dev/null +++ b/backend/migrations/20230405071524_schedule_timezone.up.sql @@ -0,0 +1,26 @@ +ALTER TABLE schedule ADD COLUMN timezone VARCHAR(255) NOT NULL DEFAULT 'UTC'; + +-- INSERT the correct IANA timezone string for each offset value + +UPDATE schedule SET timezone = 'Pacific/Honolulu' WHERE offset_ = 600; +UPDATE schedule SET timezone = 'America/Anchorage' WHERE offset_ = 540; +UPDATE schedule SET timezone = 'America/Los_Angeles' WHERE offset_ = 480; +UPDATE schedule SET timezone = 'America/Chicago' WHERE offset_ = 360; +UPDATE schedule SET timezone = 'America/New_York' WHERE offset_ = 300; +UPDATE schedule SET timezone = 'America/Halifax' WHERE offset_ = 240; +UPDATE schedule SET timezone = 'America/Sao_Paulo' WHERE offset_ = 180; +UPDATE schedule SET timezone = 'Atlantic/South_Georgia' WHERE offset_ = 120; +UPDATE schedule SET timezone = 'Atlantic/Cape_Verde' WHERE offset_ = 60; +UPDATE schedule SET timezone = 'Europe/London' WHERE offset_ = 0; +UPDATE schedule SET timezone = 'Europe/Berlin' WHERE offset_ = -60; +UPDATE schedule SET timezone = 'Europe/Athens' WHERE offset_ = -120; +UPDATE schedule SET timezone = 'Europe/Moscow' WHERE offset_ = -180; +UPDATE schedule SET timezone = 'Asia/Dubai' WHERE offset_ = -240; +UPDATE schedule SET timezone = 'Asia/Aqtau' WHERE offset_ = -300; +UPDATE schedule SET timezone = 'Asia/Almaty' WHERE offset_ = -360; +UPDATE schedule SET timezone = 'Asia/Bangkok' WHERE offset_ = -420; +UPDATE schedule SET timezone = 'Asia/Hong_Kong' WHERE offset_ = -480; +UPDATE schedule SET timezone = 'Asia/Tokyo' WHERE offset_ = -540; +UPDATE schedule SET timezone = 'Australia/Sydney' WHERE offset_ = -600; + +ALTER TABLE schedule DROP COLUMN offset_; \ No newline at end of file diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index b90ad94f1a..eb3b6c0e80 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -1993,101 +1993,6 @@ }, "query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, $5)" }, - "55960eb3cd5a82f593496349a2521607f8066d88ff11a2ca4a63b6e37e62b71b": { - "describe": { - "columns": [ - { - "name": "workspace_id", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "path", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "edited_by", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "edited_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "schedule", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "enabled", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "script_path", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "args", - "ordinal": 8, - "type_info": "Jsonb" - }, - { - "name": "extra_perms", - "ordinal": 9, - "type_info": "Jsonb" - }, - { - "name": "is_flow", - "ordinal": 10, - "type_info": "Bool" - }, - { - "name": "email", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "error", - "ordinal": 12, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Varchar", - "Jsonb", - "Text", - "Text" - ] - } - }, - "query": "UPDATE schedule SET schedule = $1, args = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *" - }, "56839d3aec6c0177d14589aedda8d5c431d841b6d5d0d99ce3836bb42d4d83d9": { "describe": { "columns": [ @@ -2602,6 +2507,102 @@ }, "query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6::text::json, dependency_job = NULL WHERE path = $7 AND workspace_id = $8" }, + "7226d470c04e58fcab92f3edac21956d2a10e5ed6b594747b22682f05a60a341": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "enabled", + "ordinal": 5, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 7, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 9, + "type_info": "Bool" + }, + { + "name": "email", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "error", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + false + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Jsonb", + "Text", + "Text" + ] + } + }, + "query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3 WHERE path = $4 AND workspace_id = $5 RETURNING *" + }, "73d3ed17fd0723ba75722f394904f6ee306b59aaf8ecfcf56484d38541343f06": { "describe": { "columns": [], @@ -3195,45 +3196,45 @@ "ordinal": 4, "type_info": "Varchar" }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, { "name": "enabled", - "ordinal": 6, + "ordinal": 5, "type_info": "Bool" }, { "name": "script_path", - "ordinal": 7, + "ordinal": 6, "type_info": "Varchar" }, { "name": "args", - "ordinal": 8, + "ordinal": 7, "type_info": "Jsonb" }, { "name": "extra_perms", - "ordinal": 9, + "ordinal": 8, "type_info": "Jsonb" }, { "name": "is_flow", - "ordinal": 10, + "ordinal": 9, "type_info": "Bool" }, { "name": "email", - "ordinal": 11, + "ordinal": 10, "type_info": "Varchar" }, { "name": "error", - "ordinal": 12, + "ordinal": 11, "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" } ], "nullable": [ @@ -3244,12 +3245,12 @@ false, false, false, - false, true, false, false, false, - true + true, + false ], "parameters": { "Left": [ @@ -3472,45 +3473,45 @@ "ordinal": 4, "type_info": "Varchar" }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, { "name": "enabled", - "ordinal": 6, + "ordinal": 5, "type_info": "Bool" }, { "name": "script_path", - "ordinal": 7, + "ordinal": 6, "type_info": "Varchar" }, { "name": "args", - "ordinal": 8, + "ordinal": 7, "type_info": "Jsonb" }, { "name": "extra_perms", - "ordinal": 9, + "ordinal": 8, "type_info": "Jsonb" }, { "name": "is_flow", - "ordinal": 10, + "ordinal": 9, "type_info": "Bool" }, { "name": "email", - "ordinal": 11, + "ordinal": 10, "type_info": "Varchar" }, { "name": "error", - "ordinal": 12, + "ordinal": 11, "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" } ], "nullable": [ @@ -3521,12 +3522,12 @@ false, false, false, - false, true, false, false, false, - true + true, + false ], "parameters": { "Left": [ @@ -3659,45 +3660,45 @@ "ordinal": 4, "type_info": "Varchar" }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, { "name": "enabled", - "ordinal": 6, + "ordinal": 5, "type_info": "Bool" }, { "name": "script_path", - "ordinal": 7, + "ordinal": 6, "type_info": "Varchar" }, { "name": "args", - "ordinal": 8, + "ordinal": 7, "type_info": "Jsonb" }, { "name": "extra_perms", - "ordinal": 9, + "ordinal": 8, "type_info": "Jsonb" }, { "name": "is_flow", - "ordinal": 10, + "ordinal": 9, "type_info": "Bool" }, { "name": "email", - "ordinal": 11, + "ordinal": 10, "type_info": "Varchar" }, { "name": "error", - "ordinal": 12, + "ordinal": 11, "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" } ], "nullable": [ @@ -3708,12 +3709,12 @@ false, false, false, - false, true, false, false, false, - true + true, + false ], "parameters": { "Left": [ @@ -4237,45 +4238,45 @@ "ordinal": 4, "type_info": "Varchar" }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, { "name": "enabled", - "ordinal": 6, + "ordinal": 5, "type_info": "Bool" }, { "name": "script_path", - "ordinal": 7, + "ordinal": 6, "type_info": "Varchar" }, { "name": "args", - "ordinal": 8, + "ordinal": 7, "type_info": "Jsonb" }, { "name": "extra_perms", - "ordinal": 9, + "ordinal": 8, "type_info": "Jsonb" }, { "name": "is_flow", - "ordinal": 10, + "ordinal": 9, "type_info": "Bool" }, { "name": "email", - "ordinal": 11, + "ordinal": 10, "type_info": "Varchar" }, { "name": "error", - "ordinal": 12, + "ordinal": 11, "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" } ], "nullable": [ @@ -4286,12 +4287,12 @@ false, false, false, - false, true, false, false, false, - true + true, + false ], "parameters": { "Left": [ @@ -4812,45 +4813,45 @@ "ordinal": 4, "type_info": "Varchar" }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, { "name": "enabled", - "ordinal": 6, + "ordinal": 5, "type_info": "Bool" }, { "name": "script_path", - "ordinal": 7, + "ordinal": 6, "type_info": "Varchar" }, { "name": "args", - "ordinal": 8, + "ordinal": 7, "type_info": "Jsonb" }, { "name": "extra_perms", - "ordinal": 9, + "ordinal": 8, "type_info": "Jsonb" }, { "name": "is_flow", - "ordinal": 10, + "ordinal": 9, "type_info": "Bool" }, { "name": "email", - "ordinal": 11, + "ordinal": 10, "type_info": "Varchar" }, { "name": "error", - "ordinal": 12, + "ordinal": 11, "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" } ], "nullable": [ @@ -4861,12 +4862,12 @@ false, false, false, - false, true, false, false, false, - true + true, + false ], "parameters": { "Left": [ @@ -5690,107 +5691,6 @@ }, "query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'iterator', 'index'], ((flow_status->'modules'->$1::int->'iterator'->>'index')::int + 1)::text::jsonb)\n WHERE id = $2\n RETURNING (flow_status->'modules'->$1::int->'iterator'->>'index')::int\n " }, - "e5ba82116000d2ae71e2099f417b39121178349fda9f8438bdd5556ff632ceb2": { - "describe": { - "columns": [ - { - "name": "workspace_id", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "path", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "edited_by", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "edited_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "schedule", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "enabled", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "script_path", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "args", - "ordinal": 8, - "type_info": "Jsonb" - }, - { - "name": "extra_perms", - "ordinal": 9, - "type_info": "Jsonb" - }, - { - "name": "is_flow", - "ordinal": 10, - "type_info": "Bool" - }, - { - "name": "email", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "error", - "ordinal": 12, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Int4", - "Varchar", - "Varchar", - "Bool", - "Jsonb", - "Bool", - "Varchar" - ] - } - }, - "query": "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *" - }, "e6f85cdbe681ace495fde31e67339dc58460b6914440473fe94de7a7bc292af4": { "describe": { "columns": [], @@ -5885,6 +5785,107 @@ }, "query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name, slack_email)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3, slack_email = $4" }, + "ebbabe9b71804ee983235db7d7844052139c23ae7a27cae925c19e1c01037143": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "enabled", + "ordinal": 5, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 7, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 9, + "type_info": "Bool" + }, + { + "name": "email", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "error", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + false + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Jsonb", + "Bool", + "Varchar" + ] + } + }, + "query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *" + }, "ebc06efe51532f7f60e98f62d09a3453fc1d97d93e96ec849eac96dca05236f0": { "describe": { "columns": [ @@ -5963,45 +5964,45 @@ "ordinal": 4, "type_info": "Varchar" }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, { "name": "enabled", - "ordinal": 6, + "ordinal": 5, "type_info": "Bool" }, { "name": "script_path", - "ordinal": 7, + "ordinal": 6, "type_info": "Varchar" }, { "name": "args", - "ordinal": 8, + "ordinal": 7, "type_info": "Jsonb" }, { "name": "extra_perms", - "ordinal": 9, + "ordinal": 8, "type_info": "Jsonb" }, { "name": "is_flow", - "ordinal": 10, + "ordinal": 9, "type_info": "Bool" }, { "name": "email", - "ordinal": 11, + "ordinal": 10, "type_info": "Varchar" }, { "name": "error", - "ordinal": 12, + "ordinal": 11, "type_info": "Text" + }, + { + "name": "timezone", + "ordinal": 12, + "type_info": "Varchar" } ], "nullable": [ @@ -6012,12 +6013,12 @@ false, false, false, - false, true, false, false, false, - true + true, + false ], "parameters": { "Left": [ diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index d7e868d3a7..885ca0b1f9 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -47,6 +47,7 @@ tracing.workspace = true sql-builder.workspace = true serde_json.workspace = true chrono.workspace = true +chrono-tz.workspace = true hex.workspace = true base64.workspace = true serde_urlencoded.workspace = true diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 9e75aa24d0..42d067869e 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2021,8 +2021,7 @@ paths: schema: type: string - name: first_parent_hash - description: - mask to filter scripts whom first direct parent has exact hash + description: mask to filter scripts whom first direct parent has exact hash in: query schema: type: string @@ -2279,8 +2278,7 @@ paths: /w/{workspace}/scripts/delete/h/{hash}: post: - summary: - delete script by hash (erase content but keep hash, require admin) + summary: delete script by hash (erase content but keep hash, require admin) operationId: deleteScriptByHash tags: - script @@ -2435,15 +2433,13 @@ paths: type: string format: date-time - name: scheduled_in_secs - description: - schedule the script to execute in the number of seconds starting now + description: schedule the script to execute in the number of seconds starting now in: query schema: type: integer - $ref: "#/components/parameters/ParentJob" - name: invisible_to_owner - description: - make the run invisible to the the script owner (default false) + description: make the run invisible to the the script owner (default false) in: query schema: type: boolean @@ -3019,16 +3015,14 @@ paths: type: string format: date-time - name: scheduled_in_secs - description: - schedule the script to execute in the number of seconds starting now + description: schedule the script to execute in the number of seconds starting now in: query schema: type: integer - $ref: "#/components/parameters/ParentJob" - $ref: "#/components/parameters/IncludeHeader" - name: invisible_to_owner - description: - make the run invisible to the the flow owner (default false) + description: make the run invisible to the the flow owner (default false) in: query schema: type: boolean @@ -3066,16 +3060,14 @@ paths: type: string format: date-time - name: scheduled_in_secs - description: - schedule the script to execute in the number of seconds starting now + description: schedule the script to execute in the number of seconds starting now in: query schema: type: integer - $ref: "#/components/parameters/ParentJob" - $ref: "#/components/parameters/IncludeHeader" - name: invisible_to_owner - description: - make the run invisible to the the script owner (default false) + description: make the run invisible to the the script owner (default false) in: query schema: type: boolean @@ -3106,8 +3098,7 @@ paths: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/IncludeHeader" - name: invisible_to_owner - description: - make the run invisible to the the script owner (default false) + description: make the run invisible to the the script owner (default false) in: query schema: type: boolean @@ -3138,8 +3129,7 @@ paths: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/IncludeHeader" - name: invisible_to_owner - description: - make the run invisible to the the script owner (default false) + description: make the run invisible to the the script owner (default false) in: query schema: type: boolean @@ -3479,8 +3469,7 @@ paths: /w/{workspace}/jobs/resume_urls/{id}/{resume_id}: get: - summary: - get resume urls given a job_id, resume_id and a nonce to resume a flow + summary: get resume urls given a job_id, resume_id and a nonce to resume a flow operationId: getResumeUrls tags: - job @@ -3738,14 +3727,14 @@ paths: properties: schedule: type: string - offset: - type: integer + timezone: + type: string required: - schedule + - timezone responses: "200": - description: - the preview of the next 10 time this schedule would apply to + description: List of 5 estimated upcoming execution events (in UTC) content: application/json: schema: @@ -3932,8 +3921,7 @@ paths: - $ref: "#/components/parameters/WorkspaceId" - name: only_member_of in: query - description: - only list the groups the user is member of (default false) + description: only list the groups the user is member of (default false) schema: type: boolean responses: @@ -4121,8 +4109,7 @@ paths: - $ref: "#/components/parameters/WorkspaceId" - name: only_member_of in: query - description: - only list the folders the user is member of (default false) + description: only list the folders the user is member of (default false) schema: type: boolean responses: @@ -4605,8 +4592,7 @@ components: type: integer PerPage: name: per_page - description: - number of items to return for a given page (default 30, max 100) + description: number of items to return for a given page (default 30, max 100) in: query schema: type: integer @@ -4707,16 +4693,14 @@ components: type: boolean ArgsFilter: name: args - description: - filter on jobs containing those args as a json subset (@> in postgres) + description: filter on jobs containing those args as a json subset (@> in postgres) in: query schema: type: string ResultFilter: name: result - description: - filter on jobs containing those result as a json subset (@> in postgres) + description: filter on jobs containing those result as a json subset (@> in postgres) in: query schema: type: string @@ -5523,8 +5507,8 @@ components: format: date-time schedule: type: string - offset_: - type: integer + timezone: + type: string enabled: type: boolean script_path: @@ -5547,7 +5531,7 @@ components: - edited_at - schedule - script_path - - offset_ + - timezone - extra_perms - is_flow - enabled @@ -5560,8 +5544,8 @@ components: type: string schedule: type: string - offset: - type: integer + timezone: + type: string script_path: type: string is_flow: @@ -5573,6 +5557,7 @@ components: required: - path - schedule + - timezone - script_path - is_flow - args @@ -5582,10 +5567,13 @@ components: properties: schedule: type: string + timezone: + type: string args: $ref: "#/components/schemas/ScriptArgs" required: - schedule + - timezone - script_path - is_flow - args diff --git a/backend/windmill-api/src/schedule.rs b/backend/windmill-api/src/schedule.rs index c4c2d7cdf7..0fb5830457 100644 --- a/backend/windmill-api/src/schedule.rs +++ b/backend/windmill-api/src/schedule.rs @@ -6,8 +6,6 @@ * LICENSE-AGPL for a copy of the license. */ -use std::str::FromStr; - use crate::{ db::{UserDB, DB}, users::{maybe_refresh_folders, Authed}, @@ -17,9 +15,10 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, Utc}; use serde::Deserialize; use sqlx::{Postgres, Transaction}; +use std::str::FromStr; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ error::{Error, JsonResult, Result}, @@ -47,7 +46,7 @@ pub fn global_service() -> Router { pub struct NewSchedule { pub path: String, pub schedule: String, - pub offset: i32, + pub timezone: String, pub script_path: String, pub is_flow: bool, pub args: Option, @@ -92,12 +91,12 @@ async fn create_schedule( let schedule = sqlx::query_as!( Schedule, - "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \ + "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \ is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *", w_id, ns.path, ns.schedule, - ns.offset, + ns.timezone, &authed.username, ns.script_path, ns.is_flow, @@ -162,9 +161,10 @@ async fn edit_schedule( clear_schedule(&mut tx, path, is_flow).await?; let schedule = sqlx::query_as!( Schedule, - "UPDATE schedule SET schedule = $1, args = $2 WHERE path \ - = $3 AND workspace_id = $4 RETURNING *", + "UPDATE schedule SET schedule = $1, timezone = $2, args = $3 WHERE path \ + = $4 AND workspace_id = $5 RETURNING *", es.schedule, + es.timezone, es.args, path, w_id, @@ -242,15 +242,26 @@ async fn exists_schedule( Ok(Json(res)) } +#[derive(Deserialize)] +pub struct PreviewPayload { + pub schedule: String, + pub timezone: String, +} + pub async fn preview_schedule( Json(payload): Json, -) -> JsonResult>> { +) -> JsonResult>> { let schedule = cron::Schedule::from_str(&payload.schedule) .map_err(|e| Error::BadRequest(e.to_string()))?; - let upcoming: Vec> = schedule - .upcoming(get_offset(payload.offset)) - .take(10) - .map(|x| x.into()) + + let tz = + chrono_tz::Tz::from_str(&payload.timezone).map_err(|e| Error::BadRequest(e.to_string()))?; + + let upcoming: Vec> = schedule + .upcoming(tz) + .take(5) + // Convert back to UTC for a standardised API response. The client will convert to the local timezone. + .map(|x| x.with_timezone(&Utc)) .collect(); Ok(Json(upcoming)) @@ -360,6 +371,7 @@ async fn check_flow_conflict<'c>( #[derive(Deserialize)] pub struct EditSchedule { pub schedule: String, + pub timezone: String, pub args: Option, } @@ -383,16 +395,6 @@ pub async fn clear_schedule<'c>( Ok(()) } -#[derive(Deserialize)] -pub struct PreviewPayload { - pub schedule: String, - pub offset: Option, -} - -fn get_offset(offset: Option) -> FixedOffset { - FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset") -} - #[derive(Deserialize)] pub struct SetEnabled { pub enabled: bool, diff --git a/backend/windmill-common/src/schedule.rs b/backend/windmill-common/src/schedule.rs index 95089f1081..9e350d8ec9 100644 --- a/backend/windmill-common/src/schedule.rs +++ b/backend/windmill-common/src/schedule.rs @@ -17,7 +17,7 @@ pub struct Schedule { pub edited_by: String, pub edited_at: DateTime, pub schedule: String, - pub offset_: i32, + pub timezone: String, pub enabled: bool, pub script_path: String, pub is_flow: bool, diff --git a/backend/windmill-queue/Cargo.toml b/backend/windmill-queue/Cargo.toml index 9e67f92cd9..092d663735 100644 --- a/backend/windmill-queue/Cargo.toml +++ b/backend/windmill-queue/Cargo.toml @@ -25,6 +25,7 @@ serde_json.workspace = true ulid.workspace = true uuid.workspace = true chrono.workspace = true +chrono-tz.workspace = true hex.workspace = true reqwest.workspace = true lazy_static.workspace = true diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index 8d260250d8..50473c01e8 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -6,10 +6,9 @@ * LICENSE-AGPL for a copy of the license. */ -use std::str::FromStr; - -use chrono::Duration; +use crate::{push, JobPayload}; use sqlx::{query_scalar, Postgres, Transaction}; +use std::str::FromStr; use windmill_common::{ error::{self, Result}, schedule::Schedule, @@ -17,8 +16,6 @@ use windmill_common::{ utils::{now_from_db, StripPath}, }; -use crate::{push, JobPayload}; - pub async fn push_scheduled_job<'c>( mut tx: Transaction<'c, Postgres>, schedule: Schedule, @@ -26,13 +23,21 @@ pub async fn push_scheduled_job<'c>( let sched = cron::Schedule::from_str(&schedule.schedule) .map_err(|e| error::Error::BadRequest(e.to_string()))?; - let offset = Duration::minutes(schedule.offset_.into()); - let now = now_from_db(&mut tx).await?; + let tz = chrono_tz::Tz::from_str(&schedule.timezone) + .map_err(|e| error::Error::BadRequest(e.to_string()))?; + + let now = now_from_db(&mut tx).await?.with_timezone(&tz); + let next = sched - .after(&(now - offset + Duration::seconds(1))) + .after(&now) .next() - .expect("a schedule should have a next event") - + offset; + .expect("a schedule should have a next event"); + + // println!("next event ({:?}): {}", tz, next); + // println!("next event(UTC): {}", next.with_timezone(&chrono::Utc)); + + // Scheduled events must be stored in the database in UTC + let next = next.with_timezone(&chrono::Utc); let already_exists: bool = query_scalar!( "SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)", diff --git a/backend/windmill-worker/src/jobs.rs b/backend/windmill-worker/src/jobs.rs index 2686c2b4db..8c467969c2 100644 --- a/backend/windmill-worker/src/jobs.rs +++ b/backend/windmill-worker/src/jobs.rs @@ -232,7 +232,7 @@ pub async fn schedule_again_if_scheduled<'c>( edited_by: schedule.edited_by, edited_at: schedule.edited_at, schedule: schedule.schedule, - offset_: schedule.offset_, + timezone: schedule.timezone, enabled: schedule.enabled, script_path: schedule.script_path, is_flow: schedule.is_flow, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 900ca863cf..4b26c76be1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,6 +30,7 @@ "svelte-chartjs": "^3.1.0", "svelte-portal": "^2.2.0", "svelte-select": "^5.3.1", + "svelte-timezone-picker": "^2.0.3", "tailwind-merge": "^1.9.1", "vscode-languageclient": "~8.0.2", "vscode-ws-jsonrpc": "3.0.0-next.1" @@ -72,6 +73,7 @@ "svelte-awesome-color-picker": "^2.4.1", "svelte-check": "^3.0.2", "svelte-highlight": "^7.2.1", + "svelte-multiselect": "^8.6.0", "svelte-overlay": "^1.4.1", "svelte-popperjs": "^1.3.2", "svelte-preprocess": "^5.0.1", @@ -2115,6 +2117,11 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6428,6 +6435,15 @@ "svelte": ">=3.19.0" } }, + "node_modules/svelte-multiselect": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.0.tgz", + "integrity": "sha512-ce1axNn5YrvDwpUA1R4pSKC7oCD8buP7If61VRoOEM5cxMU8t14tLp5fJf27qd8FBJ7vzvJokiN9BUl1u8oL6w==", + "dev": true, + "dependencies": { + "svelte": "^3.57.0" + } + }, "node_modules/svelte-overlay": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/svelte-overlay/-/svelte-overlay-1.4.1.tgz", @@ -6550,6 +6566,21 @@ "svelte": "^3.54.0" } }, + "node_modules/svelte-timezone-picker": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/svelte-timezone-picker/-/svelte-timezone-picker-2.0.3.tgz", + "integrity": "sha512-p2gqAwuIeDGAxxTI21A4VPY4QrDaobuCq5OSCVFbjvS9Ok9JvVv6vQRR2i+E2TMx6AytXV9CGs1dnpvE/lwvHQ==", + "dependencies": { + "compute-scroll-into-view": "^1.0.16" + }, + "engines": { + "node": ">= 10", + "npm": ">= 6" + }, + "peerDependencies": { + "svelte": "^3.25.0" + } + }, "node_modules/svelte2tsx": { "version": "0.6.10", "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.10.tgz", @@ -8722,6 +8753,11 @@ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true }, + "compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -11881,6 +11917,15 @@ "dev": true, "requires": {} }, + "svelte-multiselect": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.0.tgz", + "integrity": "sha512-ce1axNn5YrvDwpUA1R4pSKC7oCD8buP7If61VRoOEM5cxMU8t14tLp5fJf27qd8FBJ7vzvJokiN9BUl1u8oL6w==", + "dev": true, + "requires": { + "svelte": "^3.57.0" + } + }, "svelte-overlay": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/svelte-overlay/-/svelte-overlay-1.4.1.tgz", @@ -11947,6 +11992,14 @@ "esm-env-robust": "^0.0.3" } }, + "svelte-timezone-picker": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/svelte-timezone-picker/-/svelte-timezone-picker-2.0.3.tgz", + "integrity": "sha512-p2gqAwuIeDGAxxTI21A4VPY4QrDaobuCq5OSCVFbjvS9Ok9JvVv6vQRR2i+E2TMx6AytXV9CGs1dnpvE/lwvHQ==", + "requires": { + "compute-scroll-into-view": "^1.0.16" + } + }, "svelte2tsx": { "version": "0.6.10", "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.10.tgz", diff --git a/frontend/package.json b/frontend/package.json index fae7ff3f51..73dab5a5af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,6 +52,7 @@ "svelte-awesome-color-picker": "^2.4.1", "svelte-check": "^3.0.2", "svelte-highlight": "^7.2.1", + "svelte-multiselect": "^8.6.0", "svelte-overlay": "^1.4.1", "svelte-popperjs": "^1.3.2", "svelte-preprocess": "^5.0.1", @@ -88,6 +89,7 @@ "svelte-chartjs": "^3.1.0", "svelte-portal": "^2.2.0", "svelte-select": "^5.3.1", + "svelte-timezone-picker": "^2.0.3", "tailwind-merge": "^1.9.1", "vscode-languageclient": "~8.0.2", "vscode-ws-jsonrpc": "3.0.0-next.1" diff --git a/frontend/src/lib/components/CronInput.svelte b/frontend/src/lib/components/CronInput.svelte index 8770250255..764980195b 100644 --- a/frontend/src/lib/components/CronInput.svelte +++ b/frontend/src/lib/components/CronInput.svelte @@ -1,92 +1,336 @@ - - -
-
{cronError}
-
- - -
- {#if !disabled} -
- - - -
- {/if} +
+
+ + +

Basic

+
+ +

Cron

+
+
- - {#if preview && preview.length > 0} -
-
The next runs will be scheduled at:
-
    - {#each preview.slice(0, limit) as p} -
  • {displayDate(p, true)}
  • - {/each} -
  • ...
  • - {#if limit != 10} - - {:else} - + {#if tab == 'basic'} +
    +
    + Execute schedule every + +
    +
    + +
    + +
    + {#if executeEvery == 'second'} + + Valid range 0-59 + {:else if executeEvery == 'minute'} + + Valid range 0-59 + {:else if executeEvery == 'hour'} + + Valid range 0-23 + {:else if executeEvery == 'day-month'} + + {/if} +
    +
    +
    + +
    + {#if executeEvery == 'month'} +
    + +
    {/if} -
+ + {#if executeEvery == 'day-week'} +
+ +
+ {/if} + + {#if executeEvery == 'day-month' || executeEvery == 'month'} +
+ {#if executeEvery == 'month'} + On day of the month + {/if} +
+
+ +
+ + +
+ Schedule will only execute on valid calendar days +
+ {/if} + + {#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'} +
+ At UTC Time + +
+ {/if} +
+ +
+ Cron + +
+ {schedule} +
+
+
+ {:else} +
+ Cron + + {#if !validCRON} + Invalid cron syntax + {/if} +
+ +
+ Timezone + + (timezone = e.detail.timezone)} /> + +
{/if} - +
+ +
+

Execution summary

+
+
+ Estimated upcoming events ({timezone}) +
+ {#each preview as date} +
+ {dateFormatter(new Date(date))} +
+ {/each} +
+
+
diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 76fbecc2b1..919425812a 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -10,9 +10,8 @@ import CenteredPage from './CenteredPage.svelte' import { Button, ButtonPopup, ButtonPopupItem, UndoRedo } from './common' import { dirtyStore } from './common/confirmationModal/dirtyStore' - import { OFFSET } from './CronInput.svelte' - import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte' import FlowEditor from './flows/FlowEditor.svelte' + import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte' import type { FlowState } from './flows/flowState' import { dfs } from './flows/flowStore' import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte' @@ -29,7 +28,7 @@ export let flowStateStore: Writable async function createSchedule(path: string) { - const { cron, args, enabled } = $scheduleStore + const { cron, timezone, args, enabled } = $scheduleStore try { await ScheduleService.createSchedule({ @@ -37,7 +36,7 @@ requestBody: { path: path, schedule: formatCron(cron), - offset: OFFSET, + timezone, script_path: path, is_flow: true, args, @@ -55,7 +54,7 @@ loadingSave = true try { const flow = cleanInputs($flowStore) - const { cron, args, enabled } = $scheduleStore + const { cron, timezone, args, enabled } = $scheduleStore $dirtyStore = false if (initialPath === '') { localStorage.removeItem('flow') @@ -100,6 +99,7 @@ path: flow.path, requestBody: { schedule: formatCron(cron), + timezone, args } }) @@ -155,7 +155,7 @@ const selectedIdStore = writable(selectedId ?? 'settings-metadata') - const scheduleStore = writable({ args: {}, cron: '', enabled: false }) + const scheduleStore = writable({ args: {}, cron: '', timezone: '', enabled: false }) const previewArgsStore = writable>(initialArgs) const scriptEditorDrawer = writable(undefined) const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined) @@ -187,6 +187,7 @@ .catch(() => { scheduleStore.set({ cron: '0 */5 * * *', + timezone: 'UTC', args: {}, enabled: false }) diff --git a/frontend/src/lib/components/ScheduleEditor.svelte b/frontend/src/lib/components/ScheduleEditor.svelte index 244ed4a654..c0b9293696 100644 --- a/frontend/src/lib/components/ScheduleEditor.svelte +++ b/frontend/src/lib/components/ScheduleEditor.svelte @@ -7,26 +7,26 @@ - + {#if emptyString($schedule.cron)}

Define a schedule frequency first

diff --git a/frontend/src/lib/components/flows/scheduleUtils.ts b/frontend/src/lib/components/flows/scheduleUtils.ts index a1590287cb..d264fe7529 100644 --- a/frontend/src/lib/components/flows/scheduleUtils.ts +++ b/frontend/src/lib/components/flows/scheduleUtils.ts @@ -3,6 +3,7 @@ import { ScheduleService } from '$lib/gen' export type Schedule = { args: Record cron: string + timezone: string enabled: boolean } @@ -25,6 +26,7 @@ export async function loadFlowSchedule(path: string, workspace: string = ''): Pr return { enabled: schedule.enabled, cron: schedule.schedule, - args: schedule.args ?? {}, + timezone: schedule.timezone, + args: schedule.args ?? {} } } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 9a3c8e6bd3..71a15a406c 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -2,16 +2,16 @@ import { goto } from '$app/navigation' import { AppService, + type Flow, FlowService, Script, ScriptService, - UserService, - type Flow, - type User + type User, + UserService } from '$lib/gen' import { toast } from '@zerodevx/svelte-toast' import type { Schema, SupportedLanguage } from './common' -import { hubScripts, workspaceStore, type UserExt } from './stores' +import { hubScripts, type UserExt, workspaceStore } from './stores' import { page } from '$app/stores' import { get } from 'svelte/store' @@ -186,7 +186,10 @@ export async function isOwner(path: string, user: UserExt, workspace: string): P } else if (workspace == 'starter' || workspace == 'admin') { return false } else { - return await UserService.isOwnerOfPath({ path: path, workspace: workspace }) + return await UserService.isOwnerOfPath({ + path: path, + workspace: workspace + }) } } diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index 4a9c17dcad..83e8d59e74 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -412,7 +412,12 @@
- +
{/if} diff --git a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte index 925d39039f..9ab15c65a9 100644 --- a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte @@ -63,9 +63,9 @@ - +
{#if loading} @@ -79,15 +79,15 @@ Schedule Script/Flow - schedule + Schedule + Timezone - off/on - timezone - last edit + Enabled + Last Edit - {#each schedules as { path, error, edited_by, edited_at, schedule, offset_, enabled, script_path, is_flow, extra_perms, canWrite }} + {#each schedules as { path, error, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, extra_perms, canWrite }} @@ -100,17 +100,25 @@ {path} -
+
+ {is_flow ? 'flow' : 'script'}
- {schedule} + > + {script_path} + +
+ {is_flow ? 'flow' : 'script'}> +
+ + + {schedule} + + + {timezone} +
{#if error} @@ -135,8 +143,8 @@
{/if} -
+ + - {offset_ < 0 ? '+' : ''}{(offset_ / 60) * -1} - By {edited_by}
the {displayDate(edited_at)}
+ /> + + + By {edited_by}
the {displayDate(edited_at)}
+