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 <ruben@rubenfiszel.com>
This commit is contained in:
Oliver Veal
2023-04-05 16:37:24 +02:00
committed by GitHub
co-authored by Ruben Fiszel
parent 4fb7468cf3
commit 17176bb8d1
22 changed files with 847 additions and 467 deletions
+33
View File
@@ -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",
+1
View File
@@ -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 }
@@ -0,0 +1,2 @@
ALTER TABLE schedule DROP COLUMN timezone;
ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0;
@@ -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_;
+281 -280
View File
@@ -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": [
+1
View File
@@ -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
+29 -41
View File
@@ -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
+25 -23
View File
@@ -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<serde_json::Value>,
@@ -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<PreviewPayload>,
) -> JsonResult<Vec<DateTime<chrono::Utc>>> {
) -> JsonResult<Vec<DateTime<Utc>>> {
let schedule = cron::Schedule::from_str(&payload.schedule)
.map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<chrono::Utc>> = 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<DateTime<Utc>> = 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<serde_json::Value>,
}
@@ -383,16 +395,6 @@ pub async fn clear_schedule<'c>(
Ok(())
}
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub offset: Option<i32>,
}
fn get_offset(offset: Option<i32>) -> FixedOffset {
FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset")
}
#[derive(Deserialize)]
pub struct SetEnabled {
pub enabled: bool,
+1 -1
View File
@@ -17,7 +17,7 @@ pub struct Schedule {
pub edited_by: String,
pub edited_at: DateTime<chrono::Utc>,
pub schedule: String,
pub offset_: i32,
pub timezone: String,
pub enabled: bool,
pub script_path: String,
pub is_flow: bool,
+1
View File
@@ -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
+15 -10
View File
@@ -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)",
+1 -1
View File
@@ -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,
+53
View File
@@ -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",
+2
View File
@@ -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"
+309 -65
View File
@@ -1,92 +1,336 @@
<script context="module">
export const OFFSET = new Date().getTimezoneOffset()
</script>
<script lang="ts">
import { ScheduleService } from '$lib/gen'
import { displayDate, emptyString, formatCron } from '$lib/utils'
import CollapseLink from './CollapseLink.svelte'
import { emptyString, formatCron } from '$lib/utils'
import { Tab } from './common/index.js'
import Tabs from './common/tabs/Tabs.svelte'
// @ts-ignore
import Multiselect from 'svelte-multiselect'
import TimezonePicker from 'svelte-timezone-picker'
export let schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
export let timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
export let disabled = false
export let validCRON = true
export let edit: boolean
let preview: string[] = []
let cronError = ''
export let schedule: string = '0 0 12 * *'
export let disabled = false
let limit = 3
// If the user has already entered a cron string, switching to the basic tab will override it.
let tab: 'basic' | 'cron' = edit ? 'cron' : 'basic'
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' = 'minute'
$: !emptyString(schedule) && handleScheduleInput(schedule)
let seconds = 30
let minutes = 30
let hours = 1
const daysOfMonthOptions: number[] = Array.from(Array(31).keys()).map((i) => i + 1)
let daysOfMonth: number[] = []
// let lastDayOfMonth = false
const monthsOfYearOptions: string[] = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
]
let monthsOfYear: string[] = []
const daysOfWeekOptions: string[] = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
]
let daysOfWeek: string[] = []
let UTCTime: string = ''
async function handleScheduleInput(input: string): Promise<void> {
$: !emptyString(schedule) && handleScheduleInput(schedule, timezone)
async function handleScheduleInput(input: string, timezone: string): Promise<void> {
try {
preview = await ScheduleService.previewSchedule({
requestBody: { schedule: formatCron(input), offset: OFFSET }
requestBody: { schedule: formatCron(input), timezone }
})
cronError = ''
validCRON = true
} catch (err) {
if (err.status == 400 && err.body.includes('cron')) {
cronError = `Invalid cron expression`
validCRON = false
} else {
validCRON = false
}
}
}
$: {
// CRON string format
// sec min hour day of month month day of week year
// 0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2
let s_daysOfMonth = ''
// if (lastDayOfMonth) {
// s_daysOfMonth = 'L'
// } else
if (daysOfMonth.length > 0) {
s_daysOfMonth = daysOfMonth.join(',')
} else {
s_daysOfMonth = '*'
}
let s_months = ''
if (monthsOfYear.length > 0) {
s_months = monthsOfYear.map((m) => m.slice(0, 3).toLowerCase()).join(',')
} else {
s_months = '*'
}
let s_daysOfWeek = ''
if (daysOfWeek.length > 0) {
s_daysOfWeek = daysOfWeek.map((d) => d.slice(0, 3).toLowerCase()).join(',')
} else {
s_daysOfWeek = '*'
}
const s_AtUTCHours = parseInt(UTCTime.split(':')[0]) || '0'
const s_AtUTCMinutes = parseInt(UTCTime.split(':')[1]) || '0'
// If using the basic editor, set the cron string based on the selected options
if (tab === 'basic') {
if (executeEvery === 'second') {
if (seconds > 0) {
schedule = `*/${seconds} * * * *`
} else {
schedule = `* * * * *`
}
} else if (executeEvery === 'minute') {
if (minutes > 0) {
schedule = `0 */${minutes} * * *`
} else {
schedule = `* * * * *`
}
} else if (executeEvery === 'hour') {
if (hours > 0) {
schedule = `0 0 */${hours} * *`
} else {
schedule = `* * * * *`
}
} else if (executeEvery === 'day-month') {
schedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} *`
} else if (executeEvery === 'month') {
schedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} ${s_months}`
} else if (executeEvery === 'day-week') {
schedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} * * ${s_daysOfWeek}`
}
}
}
$: dateFormatter = new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
</script>
<div class="max-w-xl">
<div class="w-full text-right text-red-600 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}
{disabled}
/>
</div>
{#if !disabled}
<div class="flex flex-row text-xs text-blue-500 gap-3 pl-28 mb-2">
<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>
{/if}
<div class="w-full flex space-x-16 p-4">
<div class="w-full flex flex-col space-y-2">
<Tabs bind:selected={tab}>
<Tab size="sm" value="basic">
<h3> Basic </h3>
</Tab>
<Tab size="sm" value="cron">
<h3> Cron </h3>
</Tab>
</Tabs>
<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 runs will be scheduled at:</div>
<ul class="list-disc mx-12">
{#each preview.slice(0, limit) as p}
<li class="mx-2 text-gray-700 text-sm">{displayDate(p, true)}</li>
{/each}
<li class="text-sm mx-2">...</li>
{#if limit != 10}
<button class="underline text-gray-400" on:click={() => (limit = 10)}>Load more</button>
{:else}
<button class="underline text-gray-400" on:click={() => (limit = 3)}>Load less</button>
{#if tab == 'basic'}
<div class="w-full flex flex-col gap-4">
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Execute schedule every</small>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1">
<select name="execute_every" id="execute_every" bind:value={executeEvery}>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
</div>
</div>
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<Multiselect
bind:selected={monthsOfYear}
options={monthsOfYearOptions}
selectedOptionsDraggable={false}
placeholder="Every month"
/>
</div>
{/if}
</ul>
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<Multiselect
bind:selected={daysOfWeek}
options={daysOfWeekOptions}
selectedOptionsDraggable={false}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<Multiselect
bind:selected={daysOfMonth}
options={daysOfMonthOptions}
selectedOptionsDraggable={false}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div>
{/if} -->
</div>
<small>Schedule will only execute on valid calendar days</small>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At UTC Time</small>
<input type="time" name="atUTCTime" id="atUTCTime" bind:value={UTCTime} />
</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Cron</small>
<div class="flex p-2 px-4 rounded-md bg-gray-100">
<span>{schedule}</span>
</div>
</div>
</div>
{:else}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Cron</small>
<input
class="inline-block"
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="*/30 * * * *"
bind:value={schedule}
{disabled}
/>
{#if !validCRON}
<small class="text-red-600"> Invalid cron syntax </small>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Timezone</small>
<TimezonePicker {timezone} on:update={(e) => (timezone = e.detail.timezone)} />
<!-- <select name="timezone" id="timezone" bind:value={offset}>
<option value={-11 * 60}>UTC-11</option>
<option value={-10 * 60}>UTC-10</option>
<option value={-9 * 60}>UTC-9</option>
<option value={-8 * 60}>UTC-8</option>
<option value={-7 * 60}>UTC-7</option>
<option value={-6 * 60}>UTC-6</option>
<option value={-5 * 60}>UTC-5</option>
<option value={-4 * 60}>UTC-4</option>
<option value={-3 * 60}>UTC-3</option>
<option value={-2 * 60}>UTC-2</option>
<option value={-1 * 60}>UTC-1</option>
<option value={0 * 60}>UTC+0</option>
<option value={1 * 60}>UTC+1</option>
<option value={2 * 60}>UTC+2</option>
<option value={3 * 60}>UTC+3</option>
<option value={4 * 60}>UTC+4</option>
<option value={5 * 60}>UTC+5</option>
<option value={6 * 60}>UTC+6</option>
<option value={7 * 60}>UTC+7</option>
<option value={8 * 60}>UTC+8</option>
<option value={9 * 60}>UTC+9</option>
<option value={10 * 60}>UTC+10</option>
<option value={11 * 60}>UTC+11</option>
<option value={12 * 60}>UTC+12</option>
<option value={13 * 60}>UTC+13</option>
<option value={14 * 60}>UTC+14</option>
</select> -->
</div>
{/if}
</CollapseLink>
</div>
<div class="w-full flex flex-col space-y-2">
<h3>Execution summary</h3>
<hr />
<div class="flex flex-col space-y-2">
<small>Estimated upcoming events ({timezone})</small>
<div class="flex flex-col rounded-md p-4 border text-gray-600">
{#each preview as date}
<div class="flex items-center space-x-2">
<span>{dateFormatter(new Date(date))}</span>
</div>
{/each}
</div>
</div>
</div>
</div>
@@ -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<FlowState>
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<string>(selectedId ?? 'settings-metadata')
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
const scheduleStore = writable<Schedule>({ args: {}, cron: '', timezone: '', enabled: false })
const previewArgsStore = writable<Record<string, any>>(initialArgs)
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(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
})
@@ -7,26 +7,26 @@
</script>
<script lang="ts">
import Toggle from '$lib/components/Toggle.svelte'
import { FlowService, ScheduleService, Script, ScriptService, type Flow } from '$lib/gen'
import { canWrite, emptyString, formatCron, sendUserToast } from '$lib/utils'
import { Alert, Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import CronInput, { OFFSET } from '$lib/components/CronInput.svelte'
import CronInput from '$lib/components/CronInput.svelte'
import Path from '$lib/components/Path.svelte'
import Required from '$lib/components/Required.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { FlowService, ScheduleService, Script, ScriptService, type Flow } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, formatCron, sendUserToast } from '$lib/utils'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
let initialPath = ''
let edit = true
let schedule: string = '0 0 12 * *'
let timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
let itemKind: 'flow' | 'script' = 'script'
@@ -91,6 +91,7 @@
})
enabled = s.enabled
schedule = s.schedule
timezone = s.timezone
script_path = s.script_path ?? ''
is_flow = s.is_flow
args = s.args ?? {}
@@ -107,6 +108,7 @@
path: initialPath,
requestBody: {
schedule: formatCron(schedule),
timezone,
args
}
})
@@ -117,7 +119,7 @@
requestBody: {
path,
schedule: formatCron(schedule),
offset: OFFSET,
timezone,
script_path,
is_flow,
args,
@@ -190,7 +192,8 @@
<span class="mr-1">Schedule</span>
<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
</h2>
<CronInput disabled={!can_write} bind:schedule bind:validCRON />
<CronInput disabled={!can_write} {edit} bind:schedule bind:timezone bind:validCRON />
<h2 class="border-b pb-1 mt-8 mb-2">Runnable</h2>
{#if !edit}
@@ -9,7 +9,7 @@
const { schedule, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
<CronInput bind:schedule={$schedule.cron} />
<CronInput edit={true} bind:schedule={$schedule.cron} bind:timezone={$schedule.timezone} />
<SchemaForm schema={$flowStore.schema} bind:args={$schedule.args} />
{#if emptyString($schedule.cron)}
<p class="text-xs text-gray-600 mt-10">Define a schedule frequency first</p>
@@ -3,6 +3,7 @@ import { ScheduleService } from '$lib/gen'
export type Schedule = {
args: Record<string, any>
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 ?? {}
}
}
+8 -5
View File
@@ -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
})
}
}
@@ -412,7 +412,12 @@
<JobArgs args={schedule.args ?? {}} />
</div>
<div class="box max-w-lg mt-2">
<CronInput disabled={true} schedule={schedule.schedule} />
<CronInput
disabled={true}
edit={true}
schedule={schedule.schedule}
timezone={schedule.timezone}
/>
</div>
</div>
{/if}
@@ -63,9 +63,9 @@
<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 }} on:click={() => scheduleEditor.openNew(false)}
>New&nbsp;schedule</Button
>
<Button size="md" startIcon={{ icon: faPlus }} on:click={() => scheduleEditor.openNew(false)}>
New&nbsp;schedule
</Button>
</PageHeader>
<div class="mt-10 mb-40">
{#if loading}
@@ -79,15 +79,15 @@
<th class="!px-0" />
<th>Schedule</th>
<th>Script/Flow</th>
<th>schedule</th>
<th>Schedule</th>
<th>Timezone</th>
<th />
<th>off/on</th>
<th>timezone</th>
<th>last edit</th>
<th>Enabled</th>
<th>Last Edit</th>
<th />
</tr>
<tbody slot="body">
{#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 }}
<tr class={enabled ? '' : 'bg-gray-50'}>
<td class="!px-0 text-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
@@ -100,17 +100,25 @@
{path}
</button>
</td>
<td
><div class="inline-flex flex-row gap-x-2 align-middle w-full"
><div class="grow"
><a
<td>
<div class="inline-flex flex-row gap-x-2 align-middle w-full">
<div class="grow">
<a
class="text-sm break-words"
href="{is_flow ? '/flows/get' : '/scripts/get'}/{script_path}"
>{script_path}</a
></div
><Badge class="text-2xs font-mono">{is_flow ? 'flow' : 'script'}</Badge></div
>
</td><td><Badge color="blue">{schedule}</Badge></td>
>
{script_path}
</a>
</div>
<Badge class="text-2xs font-mono">{is_flow ? 'flow' : 'script'}></Badge>
</div>
</td>
<td>
<Badge color="blue">{schedule}</Badge>
</td>
<td>
<Badge color="blue">{timezone}</Badge>
</td>
<td>
<div class="w-10">
{#if error}
@@ -135,8 +143,8 @@
</div>
</Popover>
{/if}
</div></td
>
</div>
</td>
<td>
<Toggle
checked={enabled}
@@ -147,12 +155,11 @@
sendUserToast('not enough permission', true)
}
}}
/></td
>
<td><Badge color="blue">{offset_ < 0 ? '+' : ''}{(offset_ / 60) * -1}</Badge></td>
<td
><span class="text-2xs">By {edited_by} <br />the {displayDate(edited_at)}</span></td
>
/>
</td>
<td>
<span class="text-2xs">By {edited_by} <br />the {displayDate(edited_at)}</span>
</td>
<td>
<div class="inline-flex gap-2">
<Button