feat: Mute workspace error handler for flows and scripts (#2458)

* feat: Mute workspace error handler for flows and scripts

* implement BE muting

* Add tooltip to bell icon

* change field name to ws_error_handler_muted

* revert new field for queue table

* Error when error handler not set

* extract svelte component and use flow table column

* Fix flow error handler result arg
This commit is contained in:
Guillaume Bouvignies
2023-10-17 22:07:13 +02:00
committed by GitHub
parent 61ac5e45f5
commit 09e29d6b3c
35 changed files with 631 additions and 46 deletions
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script SET ws_error_handler_muted = $3 WHERE workspace_id = $2 AND path = $1 AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "1182fe055306d7ea435d76b74d781e066915c8397e6bbc9e408ff3dda9fec27f"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND archived = false)",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND archived = false)",
"describe": {
"columns": [
{
@@ -75,5 +75,5 @@
true
]
},
"hash": "5cd89ab614d3cac80fb81627267ee85b191263989b0c78b2bfce77e796e96825"
"hash": "251f18ccdd89255023258a33285757801cabe4e86385e2dea10b666d1c6f2b61"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"query": "select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -75,5 +75,5 @@
true
]
},
"hash": "65835f2e5ad38f7cc6b147dadfef6f580f15bca96d9746c9359e98ca793f8f1f"
"hash": "27ab808d7e05235180449df043ef57c6fbe7d3e261e5a52313d01782739c0e77"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET ws_error_handler_muted = $3 WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "899b48109ce20a8fbcf9c8e9339713dcdf4173564d388f1927dea06653c718d5"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"query": "select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -69,5 +69,5 @@
true
]
},
"hash": "123c0608e229c29187009b7961355ddd99c4ad1f46b876dd86e372b84d806ecd"
"hash": "901c430cba96e5cab1a186207ea708f3d34a04c8ab03b385f8f2e9c662d3e724"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ws_error_handler_muted FROM script WHERE workspace_id = $1 AND hash = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ws_error_handler_muted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "97b6bbcd655001a99f0cd9b127dcddf6d45ad87308c1294e52f1ae65529271d1"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ws_error_handler_muted FROM flow WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ws_error_handler_muted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "9f545c9c03d550ba0468dd7b4be39b76a7754a53f4508dce05e3f36300db483b"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
"describe": {
"columns": [],
"parameters": {
@@ -58,10 +58,11 @@
"Int4",
"Int4",
"Int4",
"Bool",
"Bool"
]
},
"nullable": []
},
"hash": "438b5b5d29b05846c2e074cad2404e797527841cd97dba80c271cbefafae65cc"
"hash": "b5646d44502b289e5d92f53f74bf0a52b8bf76775d87a1f698ed2a3c3446c86e"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "error_handler",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "f8b34e09453d51d3df5be20652938a890ee353fec64ee73c312a3352da7f7515"
}
@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE script DROP COLUMN ws_error_handler_muted;
ALTER TABLE flow DROP COLUMN ws_error_handler_muted;
@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN ws_error_handler_muted BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE flow ADD COLUMN ws_error_handler_muted BOOLEAN NOT NULL DEFAULT false;
+2
View File
@@ -2564,6 +2564,7 @@ async fn test_flow_lock_all(db: Pool<Postgres>) {
open_flow: flow,
path: "g/all/flow_lock_all".to_owned(),
tag: None,
ws_error_handler_muted: None,
},
draft_only: None,
},
@@ -2917,6 +2918,7 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
summary: "".to_string(),
tag: None,
schema: std::collections::HashMap::new(),
ws_error_handler_muted: Some(false),
},
).await.unwrap();
+72
View File
@@ -3931,6 +3931,8 @@ paths:
type: number
dedicated_worker:
type: boolean
ws_error_handler_muted:
type: boolean
required: &ref_29
- hash
- path
@@ -4115,6 +4117,8 @@ paths:
type: number
dedicated_worker:
type: boolean
ws_error_handler_muted:
type: boolean
required: &ref_32
- path
- summary
@@ -4128,6 +4132,38 @@ paths:
text/plain:
schema:
type: string
/w/{workspace}/scripts/toggle_workspace_error_handler/p/{path}:
post:
summary: Toggle ON and OFF the workspace error handler for a given script
operationId: toggleWorkspaceErrorHandlerForScript
tags:
- script
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
- name: path
in: path
required: true
schema: *ref_27
requestBody:
description: Workspace error handler enabled
required: true
content:
application/json:
schema:
type: object
properties:
muted:
type: boolean
responses:
'200':
description: error handler toggled
content:
text/plain:
schema:
type: string
/workers/custom_tags:
get:
summary: >-
@@ -4955,6 +4991,8 @@ paths:
type: boolean
tag:
type: string
ws_error_handler_muted:
type: boolean
required: &ref_141
- path
- edited_by
@@ -4989,6 +5027,38 @@ paths:
application/json:
schema:
allOf: *ref_41
/w/{workspace}/flows/toggle_workspace_error_handler/{path}:
post:
summary: Toggle ON and OFF the workspace error handler for a given flow
operationId: toggleWorkspaceErrorHandlerForFlow
tags:
- flow
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
- name: path
in: path
required: true
schema: *ref_27
requestBody:
description: Workspace error handler enabled
required: true
content:
application/json:
schema:
type: object
properties:
muted:
type: boolean
responses:
'200':
description: error handler toggled
content:
text/plain:
schema:
type: string
/w/{workspace}/flows/get/draft/{path}:
get:
summary: get flow by path with draft
@@ -5066,6 +5136,8 @@ paths:
type: string
tag:
type: string
ws_error_handler_muted:
type: boolean
required:
- path
- type: object
+62
View File
@@ -2898,6 +2898,33 @@ paths:
schema:
type: string
/w/{workspace}/scripts/toggle_workspace_error_handler/p/{path}:
post:
summary: Toggle ON and OFF the workspace error handler for a given script
operationId: toggleWorkspaceErrorHandlerForScript
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
requestBody:
description: Workspace error handler enabled
required: true
content:
application/json:
schema:
type: object
properties:
muted:
type: boolean
responses:
"200":
description: error handler toggled
content:
text/plain:
schema:
type: string
/workers/custom_tags:
get:
summary: get all instance custom tags (tags are used to dispatch jobs to
@@ -3446,6 +3473,33 @@ paths:
schema:
$ref: "#/components/schemas/Flow"
/w/{workspace}/flows/toggle_workspace_error_handler/{path}:
post:
summary: Toggle ON and OFF the workspace error handler for a given flow
operationId: toggleWorkspaceErrorHandlerForFlow
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
requestBody:
description: Workspace error handler enabled
required: true
content:
application/json:
schema:
type: object
properties:
muted:
type: boolean
responses:
"200":
description: error handler toggled
content:
text/plain:
schema:
type: string
/w/{workspace}/flows/get/draft/{path}:
get:
summary: get flow by path with draft
@@ -6390,6 +6444,8 @@ components:
type: number
dedicated_worker:
type: boolean
ws_error_handler_muted:
type: boolean
required:
- hash
- path
@@ -6467,6 +6523,8 @@ components:
type: number
dedicated_worker:
type: boolean
ws_error_handler_muted:
type: boolean
required:
- path
- summary
@@ -7712,6 +7770,8 @@ components:
type: boolean
tag:
type: string
ws_error_handler_muted:
type: boolean
required:
- path
- edited_by
@@ -7728,6 +7788,8 @@ components:
type: string
tag:
type: string
ws_error_handler_muted:
type: boolean
required:
- path
+85 -2
View File
@@ -53,6 +53,10 @@ pub fn workspaced_service() -> Router {
.route("/get/draft/*path", get(get_flow_by_path_w_draft))
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
.route(
"/toggle_workspace_error_handler/*path",
post(toggle_workspace_error_handler),
)
}
pub fn global_service() -> Router {
@@ -114,7 +118,8 @@ async fn list_flows(
"extra_perms",
"favorite.path IS NOT NULL as starred",
"draft.path IS NOT NULL as has_draft",
"draft_only"
"draft_only",
"ws_error_handler_muted"
])
.left()
.join("favorite")
@@ -209,6 +214,56 @@ pub async fn get_hub_flow_by_id(
Ok(Json(value))
}
#[derive(Deserialize)]
pub struct ToggleWorkspaceErrorHandler {
pub muted: Option<bool>,
}
async fn toggle_workspace_error_handler(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(req): Json<ToggleWorkspaceErrorHandler>,
) -> Result<String> {
#[cfg(not(feature = "enterprise"))]
if true {
return Err(Error::BadRequest(
"Muting the error handler for certain flow is only available in enterprise version"
.to_string(),
));
}
let mut tx = user_db.begin(&authed).await?;
let error_handler_maybe: Option<String> = sqlx::query_scalar!(
"SELECT error_handler FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.fetch_optional(&mut *tx)
.await?
.unwrap_or(None);
return match error_handler_maybe {
Some(_) => {
sqlx::query_scalar!(
"UPDATE flow SET ws_error_handler_muted = $3 WHERE path = $1 AND workspace_id = $2",
path.to_path(),
w_id,
req.muted,
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok("".to_string())
}
None => {
tx.commit().await?;
Err(Error::ExecutionErr(
"Workspace error handler needs to be defined".to_string(),
))
}
};
}
async fn check_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
@@ -237,6 +292,19 @@ async fn create_flow(
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
#[cfg(not(feature = "enterprise"))]
if nf
.value
.get("ws_error_handler_muted")
.map(|val| val.as_bool().unwrap_or(false))
.is_some_and(|val| val)
{
return Err(Error::BadRequest(
"Muting the error handler for certain flow is only available in enterprise version"
.to_string(),
));
}
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
let authed = maybe_refresh_folders(&nf.path, &w_id, authed, &db).await;
@@ -372,6 +440,19 @@ async fn update_flow(
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
#[cfg(not(feature = "enterprise"))]
if nf
.value
.get("ws_error_handler_muted")
.map(|val| val.as_bool().unwrap_or(false))
.is_some_and(|val| val)
{
return Err(Error::BadRequest(
"Muting the error handler for certain flow is only available in enterprise version"
.to_string(),
));
}
let flow_path = flow_path.to_path();
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await;
@@ -554,6 +635,7 @@ pub struct FlowWDraft {
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
pub tag: Option<String>,
pub ws_error_handler_muted: Option<bool>,
}
async fn get_flow_by_path_w_draft(
@@ -565,7 +647,7 @@ async fn get_flow_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let flow_o = sqlx::query_as::<_, FlowWDraft>(
"SELECT flow.path, flow.summary, flow,description, flow.schema, flow.value, flow.extra_perms, flow.draft_only, draft.value as draft, flow.tag FROM flow
"SELECT flow.path, flow.summary, flow,description, flow.schema, flow.value, flow.extra_perms, flow.draft_only, flow.ws_error_handler_muted, draft.value as draft, flow.tag FROM flow
LEFT JOIN draft ON
flow.path = draft.path AND draft.workspace_id = $2 AND draft.typ = 'flow'
WHERE flow.path = $1 AND flow.workspace_id = $2",
@@ -794,6 +876,7 @@ mod tests {
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
};
let expect = serde_json::json!({
"modules": [
+1 -1
View File
@@ -316,7 +316,7 @@ pub async fn get_path_tag_limits_cache_for_hash(
Option<bool>,
)> {
let script = sqlx::query!(
"select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
hash,
w_id
)
+70 -5
View File
@@ -77,6 +77,7 @@ pub struct ScriptWDraft {
pub cache_ttl: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dedicated_worker: Option<bool>,
pub ws_error_handler_muted: Option<bool>,
}
pub fn global_service() -> Router {
@@ -113,6 +114,10 @@ pub fn workspaced_service() -> Router {
.route("/raw/h/:hash", get(raw_script_by_hash))
.route("/deployment_status/h/:hash", get(get_deployment_status))
.route("/list_paths", get(list_paths))
.route(
"/toggle_workspace_error_handler/p/*path",
post(toggle_workspace_error_handler),
)
}
#[derive(Serialize, FromRow)]
@@ -168,7 +173,8 @@ async fn list_scripts(
"favorite.path IS NOT NULL as starred",
"tag",
"draft.path IS NOT NULL as has_draft",
"draft_only"
"draft_only",
"ws_error_handler_muted"
])
.left()
.join("favorite")
@@ -304,6 +310,14 @@ async fn create_script(
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
) -> Result<(StatusCode, String)> {
#[cfg(not(feature = "enterprise"))]
if ns.ws_error_handler_muted.is_some_and(|val| val) {
return Err(Error::BadRequest(
"Muting the error handler for certain script is only available in enterprise version"
.to_string(),
));
}
let hash = ScriptHash(hash_script(&ns));
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
@@ -456,8 +470,8 @@ async fn create_script(
sqlx::query!(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)",
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
&w_id,
&hash.0,
ns.path,
@@ -478,7 +492,8 @@ async fn create_script(
ns.concurrent_limit,
ns.concurrency_time_window_s,
ns.cache_ttl,
ns.dedicated_worker
ns.dedicated_worker,
ns.ws_error_handler_muted.unwrap_or(false),
)
.execute(&mut tx)
.await?;
@@ -675,7 +690,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, draft.value as draft, dedicated_worker FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \
@@ -709,6 +724,56 @@ async fn list_paths(
Ok(Json(scripts))
}
#[derive(Deserialize)]
pub struct ToggleWorkspaceErrorHandler {
pub muted: Option<bool>,
}
async fn toggle_workspace_error_handler(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(req): Json<ToggleWorkspaceErrorHandler>,
) -> Result<String> {
#[cfg(not(feature = "enterprise"))]
if true {
return Err(Error::BadRequest(
"Muting the error handler for certain script is only available in enterprise version"
.to_string(),
));
}
let mut tx = user_db.begin(&authed).await?;
let error_handler_maybe: Option<String> = sqlx::query_scalar!(
"SELECT error_handler FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.fetch_optional(&mut *tx)
.await?
.unwrap_or(None);
match error_handler_maybe {
Some(_) => {
sqlx::query_scalar!(
"UPDATE script SET ws_error_handler_muted = $3 WHERE workspace_id = $2 AND path = $1 AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2)",
path.to_path(),
w_id,
req.muted,
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok("".to_string())
}
None => {
tx.commit().await?;
Err(Error::ExecutionErr(
"Workspace error handler needs to be defined".to_string(),
))
}
}
}
async fn get_tokened_raw_script_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, token, path)): Path<(String, String, StripPath)>,
+4
View File
@@ -37,6 +37,7 @@ pub struct Flow {
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
pub tag: Option<String>,
pub ws_error_handler_muted: Option<bool>,
}
#[derive(Serialize)]
@@ -54,6 +55,7 @@ pub struct ListableFlow {
pub has_draft: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
pub ws_error_handler_muted: Option<bool>,
}
#[derive(Deserialize)]
@@ -86,6 +88,8 @@ pub struct FlowValue {
pub skip_expr: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_ttl: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ws_error_handler_muted: Option<bool>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
+1 -1
View File
@@ -281,7 +281,7 @@ pub async fn script_hash_to_tag_and_limits<'c>(
Option<bool>,
)> {
let script = sqlx::query!(
"select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
script_hash.0,
w_id
)
+1 -1
View File
@@ -222,7 +222,7 @@ pub async fn get_latest_hash_for_path<'c>(
Option<bool>,
)> {
let r_o = sqlx::query!(
"select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND
"select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND archived = false)",
script_path,
+3
View File
@@ -162,6 +162,7 @@ pub struct Script {
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_time_window_s: Option<i32>,
pub dedicated_worker: Option<bool>,
pub ws_error_handler_muted: Option<bool>,
}
#[derive(Serialize)]
@@ -181,6 +182,7 @@ pub struct ListableScript {
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
pub has_deploy_errors: bool,
pub ws_error_handler_muted: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -217,6 +219,7 @@ pub struct NewScript {
pub concurrency_time_window_s: Option<i32>,
pub cache_ttl: Option<i32>,
pub dedicated_worker: Option<bool>,
pub ws_error_handler_muted: Option<bool>,
}
#[derive(Deserialize)]
+37 -11
View File
@@ -591,16 +591,40 @@ pub async fn send_error_to_workspace_handler<
.ok_or_else(|| Error::InternalErr(format!("no workspace settings for id {w_id}")))?;
if let Some(error_handler) = error_handler {
run_error_handler(
rsmq,
queued_job,
db,
result,
&error_handler.strip_prefix("script/").unwrap(),
error_handler_extra_args,
false,
)
.await?
let ws_error_handler_muted: Option<bool> = match queued_job.job_kind {
JobKind::Script => {
sqlx::query_scalar!(
"SELECT ws_error_handler_muted FROM script WHERE workspace_id = $1 AND hash = $2",
queued_job.workspace_id,
queued_job.script_hash.unwrap().0,
)
.fetch_optional(db)
.await?
}
JobKind::Flow => {
sqlx::query_scalar!(
"SELECT ws_error_handler_muted FROM flow WHERE workspace_id = $1 AND path = $2",
queued_job.workspace_id,
queued_job.script_path.as_ref().unwrap(),
)
.fetch_optional(db)
.await?
}
_ => None,
};
if !ws_error_handler_muted.unwrap_or(false) {
run_error_handler(
rsmq,
queued_job,
db,
result,
&error_handler.strip_prefix("script/").unwrap(),
error_handler_extra_args,
false,
)
.await?
}
}
Ok(())
@@ -2066,7 +2090,9 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
"INSERT INTO queue
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl)
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, \
visible_to_owner, root_job, tag, concurrent_limit, concurrency_time_window_s, timeout, \
flow_step_id, cache_ttl)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29) \
RETURNING id",
workspace_id,
+35 -11
View File
@@ -650,17 +650,36 @@ pub async fn update_flow_status_after_job_completion_internal<
save_in_cache(db, &flow_job, cached_res_path, &nresult).await;
}
add_completed_job(
db,
&flow_job,
success && !is_failure_step && !skip_error_handler,
stop_early && skip_if_stop_early,
Json(&nresult),
logs,
0,
rsmq.clone(),
)
.await?;
let success = success && !is_failure_step && !skip_error_handler;
if success {
add_completed_job(
db,
&flow_job,
success,
stop_early && skip_if_stop_early,
Json(&nresult),
logs,
0,
rsmq.clone(),
)
.await?;
} else {
add_completed_job(
db,
&flow_job,
success,
stop_early && skip_if_stop_early,
Json(
&serde_json::from_str::<Value>(nresult.get()).unwrap_or_else(
|e| json!({"error": format!("Impossible to serialize error: {e}")}),
),
),
logs,
0,
rsmq.clone(),
)
.await?;
}
}
true
} else {
@@ -2079,6 +2098,7 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
},
path: inner_path,
},
@@ -2136,6 +2156,7 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
},
path: Some(format!("{}/forloop", flow_job.script_path())),
},
@@ -2227,6 +2248,7 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
},
path: Some(format!(
"{}/branchone-{}",
@@ -2271,6 +2293,7 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
},
path: Some(format!(
"{}/branchall-{}",
@@ -2332,6 +2355,7 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
},
path: Some(format!(
"{}/branchall-{}",
@@ -113,7 +113,8 @@
value: flow.value,
schema: flow.schema,
tag: flow.tag,
draft_only: true
draft_only: true,
ws_error_handler_muted: flow.ws_error_handler_muted
}
})
}
@@ -159,7 +160,8 @@
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
schema: flow.schema,
ws_error_handler_muted: flow.ws_error_handler_muted
}
})
if (enabled) {
@@ -176,7 +178,8 @@
description: flow.description ?? '',
value: flow.value,
schema: flow.schema,
tag: flow.tag
tag: flow.tag,
ws_error_handler_muted: flow.ws_error_handler_muted
}
})
const scheduleExists = await ScheduleService.existsSchedule({
@@ -17,6 +17,7 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import ErrorHandlerToggleButton from '$lib/components/details/ErrorHandlerToggleButton.svelte'
import {
Bug,
CheckCircle,
@@ -190,7 +191,8 @@
dedicated_worker: script.dedicated_worker,
concurrent_limit: script.concurrent_limit,
concurrency_time_window_s: script.concurrency_time_window_s,
cache_ttl: script.cache_ttl
cache_ttl: script.cache_ttl,
ws_error_handler_muted: script.ws_error_handler_muted
}
})
history.replaceState(history.state, '', `/scripts/edit/${script.path}`)
@@ -233,7 +235,8 @@
envs: script.envs,
concurrent_limit: script.concurrent_limit,
concurrency_time_window_s: script.concurrency_time_window_s,
cache_ttl: script.cache_ttl
cache_ttl: script.cache_ttl,
ws_error_handler_muted: script.ws_error_handler_muted
}
})
}
@@ -318,6 +321,16 @@
<TabContent value="metadata">
<div class="flex flex-col gap-8">
<Section label="Metadata">
<svelte:fragment slot="action">
<div class="flex flex-row items-center gap-2">
<ErrorHandlerToggleButton
kind="script"
scriptOrFlowPath={script.path}
bind:errorHandlerMuted={script.ws_error_handler_muted}
iconOnly={false}
/>
</div>
</svelte:fragment>
<div class="flex flex-col gap-4">
<Label label="Summary">
<input
@@ -34,6 +34,7 @@
export let moveDrawer: MoveDrawer
export let deleteConfirmedCallback: (() => void) | undefined
export let deploymentDrawer: DeployWorkspaceDrawer
export let errorHandlerMuted: boolean
let { summary, path, extra_perms, canWrite, workspace_id, archived, draft_only, has_draft } = flow
@@ -74,6 +75,7 @@
{path}
{summary}
{starred}
{errorHandlerMuted}
on:change
canFavorite={!draft_only}
>
@@ -2,10 +2,12 @@
import Star from '$lib/components/Star.svelte'
import { createEventDispatcher } from 'svelte'
import RowIcon from './RowIcon.svelte'
import { BellOff } from 'lucide-svelte'
export let marked: string | undefined
export let starred: boolean
export let canFavorite: boolean = true
export let errorHandlerMuted: boolean = false
const dispatch = createEventDispatcher()
@@ -37,6 +39,11 @@ first-of-type:rounded-t-md last-of-type:rounded-b-md [*:not(:last-child)]:border
</div>
</div>
</a>
{#if errorHandlerMuted}
<BellOff class="w-8 opacity-60" size={12} fill="currentcolor" />
{/if}
{#if $$slots.badges}
<div class="hidden lg:flex flex-row gap-4 items-center">
<slot name="badges" />
@@ -36,6 +36,7 @@
export let moveDrawer: MoveDrawer
export let deploymentDrawer: DeployWorkspaceDrawer
export let deleteConfirmedCallback: (() => void) | undefined
export let errorHandlerMuted: boolean
let {
summary,
@@ -91,6 +92,7 @@
{path}
{summary}
{starred}
{errorHandlerMuted}
workspaceId={$workspaceStore ?? ''}
on:change
canFavorite={!draft_only}
@@ -4,6 +4,7 @@
import Menu from '$lib/components/details/Menu.svelte'
import MenuItem from '$lib/components/common/menu/MenuItem.svelte'
import { classNames } from '$lib/utils'
import ErrorHandlerToggleButton from './ErrorHandlerToggleButton.svelte'
type MainButton = {
label: string
@@ -22,6 +23,10 @@
export let mainButtons: MainButton[] = []
export let menuItems: MenuItemButton[] = []
export let title: string
export let errorHandlerKind: 'flow' | 'script'
export let scriptOrFlowPath: string
export let errorHandlerMuted: boolean | undefined
</script>
<div class="border-b p-2 shadow-md">
@@ -58,6 +63,11 @@
</svelte:fragment>
</Menu>
{/if}
<ErrorHandlerToggleButton
kind={errorHandlerKind}
{scriptOrFlowPath}
bind:errorHandlerMuted
/>
{#each mainButtons as btn}
<Button
{...btn.buttonProps}
@@ -0,0 +1,73 @@
<script lang="ts">
import { Bell, BellOff } from 'lucide-svelte'
import { Button } from '$lib/components/common'
import { FlowService, ScriptService } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import { workspaceStore } from '$lib/stores'
export let kind: 'script' | 'flow'
export let scriptOrFlowPath: string
export let errorHandlerMuted: boolean | undefined
export let iconOnly: boolean = true
async function toggleErrorHandler(): Promise<void> {
if ($workspaceStore !== undefined) {
try {
if (kind === 'flow') {
await FlowService.toggleWorkspaceErrorHandlerForFlow({
workspace: $workspaceStore,
path: scriptOrFlowPath,
requestBody: {
muted: !errorHandlerMuted
}
})
} else {
await ScriptService.toggleWorkspaceErrorHandlerForScript({
workspace: $workspaceStore,
path: scriptOrFlowPath,
requestBody: {
muted: !errorHandlerMuted
}
})
}
} catch (error) {
sendUserToast(
`Error while toggling Workspace Error Handler: ${error.body || error.message}`,
true
)
return
}
errorHandlerMuted = !errorHandlerMuted
sendUserToast(
errorHandlerMuted ? 'Workspace error handler muted' : 'Workspace error handler active',
false
)
}
}
</script>
<Button
title={errorHandlerMuted === undefined || !errorHandlerMuted
? 'Disable workspace error handler for this script'
: 'Enable workspace error handler for this script'}
size="xs"
on:click={toggleErrorHandler}
color="light"
>
{#if errorHandlerMuted === undefined || !errorHandlerMuted}
<div class="flex flex-row items-center">
{#if !iconOnly}
Mute
{/if}
<Bell class="w-4" size={12} fill="currentcolor" />
</div>
{:else}
<div class="flex flex-row items-center">
{#if !iconOnly}
Unmute
{/if}
<BellOff class="w-4" size={12} fill="currentcolor" />
</div>
{/if}
</Button>
@@ -25,6 +25,7 @@
import type { Schema } from '$lib/common'
import Section from '$lib/components/Section.svelte'
import Label from '$lib/components/Label.svelte'
import ErrorHandlerToggleButton from '$lib/components/details/ErrorHandlerToggleButton.svelte'
const { selectedId, flowStore, initialPath, previewArgs } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -111,6 +112,16 @@
rows="3"
/>
</Label>
<div class="flex flex-row items-center gap-1">
<ErrorHandlerToggleButton
kind="flow"
scriptOrFlowPath={$flowStore.path}
bind:errorHandlerMuted={$flowStore.ws_error_handler_muted}
iconOnly={false}
/>
</div>
<Slider text="How to trigger flows?">
<div class="text-sm text-tertiary border p-4 mb-20">
On-demand:
@@ -36,6 +36,7 @@
schema?: any
kind: 'script' | 'failure' | 'trigger' | 'command' | 'approval' | undefined
envs?: string[]
ws_error_handler_muted?: boolean
}
| undefined = undefined
@@ -64,7 +65,8 @@
is_template: false,
language: script.language,
kind: script.kind as Script.kind | undefined,
envs: script.envs
envs: script.envs,
ws_error_handler_muted: script.ws_error_handler_muted
}
})
callback?.()
@@ -399,6 +399,10 @@
marked={item.marked}
on:change={loadScripts}
script={item}
errorHandlerMuted={item.ws_error_handler_muted === undefined ||
item.ws_error_handler_muted === null
? false
: item.ws_error_handler_muted}
{shareModal}
{moveDrawer}
{deploymentDrawer}
@@ -410,6 +414,10 @@
marked={item.marked}
on:change={loadFlows}
flow={item}
errorHandlerMuted={item.ws_error_handler_muted === undefined ||
item.ws_error_handler_muted === null
? false
: item.ws_error_handler_muted}
{shareModal}
{moveDrawer}
{deploymentDrawer}
@@ -252,6 +252,9 @@
{mainButtons}
menuItems={getMenuItems(flow)}
title={defaultIfEmptyString(flow.summary, flow.path)}
bind:errorHandlerMuted={flow.ws_error_handler_muted}
scriptOrFlowPath={flow.path}
errorHandlerKind="flow"
/>
</svelte:fragment>
<svelte:fragment slot="form">
@@ -411,6 +411,9 @@
{mainButtons}
menuItems={getMenuItems(script)}
title={defaultIfEmptyString(script.summary, script.path)}
bind:errorHandlerMuted={script.ws_error_handler_muted}
errorHandlerKind="script"
scriptOrFlowPath={script.path}
>
{#if script?.concurrent_limit != undefined && script.concurrency_time_window_s != undefined}
<div class="hidden md:block">