mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 00:00:33 +00:00
require ownership to move items && automatically edit schedule paths
This commit is contained in:
+427
-109
@@ -487,6 +487,35 @@
|
||||
},
|
||||
"query": "SELECT * from workspace_invite WHERE email = $1"
|
||||
},
|
||||
"1b9a418e1a6bda6abde432112a4e94bfef3708a57a8fa31670a61e001aaf2fd6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies"
|
||||
]
|
||||
},
|
||||
"name": "job_kind"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND job_kind = $2"
|
||||
},
|
||||
"1bf2ca894246bd716875635b2d0c294a1ce2ed21916097ea165df240f7421a1e": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1067,18 +1096,6 @@
|
||||
},
|
||||
"query": "UPDATE app SET versions = array_append(versions, $1) WHERE id = $2"
|
||||
},
|
||||
"3fabb3857c9cf2d057814b54ee54a95d01b6a7d9e89bea239b832a9d70f0044b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false"
|
||||
},
|
||||
"453501fbd61efd26647baf9b6ef702ce0bc2e920914e9f08fe5f2a5f4ab03f02": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1189,103 +1206,6 @@
|
||||
},
|
||||
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['step'], $1)\n WHERE id = $2\n "
|
||||
},
|
||||
"4eb6b80c410e00e8a72eced6c25cdd3ed941e7a46a0619173f272eec7f28a3c1": {
|
||||
"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",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *"
|
||||
},
|
||||
"5061c0d054bf4f028e7fe51a8f9389024c6ae4492755cadac0f7167e5300bda0": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1557,6 +1477,122 @@
|
||||
},
|
||||
"query": "SELECT app.id, app.path, app.summary, app.versions, app.policy,\n app.extra_perms, app_version.value, \n app_version.created_at, app_version.created_by from app, app_version \n WHERE app_version.id = $1 AND app.id = app_version.flow_id AND app.workspace_id = $2"
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"name": "is_flow",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT is_flow FROM schedule WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"576b00c515ee7cbf628b1881596d8a03c1c506b309e39a0048a3a8fe18f37070": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2772,6 +2808,100 @@
|
||||
},
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1"
|
||||
},
|
||||
"911b1e1f2a5ba6d5159916e5598020e680c45043b0736ad0153ee261a151dd90": {
|
||||
"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",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *"
|
||||
},
|
||||
"924e1d5cbb682db0d6121dd911f38cf8948d0e59ca47428daa4f43fce686ba80": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3692,6 +3822,100 @@
|
||||
},
|
||||
"query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"bfc760c2f3f1477b177a7c570ddadda818dd3f57142165f381c1db01c26cf1bf": {
|
||||
"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",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *"
|
||||
},
|
||||
"c07577e99ef7817473b7f8cef2a4d97e9867260f932aeed2755964fab4a4e0de": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -4518,6 +4742,100 @@
|
||||
},
|
||||
"query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator"
|
||||
},
|
||||
"eced0a09ba547ce1dccb54a5419b22373603c9d01f77047b3553bde125bf71e8": {
|
||||
"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",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *"
|
||||
},
|
||||
"ed400dbf4a1fa597bc984240cfe9069369a8a265b127116187d78c0ba93ae076": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
@@ -4806,10 +4806,6 @@ components:
|
||||
properties:
|
||||
schedule:
|
||||
type: string
|
||||
script_path:
|
||||
type: string
|
||||
is_flow:
|
||||
type: boolean
|
||||
args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
required:
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::collections::HashMap;
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
jobs::script_path_to_payload,
|
||||
users::{Authed, OptAuthed},
|
||||
users::{require_owner_of_path, Authed, OptAuthed},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -309,6 +309,7 @@ async fn delete_app(
|
||||
async fn update_app(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditApp>,
|
||||
) -> Result<String> {
|
||||
@@ -324,6 +325,11 @@ async fn update_app(
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
|
||||
if let Some(npath) = &ns.path {
|
||||
if npath != path {
|
||||
if !authed.is_admin {
|
||||
require_owner_of_path(&w_id, &authed.username, &path, &db).await?;
|
||||
}
|
||||
}
|
||||
sqlb.set_str("path", npath);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +21,17 @@ use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{self, to_anyhow, Error, JsonResult, Result},
|
||||
flows::{Flow, ListFlowQuery, ListableFlow, NewFlow},
|
||||
schedule::Schedule,
|
||||
utils::{
|
||||
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
|
||||
},
|
||||
};
|
||||
use windmill_queue::{push, JobPayload};
|
||||
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload};
|
||||
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
schedule::clear_schedule,
|
||||
users::{require_owner_of_path, Authed},
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -238,6 +240,7 @@ async fn check_schedule_conflict<'c>(
|
||||
async fn update_flow(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||
Json(nf): Json<NewFlow>,
|
||||
) -> Result<String> {
|
||||
@@ -249,7 +252,7 @@ async fn update_flow(
|
||||
let schema = nf.schema.map(|x| x.0);
|
||||
let old_dep_job = sqlx::query_scalar!(
|
||||
"SELECT dependency_job FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
nf.path,
|
||||
flow_path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
@@ -270,6 +273,43 @@ async fn update_flow(
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
if nf.path != flow_path {
|
||||
if !authed.is_admin {
|
||||
require_owner_of_path(&w_id, &authed.username, &flow_path, &db).await?;
|
||||
}
|
||||
|
||||
let mut schedulables = sqlx::query_as!(
|
||||
Schedule,
|
||||
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
|
||||
nf.path,
|
||||
flow_path,
|
||||
w_id,
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
|
||||
let schedule = sqlx::query_as!(Schedule,
|
||||
"UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
|
||||
nf.path,
|
||||
flow_path,
|
||||
w_id,
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some(schedule) = schedule {
|
||||
schedulables.push(schedule);
|
||||
}
|
||||
|
||||
for schedule in schedulables {
|
||||
clear_schedule(&mut tx, flow_path, true).await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(tx, schedule).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
users::{require_owner_of_path, Authed},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -315,6 +315,7 @@ async fn delete_resource(
|
||||
async fn update_resource(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditResource>,
|
||||
) -> Result<String> {
|
||||
@@ -346,6 +347,9 @@ async fn update_resource(
|
||||
let npath = not_found_if_none(npath_o, "Resource", path)?;
|
||||
|
||||
if let Some(npath) = ns.path {
|
||||
if !authed.is_admin {
|
||||
require_owner_of_path(&w_id, &authed.username, &path, &db).await?;
|
||||
}
|
||||
sqlx::query!(
|
||||
"UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
npath,
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
@@ -15,15 +17,16 @@ use axum::{
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::Deserialize;
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{JsonResult, Result},
|
||||
utils::{not_found_if_none, Pagination, StripPath},
|
||||
};
|
||||
use windmill_queue::{
|
||||
self,
|
||||
schedule::{EditSchedule, NewSchedule, PreviewPayload, Schedule, SetEnabled},
|
||||
error::{Error, JsonResult, Result},
|
||||
schedule::Schedule,
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
};
|
||||
use windmill_queue::{self, schedule::push_scheduled_job, JobKind};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
@@ -40,17 +43,71 @@ pub fn global_service() -> Router {
|
||||
Router::new().route("/preview", post(preview_schedule))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NewSchedule {
|
||||
pub path: String,
|
||||
pub schedule: String,
|
||||
pub offset: i32,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
async fn create_schedule(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ns): Json<NewSchedule>,
|
||||
) -> Result<String> {
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
let res =
|
||||
windmill_queue::schedule::create_schedule(tx, w_id, ns, &authed.username, &authed.email)
|
||||
.await?;
|
||||
Ok(res)
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
|
||||
|
||||
let schedule = sqlx::query_as!(
|
||||
Schedule,
|
||||
"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 *",
|
||||
w_id,
|
||||
ns.path,
|
||||
ns.schedule,
|
||||
ns.offset,
|
||||
&authed.username,
|
||||
ns.script_path,
|
||||
ns.is_flow,
|
||||
ns.args,
|
||||
ns.enabled.unwrap_or(false),
|
||||
&authed.email
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("inserting schedule in {w_id}: {e}")))?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"schedule.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&ns.path.to_string()),
|
||||
Some(
|
||||
[
|
||||
Some(("schedule", ns.schedule.as_str())),
|
||||
Some(("script_path", ns.script_path.as_str())),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if ns.enabled.unwrap_or(true) {
|
||||
tx = push_scheduled_job(tx, schedule).await?
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(ns.path.to_string())
|
||||
}
|
||||
|
||||
async fn edit_schedule(
|
||||
@@ -59,9 +116,55 @@ async fn edit_schedule(
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(es): Json<EditSchedule>,
|
||||
) -> Result<String> {
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
let res = windmill_queue::schedule::edit_schedule(tx, w_id, path, es, &authed.username).await?;
|
||||
Ok(res)
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let path = path.to_path();
|
||||
|
||||
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
|
||||
let is_flow = sqlx::query_scalar!(
|
||||
"SELECT is_flow FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
|
||||
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 *",
|
||||
es.schedule,
|
||||
es.args,
|
||||
path,
|
||||
w_id,
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(tx, schedule).await?;
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"schedule.edit",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&path.to_string()),
|
||||
Some(
|
||||
[Some(("schedule", es.schedule.as_str()))]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(path.to_string())
|
||||
}
|
||||
|
||||
async fn list_schedule(
|
||||
@@ -70,9 +173,19 @@ async fn list_schedule(
|
||||
Path(w_id): Path<String>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<Schedule>> {
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
let res = windmill_queue::schedule::list_schedule(tx, w_id, pagination).await?;
|
||||
Ok(Json(res))
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
let rows = sqlx::query_as!(
|
||||
Schedule,
|
||||
"SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3",
|
||||
w_id,
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
async fn get_schedule(
|
||||
@@ -102,7 +215,15 @@ async fn exists_schedule(
|
||||
pub async fn preview_schedule(
|
||||
Json(payload): Json<PreviewPayload>,
|
||||
) -> JsonResult<Vec<DateTime<chrono::Utc>>> {
|
||||
Ok(Json(windmill_queue::schedule::preview_schedule(payload)?))
|
||||
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())
|
||||
.collect();
|
||||
|
||||
Ok(Json(upcoming))
|
||||
}
|
||||
|
||||
pub async fn set_enabled(
|
||||
@@ -111,17 +232,41 @@ pub async fn set_enabled(
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(payload): Json<SetEnabled>,
|
||||
) -> Result<String> {
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
let res = windmill_queue::schedule::set_enabled(
|
||||
tx,
|
||||
w_id,
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let path = path.to_path();
|
||||
let schedule_o = sqlx::query_as!(
|
||||
Schedule,
|
||||
"UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *",
|
||||
&payload.enabled,
|
||||
authed.email,
|
||||
path,
|
||||
payload,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
|
||||
|
||||
clear_schedule(&mut tx, path, schedule.is_flow).await?;
|
||||
|
||||
if payload.enabled {
|
||||
tx = push_scheduled_job(tx, schedule).await?;
|
||||
}
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
"schedule.setenabled",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(path),
|
||||
Some([("enabled", payload.enabled.to_string().as_ref())].into()),
|
||||
)
|
||||
.await?;
|
||||
Ok(res)
|
||||
tx.commit().await?;
|
||||
Ok(format!(
|
||||
"succesfully updated schedule at path {} to status {}",
|
||||
path, payload.enabled
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_schedule(
|
||||
@@ -129,7 +274,96 @@ async fn delete_schedule(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
let res = windmill_queue::schedule::delete_schedule(tx, w_id, path, &authed.username).await?;
|
||||
Ok(res)
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let path = path.to_path();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"schedule.delete",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("schedule {} deleted", path))
|
||||
}
|
||||
|
||||
async fn check_flow_conflict<'c>(
|
||||
tx: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
is_flow: bool,
|
||||
script_path: &str,
|
||||
) -> Result<()> {
|
||||
if path != script_path || !is_flow {
|
||||
let exists_flow = sqlx::query_scalar!(
|
||||
"SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
if exists_flow {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"The path is the same as a flow, it can only trigger that flow.
|
||||
However the provided path is: {script_path} and is_flow is {is_flow}"
|
||||
)));
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditSchedule {
|
||||
pub schedule: String,
|
||||
pub args: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub async fn clear_schedule<'c>(
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
path: &str,
|
||||
is_flow: bool,
|
||||
) -> Result<()> {
|
||||
let job_kind = if is_flow {
|
||||
JobKind::Flow
|
||||
} else {
|
||||
JobKind::Script
|
||||
};
|
||||
sqlx::query!(
|
||||
"DELETE FROM queue WHERE schedule_path = $1 AND running = false AND job_kind = $2",
|
||||
path,
|
||||
job_kind: JobKind
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PreviewPayload {
|
||||
pub schedule: String,
|
||||
pub offset: Option<i32>,
|
||||
}
|
||||
|
||||
fn get_offset(offset: Option<i32>) -> FixedOffset {
|
||||
FixedOffset::west(offset.unwrap_or(0) * 60)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetEnabled {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ use windmill_audit::{audit_log, ActionKind};
|
||||
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
schedule::clear_schedule,
|
||||
users::{require_owner_of_path, Authed},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -30,6 +31,7 @@ use std::{
|
||||
};
|
||||
use windmill_common::{
|
||||
error::{Error, JsonResult, Result},
|
||||
schedule::Schedule,
|
||||
scripts::{
|
||||
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Script, ScriptHash,
|
||||
ScriptKind, ScriptLang,
|
||||
@@ -39,7 +41,7 @@ use windmill_common::{
|
||||
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
|
||||
},
|
||||
};
|
||||
use windmill_queue;
|
||||
use windmill_queue::{self, schedule::push_scheduled_job};
|
||||
|
||||
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
|
||||
|
||||
@@ -180,6 +182,7 @@ fn hash_script(ns: &NewScript) -> i64 {
|
||||
async fn create_script(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ns): Json<NewScript>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
@@ -210,83 +213,95 @@ async fn create_script(
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
let parent_hashes_and_perms: Option<(Vec<i64>, serde_json::Value)> =
|
||||
match (&ns.parent_hash, clashing_script) {
|
||||
(None, None) => Ok(None),
|
||||
(None, Some(s)) => Err(Error::BadRequest(format!(
|
||||
"Path conflict for {} with non-archived hash {}",
|
||||
&ns.path, &s.hash
|
||||
))),
|
||||
(Some(p_hash), o) => {
|
||||
if sqlx::query_scalar!(
|
||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
p_hash.0,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
"The parent hash does not seem to exist".to_owned(),
|
||||
));
|
||||
};
|
||||
struct ParentInfo {
|
||||
p_hashes: Vec<i64>,
|
||||
perms: serde_json::Value,
|
||||
p_path: String,
|
||||
}
|
||||
let parent_hashes_and_perms: Option<ParentInfo> = match (&ns.parent_hash, clashing_script) {
|
||||
(None, None) => Ok(None),
|
||||
(None, Some(s)) => Err(Error::BadRequest(format!(
|
||||
"Path conflict for {} with non-archived hash {}",
|
||||
&ns.path, &s.hash
|
||||
))),
|
||||
(Some(p_hash), o) => {
|
||||
if sqlx::query_scalar!(
|
||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
p_hash.0,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
"The parent hash does not seem to exist".to_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
let clashing_hash_o = sqlx::query_scalar!(
|
||||
"SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2",
|
||||
p_hash.0,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
let clashing_hash_o = sqlx::query_scalar!(
|
||||
"SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2",
|
||||
p_hash.0,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some(clashing_hash) = clashing_hash_o {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"A script with hash {} with same parent_hash has been found. However, the \
|
||||
if let Some(clashing_hash) = clashing_hash_o {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"A script with hash {} with same parent_hash has been found. However, the \
|
||||
lineage must be linear: no 2 scripts can have the same parent",
|
||||
ScriptHash(clashing_hash)
|
||||
)));
|
||||
};
|
||||
ScriptHash(clashing_hash)
|
||||
)));
|
||||
};
|
||||
|
||||
let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?;
|
||||
let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?;
|
||||
|
||||
let ph = {
|
||||
let v = ps.parent_hashes.map(|x| x.0).unwrap_or_default();
|
||||
let mut v: Vec<i64> = v
|
||||
.into_iter()
|
||||
.take(MAX_HASH_HISTORY_LENGTH_STORED - 1)
|
||||
.collect();
|
||||
v.insert(0, p_hash.0);
|
||||
v
|
||||
};
|
||||
let r: Result<Option<(Vec<i64>, serde_json::Value)>> = match o {
|
||||
Some(clashing_script)
|
||||
if clashing_script.path == ns.path
|
||||
&& clashing_script.hash.0 != p_hash.0 =>
|
||||
{
|
||||
Err(Error::BadRequest(format!(
|
||||
"Path conflict for {} with non-archived hash {}",
|
||||
&ns.path, &clashing_script.hash
|
||||
)))
|
||||
}
|
||||
Some(_) => Ok(Some((ph, ps.extra_perms))),
|
||||
None => Ok(Some((ph, ps.extra_perms))),
|
||||
};
|
||||
sqlx::query!(
|
||||
"UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2",
|
||||
p_hash.0,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
r
|
||||
if ps.path != ns.path {
|
||||
if !authed.is_admin {
|
||||
require_owner_of_path(&w_id, &authed.username, &ps.path, &db).await?;
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.0[..]);
|
||||
let ph = {
|
||||
let v = ps.parent_hashes.map(|x| x.0).unwrap_or_default();
|
||||
let mut v: Vec<i64> = v
|
||||
.into_iter()
|
||||
.take(MAX_HASH_HISTORY_LENGTH_STORED - 1)
|
||||
.collect();
|
||||
v.insert(0, p_hash.0);
|
||||
v
|
||||
};
|
||||
let r: Result<Option<ParentInfo>> = match o {
|
||||
Some(clashing_script)
|
||||
if clashing_script.path == ns.path && clashing_script.hash.0 != p_hash.0 =>
|
||||
{
|
||||
Err(Error::BadRequest(format!(
|
||||
"Path conflict for {} with non-archived hash {}",
|
||||
&ns.path, &clashing_script.hash
|
||||
)))
|
||||
}
|
||||
Some(_) | None => Ok(Some(ParentInfo {
|
||||
p_hashes: ph,
|
||||
perms: ps.extra_perms,
|
||||
p_path: ps.path,
|
||||
})),
|
||||
};
|
||||
sqlx::query!(
|
||||
"UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2",
|
||||
p_hash.0,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
r
|
||||
}
|
||||
}?;
|
||||
|
||||
let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.p_hashes[..]);
|
||||
let extra_perms = parent_hashes_and_perms
|
||||
.as_ref()
|
||||
.map(|v| v.1.clone())
|
||||
.map(|v| v.perms.clone())
|
||||
.unwrap_or(json!({}));
|
||||
|
||||
let lock = if ns.language == ScriptLang::Bash || ns.language == ScriptLang::Deno {
|
||||
@@ -322,6 +337,26 @@ async fn create_script(
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some(p_path) = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone()) {
|
||||
let schedulables = sqlx::query_as!(
|
||||
Schedule,
|
||||
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *",
|
||||
ns.path,
|
||||
p_path,
|
||||
w_id,
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
|
||||
for schedule in schedulables {
|
||||
clear_schedule(&mut tx, &schedule.path, false).await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(tx, schedule).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut tx = if needs_lock_gen {
|
||||
let dependencies = match ns.language {
|
||||
ScriptLang::Python3 => {
|
||||
|
||||
@@ -782,6 +782,31 @@ pub async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
pub async fn require_owner_of_path(w_id: &str, username: &str, path: &str, db: &DB) -> Result<()> {
|
||||
let splitted = path.split("/").collect::<Vec<&str>>();
|
||||
if splitted[0] == "u" {
|
||||
if splitted[1] == username {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"only the owner {} is authorized to perform this operation",
|
||||
splitted[1]
|
||||
)));
|
||||
}
|
||||
} else if splitted[0] == "g" {
|
||||
let groups = get_groups_for_user(w_id, username, db).await?;
|
||||
if groups.contains(&username.to_string()) {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"{} is not a member of {} and hence is not authorized to perform this operation",
|
||||
username, splitted[1]
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(Error::BadRequest(format!("not recognized owner kind")))
|
||||
}
|
||||
|
||||
async fn whois(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, username)): Path<(String, String)>,
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::sync::Arc;
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
oauth2::{AllClients, _refresh_token},
|
||||
users::Authed,
|
||||
users::{require_owner_of_path, Authed},
|
||||
BaseUrl,
|
||||
};
|
||||
/*
|
||||
@@ -297,6 +297,7 @@ struct EditVariable {
|
||||
async fn update_variable(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditVariable>,
|
||||
) -> Result<String> {
|
||||
@@ -351,6 +352,9 @@ async fn update_variable(
|
||||
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
|
||||
|
||||
if let Some(npath) = ns.path {
|
||||
if !authed.is_admin {
|
||||
require_owner_of_path(&w_id, &authed.username, &path, &db).await?;
|
||||
}
|
||||
sqlx::query!(
|
||||
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
npath,
|
||||
|
||||
@@ -17,6 +17,7 @@ pub mod flow_status;
|
||||
pub mod flows;
|
||||
pub mod more_serde;
|
||||
pub mod oauth2;
|
||||
pub mod schedule;
|
||||
pub mod scripts;
|
||||
pub mod users;
|
||||
pub mod utils;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* This file and its contents are licensed under the AGPLv3 License.
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use chrono::DateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Debug)]
|
||||
pub struct Schedule {
|
||||
pub workspace_id: String,
|
||||
pub path: String,
|
||||
pub edited_by: String,
|
||||
pub edited_at: DateTime<chrono::Utc>,
|
||||
pub schedule: String,
|
||||
pub offset_: i32,
|
||||
pub enabled: bool,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub email: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
@@ -8,45 +8,16 @@
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, Duration, FixedOffset};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{query_scalar, FromRow, Postgres, Transaction};
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use chrono::Duration;
|
||||
use sqlx::{query_scalar, Postgres, Transaction};
|
||||
use windmill_common::{
|
||||
error::{self, Error, Result},
|
||||
utils::{get_owner_from_path, not_found_if_none, now_from_db, paginate, Pagination, StripPath},
|
||||
error::{self, Result},
|
||||
schedule::Schedule,
|
||||
utils::{get_owner_from_path, now_from_db, StripPath},
|
||||
};
|
||||
|
||||
use crate::{push, JobPayload};
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Debug)]
|
||||
pub struct Schedule {
|
||||
pub workspace_id: String,
|
||||
pub path: String,
|
||||
pub edited_by: String,
|
||||
pub edited_at: DateTime<chrono::Utc>,
|
||||
pub schedule: String,
|
||||
pub offset_: i32,
|
||||
pub enabled: bool,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub email: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NewSchedule {
|
||||
pub path: String,
|
||||
pub schedule: String,
|
||||
pub offset: i32,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn push_scheduled_job<'c>(
|
||||
mut tx: Transaction<'c, Postgres>,
|
||||
schedule: Schedule,
|
||||
@@ -129,181 +100,6 @@ pub async fn push_scheduled_job<'c>(
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn create_schedule(
|
||||
mut tx: Transaction<'_, Postgres>,
|
||||
w_id: String,
|
||||
ns: NewSchedule,
|
||||
username: &str,
|
||||
email: &str,
|
||||
) -> Result<String> {
|
||||
cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
|
||||
|
||||
let schedule = sqlx::query_as!(
|
||||
Schedule,
|
||||
"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 *",
|
||||
w_id,
|
||||
ns.path,
|
||||
ns.schedule,
|
||||
ns.offset,
|
||||
username,
|
||||
ns.script_path,
|
||||
ns.is_flow,
|
||||
ns.args,
|
||||
ns.enabled.unwrap_or(false),
|
||||
email
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("inserting schedule in {w_id}: {e}")))?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
username,
|
||||
"schedule.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&ns.path.to_string()),
|
||||
Some(
|
||||
[
|
||||
Some(("schedule", ns.schedule.as_str())),
|
||||
Some(("script_path", ns.script_path.as_str())),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if ns.enabled.unwrap_or(true) {
|
||||
tx = push_scheduled_job(tx, schedule).await?
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(ns.path.to_string())
|
||||
}
|
||||
|
||||
async fn check_flow_conflict<'c>(
|
||||
tx: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
is_flow: bool,
|
||||
script_path: &str,
|
||||
) -> error::Result<()> {
|
||||
if path != script_path || !is_flow {
|
||||
let exists_flow = sqlx::query_scalar!(
|
||||
"SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
if exists_flow {
|
||||
return Err(error::Error::BadConfig(format!(
|
||||
"The path is the same as a flow, it can only trigger that flow.
|
||||
However the provided path is: {script_path} and is_flow is {is_flow}"
|
||||
)));
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditSchedule {
|
||||
pub schedule: String,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
async fn clear_schedule<'c>(db: &mut Transaction<'c, Postgres>, path: &str) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"DELETE FROM queue WHERE schedule_path = $1 AND running = false",
|
||||
path
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn edit_schedule(
|
||||
mut tx: Transaction<'_, Postgres>,
|
||||
w_id: String,
|
||||
path: StripPath,
|
||||
es: EditSchedule,
|
||||
username: &String,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
|
||||
cron::Schedule::from_str(&es.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||
|
||||
check_flow_conflict(&mut tx, &w_id, &path, es.is_flow, &es.script_path).await?;
|
||||
|
||||
clear_schedule(&mut tx, path).await?;
|
||||
let schedule = sqlx::query_as!(
|
||||
Schedule,
|
||||
"UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path \
|
||||
= $5 AND workspace_id = $6 RETURNING *",
|
||||
es.schedule,
|
||||
es.script_path,
|
||||
es.is_flow,
|
||||
es.args,
|
||||
path,
|
||||
w_id,
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(tx, schedule).await?;
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
username,
|
||||
"schedule.edit",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&path.to_string()),
|
||||
Some(
|
||||
[
|
||||
Some(("schedule", es.schedule.as_str())),
|
||||
Some(("script_path", es.script_path.as_str())),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(path.to_string())
|
||||
}
|
||||
|
||||
pub async fn list_schedule(
|
||||
mut tx: Transaction<'_, Postgres>,
|
||||
w_id: String,
|
||||
pagination: Pagination,
|
||||
) -> Result<Vec<Schedule>> {
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
let rows = sqlx::query_as!(
|
||||
Schedule,
|
||||
"SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3",
|
||||
w_id,
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn get_schedule_opt<'c>(
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
@@ -339,110 +135,6 @@ pub async fn exists_schedule(
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PreviewPayload {
|
||||
pub schedule: String,
|
||||
pub offset: Option<i32>,
|
||||
}
|
||||
|
||||
pub fn preview_schedule(
|
||||
PreviewPayload { schedule, offset }: PreviewPayload,
|
||||
) -> Result<Vec<DateTime<chrono::Utc>>> {
|
||||
let schedule =
|
||||
cron::Schedule::from_str(&schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||
let upcoming: Vec<DateTime<chrono::Utc>> = schedule
|
||||
.upcoming(get_offset(offset))
|
||||
.take(10)
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
Ok(upcoming)
|
||||
}
|
||||
|
||||
fn get_offset(offset: Option<i32>) -> FixedOffset {
|
||||
FixedOffset::west(offset.unwrap_or(0) * 60)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetEnabled {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub async fn set_enabled(
|
||||
mut tx: Transaction<'_, Postgres>,
|
||||
w_id: String,
|
||||
path: StripPath,
|
||||
SetEnabled { enabled }: SetEnabled,
|
||||
username: &str,
|
||||
email: &str,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
let schedule_o = sqlx::query_as!(
|
||||
Schedule,
|
||||
"UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *",
|
||||
enabled,
|
||||
email,
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
|
||||
|
||||
clear_schedule(&mut tx, path).await?;
|
||||
|
||||
if enabled {
|
||||
tx = push_scheduled_job(tx, schedule).await?;
|
||||
}
|
||||
audit_log(
|
||||
&mut tx,
|
||||
username,
|
||||
"schedule.setenabled",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(path),
|
||||
Some([("enabled", enabled.to_string().as_ref())].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(format!(
|
||||
"succesfully updated schedule at path {} to status {}",
|
||||
path, enabled
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn delete_schedule(
|
||||
mut tx: Transaction<'_, Postgres>,
|
||||
w_id: String,
|
||||
path: StripPath,
|
||||
username: &str,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
username,
|
||||
"schedule.delete",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("schedule {} deleted", path))
|
||||
}
|
||||
|
||||
fn schedule_to_user(path: &str) -> String {
|
||||
format!("schedule-{}", path.replace('/', "-"))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde_json::{Map, Value};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::{error::Error, flow_status::FlowStatusModule};
|
||||
use windmill_common::{error::Error, flow_status::FlowStatusModule, schedule::Schedule};
|
||||
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob};
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
@@ -238,7 +238,7 @@ pub async fn schedule_again_if_scheduled(
|
||||
if schedule.enabled && script_path == schedule.script_path {
|
||||
let res = windmill_queue::schedule::push_scheduled_job(
|
||||
tx,
|
||||
windmill_queue::schedule::Schedule {
|
||||
Schedule {
|
||||
workspace_id: w_id.to_owned(),
|
||||
path: schedule.path.clone(),
|
||||
edited_by: schedule.edited_by,
|
||||
|
||||
@@ -35,56 +35,58 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 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}
|
||||
|
||||
<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}
|
||||
</ul>
|
||||
{#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}
|
||||
</CollapseLink>
|
||||
|
||||
<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}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</CollapseLink>
|
||||
</div>
|
||||
|
||||
@@ -90,25 +90,19 @@
|
||||
})
|
||||
const scheduleExists = await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
path: flow.path
|
||||
})
|
||||
if (scheduleExists) {
|
||||
const schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
path: flow.path
|
||||
})
|
||||
if (
|
||||
schedule.path != flow.path ||
|
||||
JSON.stringify(schedule.args) != JSON.stringify(args) ||
|
||||
schedule.schedule != cron
|
||||
) {
|
||||
if (JSON.stringify(schedule.args) != JSON.stringify(args) || schedule.schedule != cron) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
path: flow.path,
|
||||
requestBody: {
|
||||
schedule: formatCron(cron),
|
||||
script_path: flow.path,
|
||||
is_flow: true,
|
||||
args
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,21 +6,20 @@
|
||||
ResourceService,
|
||||
ScheduleService,
|
||||
ScriptService,
|
||||
VariableService,
|
||||
type Group
|
||||
VariableService
|
||||
} from '$lib/gen'
|
||||
import { GroupService } from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Required from './Required.svelte'
|
||||
import Popover from './Popover.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { faEye, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import GroupEditor from './GroupEditor.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton/ToggleButton.svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
|
||||
let meta: Meta | undefined = undefined
|
||||
@@ -68,9 +67,6 @@
|
||||
if (path == '' || path == 'u//') {
|
||||
meta = { ownerKind: 'user', name: namePlaceholder, owner: '' }
|
||||
|
||||
while ($userStore == undefined) {
|
||||
await sleep(500)
|
||||
}
|
||||
meta.owner = $userStore!.username
|
||||
|
||||
let i = 1
|
||||
@@ -85,10 +81,16 @@
|
||||
}
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
groups = await GroupService.listGroupNames({
|
||||
workspace: $workspaceStore!,
|
||||
onlyMemberOf: !($userStore?.is_admin || $superadmin)
|
||||
})
|
||||
let initialGroups: string[] = []
|
||||
if (initialPath?.split('/')?.[0] == 'g') {
|
||||
initialGroups.push(initialPath?.split('/')?.[1])
|
||||
}
|
||||
groups = initialGroups.concat(
|
||||
await GroupService.listGroupNames({
|
||||
workspace: $workspaceStore!,
|
||||
onlyMemberOf: !$userStore?.is_admin
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function validate(meta: Meta, path: string, kind: PathKind) {
|
||||
@@ -150,7 +152,7 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadGroups()
|
||||
initPath()
|
||||
}
|
||||
@@ -204,23 +206,14 @@
|
||||
</Drawer>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-3 gap-2 sm:gap-4 pb-0 mb-1">
|
||||
<div class="flex flex-col sm:flex-row items-center gap-2 sm:gap-4 pb-0 mb-1">
|
||||
{#if meta != undefined}
|
||||
<div class="flex gap-4 w-full">
|
||||
<div class="flex gap-4 shrink">
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap">
|
||||
<Popover
|
||||
>
|
||||
<span slot="text"
|
||||
>Select the group <span class="font-mono">all</span>
|
||||
to share it with all workspace users, and <span class="font-mono">user</span> to
|
||||
keep it private.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
|
||||
</span>
|
||||
</Popover>
|
||||
</span>
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap"> </span>
|
||||
|
||||
<ToggleButtonGroup
|
||||
class="mt-0.5"
|
||||
bind:selected={meta.ownerKind}
|
||||
on:selected={(e) => {
|
||||
const kind = e.detail
|
||||
@@ -239,9 +232,17 @@
|
||||
</ToggleButtonGroup>
|
||||
</label>
|
||||
{#if meta.ownerKind === 'user'}
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm">Owner</span>
|
||||
<label class="block shrink min-w-0">
|
||||
<span
|
||||
><span class="text-gray-700 text-sm mr-1">Owner</span><Tooltip
|
||||
>The prefix of a path defines the owner of an item. An owner has write permissions
|
||||
and can modify the path. An item can still be made writable or readable using
|
||||
granular perissioning.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">See docs</a></Tooltip
|
||||
></span
|
||||
>
|
||||
<input
|
||||
class="!w-36"
|
||||
type="text"
|
||||
bind:value={meta.owner}
|
||||
placeholder={$userStore?.username ?? ''}
|
||||
@@ -249,8 +250,15 @@
|
||||
/>
|
||||
</label>
|
||||
{:else}
|
||||
<label class="block w-full">
|
||||
<span class="text-gray-700 text-sm inline-flex justify-between w-full">Owner</span>
|
||||
<label class="block grow w-48">
|
||||
<span
|
||||
><span class="text-gray-700 text-sm mr-1">Owner</span><Tooltip
|
||||
>The prefix of a path defines the owner of an item. An owner has write permissions
|
||||
and can modify the path. An item can still be made writable or readable using
|
||||
granular perissioning.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a></Tooltip
|
||||
></span
|
||||
>
|
||||
|
||||
<div class="flex flex-row gap-1 w-full">
|
||||
<select class="grow w-full" {disabled} bind:value={meta.owner}>
|
||||
@@ -268,7 +276,7 @@
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
<label class="block col-span-2">
|
||||
<label class="block grow">
|
||||
<span class="text-gray-700 text-sm">
|
||||
Name
|
||||
<Required required={true} />
|
||||
@@ -291,9 +299,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="pt-0 text-xs px-1 flex flex-col-reverse sm:grid sm:grid-cols-4 sm:gap-4 w-full">
|
||||
<div class="col-span-2"><span class="font-mono">{path}</span></div>
|
||||
<div class="text-red-600 text-2xs col-span-2">{error}</div>
|
||||
<div class="flex-row flex justify-between">
|
||||
<div><span class="font-mono text-sm">{path}</span></div>
|
||||
<div class="text-red-600 text-2xs">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -51,21 +51,17 @@
|
||||
}
|
||||
|
||||
async function editResource(): Promise<void> {
|
||||
try {
|
||||
if (resourceToEdit) {
|
||||
await ResourceService.updateResource({
|
||||
workspace: $workspaceStore!,
|
||||
path: resourceToEdit.path,
|
||||
requestBody: { path, value: args, description }
|
||||
})
|
||||
sendUserToast(`Updated resource at ${path}`)
|
||||
dispatch('refresh')
|
||||
drawer.closeDrawer?.()
|
||||
} else {
|
||||
throw Error('Cannot edit undefined resourceToEdit')
|
||||
}
|
||||
} catch (err) {
|
||||
sendUserToast(`${err}`, true)
|
||||
if (resourceToEdit) {
|
||||
await ResourceService.updateResource({
|
||||
workspace: $workspaceStore!,
|
||||
path: resourceToEdit.path,
|
||||
requestBody: { path, value: args, description }
|
||||
})
|
||||
sendUserToast(`Updated resource at ${path}`)
|
||||
dispatch('refresh')
|
||||
drawer.closeDrawer?.()
|
||||
} else {
|
||||
throw Error('Cannot edit undefined resourceToEdit')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,6 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class:flex-col={col} class="flex rounded-md" role="group">
|
||||
<div class:flex-col={col} class="flex rounded-md {$$props.class}" role="group">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faBolt} scale={0.8} class="mr-1" />
|
||||
Trigger (script)
|
||||
Trigger (Script)
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
@@ -49,7 +49,7 @@
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faCode} scale={0.8} class="mr-1" />
|
||||
Action (script)
|
||||
Action (Script)
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left p-2 hover:bg-gray-100"
|
||||
@@ -61,7 +61,7 @@
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faCheck} scale={0.8} class="mr-1" />
|
||||
Approval (script)
|
||||
Approval (Script)
|
||||
</button>
|
||||
<button
|
||||
class="w-full inline-flex gap-1 text-left p-2 hover:bg-gray-100"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { AppService, FlowService, ListableApp, Script, ScriptService, type Flow } from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { Skeleton, ToggleButton, ToggleButtonGroup } from '$lib/components/common'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
@@ -134,7 +134,7 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
if (($userStore || $superadmin) && $workspaceStore) {
|
||||
if ($userStore && $workspaceStore) {
|
||||
loadScripts()
|
||||
loadFlows()
|
||||
loadApps()
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
label: 'Workspace',
|
||||
href: '/workspace_settings',
|
||||
icon: faCog,
|
||||
disabled: !($userStore?.is_admin || $superadmin)
|
||||
disabled: !$userStore?.is_admin
|
||||
},
|
||||
{ label: 'Workers', href: '/workers', icon: faRobot, disabled: $userStore?.operator }
|
||||
]
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
{#if !isCollapsed}
|
||||
<span class={classNames('whitespace-pre truncate')}>
|
||||
{$userStore?.username ?? ($superadmin ? $superadmin : '___')}
|
||||
{#if $userStore?.is_admin || $superadmin}
|
||||
{#if $userStore?.is_admin}
|
||||
<Icon data={faCrown} scale={0.6} />
|
||||
{:else if $userStore?.operator}
|
||||
<Icon class="ml-2" data={faHardHat} scale={0.8} />
|
||||
|
||||
@@ -51,10 +51,9 @@
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
usernames =
|
||||
$userStore?.is_admin || $superadmin
|
||||
? await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
: [$userStore?.username ?? '']
|
||||
usernames = $userStore?.is_admin
|
||||
? await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
: [$userStore?.username ?? '']
|
||||
}
|
||||
|
||||
async function gotoUsername(username: string | undefined): Promise<void> {
|
||||
@@ -98,11 +97,11 @@
|
||||
<label>
|
||||
<select class="px-6" bind:value={username} on:change={() => gotoUsername(username)}>
|
||||
{#if usernames}
|
||||
{#if $userStore?.is_admin || $superadmin}
|
||||
{#if $userStore?.is_admin}
|
||||
<option selected>all</option>
|
||||
{/if}
|
||||
{#each usernames as e}
|
||||
{#if e == username || $userStore?.is_admin || $superadmin}
|
||||
{#if e == username || $userStore?.is_admin}
|
||||
<option>{e}</option>
|
||||
{:else}
|
||||
<option disabled>{e}</option>
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
<input id="inp" type="text" bind:value={newResourceType.name} />
|
||||
</div>
|
||||
|
||||
{#if $userStore?.is_admin || $superadmin}
|
||||
{#if $userStore?.is_admin}
|
||||
<Toggle
|
||||
bind:checked={disableCustomPrefix}
|
||||
options={{ right: 'disable c_ prefix (admin only)' }}
|
||||
@@ -567,7 +567,7 @@
|
||||
workspaces</Tooltip
|
||||
></Badge
|
||||
>
|
||||
{:else if $userStore?.is_admin || $superadmin}
|
||||
{:else if $userStore?.is_admin}
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Alert, Badge, Button } from '$lib/components/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -23,6 +23,7 @@
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import CronInput, { OFFSET } from '$lib/components/CronInput.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import { faList, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
let initialPath = $page.url.searchParams.get('edit') || ''
|
||||
let edit = initialPath === '' ? false : true
|
||||
@@ -70,6 +71,7 @@
|
||||
enabled = s.enabled
|
||||
schedule = s.schedule
|
||||
script_path = s.script_path ?? ''
|
||||
is_flow = s.is_flow
|
||||
initialScriptPath = script_path
|
||||
args = s.args ?? {}
|
||||
} catch (err) {
|
||||
@@ -84,8 +86,6 @@
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatCron(schedule),
|
||||
script_path: script_path,
|
||||
is_flow: is_flow,
|
||||
args
|
||||
}
|
||||
})
|
||||
@@ -117,7 +117,15 @@
|
||||
</script>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader title={edit ? 'Edit schedule ' + initialPath : 'New schedule'} />
|
||||
<PageHeader title={edit ? 'Edit schedule ' + initialPath : 'New schedule'}>
|
||||
<Button
|
||||
startIcon={{ icon: faSave }}
|
||||
disabled={!allowSchedule || pathError != ''}
|
||||
on:click={scheduleScript}
|
||||
>
|
||||
{edit ? 'Save' : 'Schedule'}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<div>
|
||||
{#if !edit}
|
||||
@@ -132,17 +140,41 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Script</h2>
|
||||
<p class="text-xs mb-1 text-gray-600">
|
||||
Pick a script or flow to be triggered by the schedule<Required required={true} />
|
||||
</p>
|
||||
<ScriptPicker
|
||||
initialPath={initialScriptPath}
|
||||
kind={Script.kind.SCRIPT}
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
/>
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Runnable</h2>
|
||||
{#if !edit}
|
||||
<p class="text-xs mb-1 text-gray-600">
|
||||
Pick a script or flow to be triggered by the schedule<Required required={true} />
|
||||
</p>
|
||||
<ScriptPicker
|
||||
initialPath={initialScriptPath}
|
||||
kind={Script.kind.SCRIPT}
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
/>
|
||||
{:else}
|
||||
<Alert type="info" title="Runnable path cannot be edited">
|
||||
Once a schedule is created, the runnable path cannot be changed. However, when renaming a
|
||||
script or a flow, the runnable path will automatically update itself. To edit the runnable
|
||||
path, you can always delete the schedule and create a new on.
|
||||
</Alert>
|
||||
<div class="mt-4 flex items-center gap-2 max-w-xl">
|
||||
<div> <Badge large color="blue">{itemKind}</Badge></div><input
|
||||
type="text"
|
||||
disabled
|
||||
value={script_path}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
href="/runs/{script_path}"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faList }}
|
||||
>
|
||||
View runs
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={edit ? '' : 'mt-2 md:mt-6'}>
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Arguments</h2>
|
||||
{#if runnable}
|
||||
@@ -178,12 +210,5 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex flex-row-reverse mt-2 ">
|
||||
<div>
|
||||
<Button disabled={!allowSchedule || pathError != ''} on:click={scheduleScript}>
|
||||
{edit ? 'Save' : 'Schedule'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
|
||||
@@ -56,6 +56,7 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
let script: Script | undefined
|
||||
@@ -445,31 +446,37 @@
|
||||
</Tabs>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Danger zone</h3>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
script?.hash && deleteScript(script.hash)
|
||||
}}
|
||||
color="red"
|
||||
variant="contained"
|
||||
startIcon={{ icon: faTrash }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
script?.hash && archiveScript(script.hash)
|
||||
}}
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faArchive }}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
{#if can_write}
|
||||
<h3>Danger zone</h3>
|
||||
<div class="flex gap-2">
|
||||
<Popover>
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
script?.hash && deleteScript(script.hash)
|
||||
}}
|
||||
color="red"
|
||||
variant="contained"
|
||||
startIcon={{ icon: faTrash }}
|
||||
disabled={!($superadmin || ($userStore?.is_admin ?? false))}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<span slot="text">require to be admin</span>
|
||||
</Popover>
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
script?.hash && archiveScript(script.hash)
|
||||
}}
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faArchive }}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
/>
|
||||
|
||||
<CenteredPage>
|
||||
{#if $userStore?.is_admin || $superadmin}
|
||||
{#if $userStore?.is_admin}
|
||||
<PageHeader title="Workspace Settings of {$workspaceStore}" />
|
||||
|
||||
<PageHeader title="Members" primary={false} />
|
||||
|
||||
Reference in New Issue
Block a user