mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
fix: make the native trigger disable/enable toggle actually save (#11024)
* feat: let a native trigger be disabled without deleting it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: show and control the native trigger pause outside the flow editor Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: create a native trigger already paused instead of pausing it after Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: correct the native trigger enabled comments for create-time init Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
785277e0bb
commit
448fce93f7
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_hash,\n service_config,\n error,\n created_at,\n updated_at,\n summary\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND script_path = $3\n AND is_flow = $4\n LIMIT 1\n ",
|
||||
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_hash,\n service_config,\n error,\n created_at,\n updated_at,\n summary,\n enabled\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND script_path = $3\n AND is_flow = $4\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -68,6 +68,11 @@
|
||||
"ordinal": 10,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -100,8 +105,9 @@
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6dafcc89668fb0e5740f23264b515b1724c36031da39d53ae6c329a479bdf8aa"
|
||||
"hash": "0987f164fe3d64bf0a6a4e9699c2d1f339670da3ed1c08203d54d45798d022dd"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO native_trigger (\n external_id,\n workspace_id,\n service_name,\n script_path,\n is_flow,\n webhook_token_hash,\n service_config,\n summary\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n ON CONFLICT (external_id, workspace_id, service_name)\n DO UPDATE SET script_path = $4, is_flow = $5, webhook_token_hash = $6, service_config = $7, summary = $8, error = NULL, updated_at = NOW()\n ",
|
||||
"query": "\n INSERT INTO native_trigger (\n external_id,\n workspace_id,\n service_name,\n script_path,\n is_flow,\n webhook_token_hash,\n service_config,\n summary,\n enabled\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9\n )\n ON CONFLICT (external_id, workspace_id, service_name)\n DO UPDATE SET script_path = $4, is_flow = $5, webhook_token_hash = $6, service_config = $7, summary = $8, error = NULL, updated_at = NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -23,10 +23,11 @@
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Varchar"
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1048d1c95270ce1f36c02bce31a2bc8a88935c613bd213b7156299811377db8e"
|
||||
"hash": "64dce306efc0d9989542dba5bf003dc94b9337a8d3b69805308258d1fa145e63"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n nt.external_id,\n nt.workspace_id,\n nt.service_name AS \"service_name!: ServiceName\",\n nt.script_path,\n nt.is_flow,\n nt.webhook_token_hash,\n nt.service_config,\n nt.error,\n nt.created_at,\n nt.updated_at,\n nt.summary\n FROM\n native_trigger nt\n WHERE\n nt.workspace_id = $1 AND\n nt.service_name = $2 AND\n ($5::text IS NULL OR nt.script_path = $5) AND\n ($6::bool IS NULL OR nt.is_flow = $6) AND\n (\n (nt.is_flow = false AND EXISTS (\n SELECT 1 FROM script s\n WHERE s.workspace_id = nt.workspace_id\n AND s.path = nt.script_path\n ))\n OR\n (nt.is_flow = true AND EXISTS (\n SELECT 1 FROM flow f\n WHERE f.workspace_id = nt.workspace_id\n AND f.path = nt.script_path\n ))\n )\n LIMIT $3\n OFFSET $4\n ",
|
||||
"query": "\n SELECT\n nt.external_id,\n nt.workspace_id,\n nt.service_name AS \"service_name!: ServiceName\",\n nt.script_path,\n nt.is_flow,\n nt.webhook_token_hash,\n nt.service_config,\n nt.error,\n nt.created_at,\n nt.updated_at,\n nt.summary,\n nt.enabled\n FROM\n native_trigger nt\n WHERE\n nt.workspace_id = $1 AND\n nt.service_name = $2 AND\n ($5::text IS NULL OR nt.script_path = $5) AND\n ($6::bool IS NULL OR nt.is_flow = $6) AND\n (\n (nt.is_flow = false AND EXISTS (\n SELECT 1 FROM script s\n WHERE s.workspace_id = nt.workspace_id\n AND s.path = nt.script_path\n ))\n OR\n (nt.is_flow = true AND EXISTS (\n SELECT 1 FROM flow f\n WHERE f.workspace_id = nt.workspace_id\n AND f.path = nt.script_path\n ))\n )\n LIMIT $3\n OFFSET $4\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -68,6 +68,11 @@
|
||||
"ordinal": 10,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -102,8 +107,9 @@
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b0775af41a9b54cce040bf37cae770e63dab12a1383a0855d105c061e4e4ca48"
|
||||
"hash": "a2122e8520268919e2ddc85ef46b5f9322229bb2a1e104b6eabaf2a697c2776a"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT enabled\n FROM native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND external_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a3871035319012d72679f132696146d5275cdd4313961c76223ed0f3fec7dca3"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_hash,\n service_config,\n error,\n created_at,\n updated_at,\n summary\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND external_id = $3\n ",
|
||||
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_hash,\n service_config,\n error,\n created_at,\n updated_at,\n summary,\n enabled\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND external_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -68,6 +68,11 @@
|
||||
"ordinal": 10,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -99,8 +104,9 @@
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "15014ce696cf2af4f719a537a4e3ca5b322cc130a35a91f8b8854f5ebdf25ad2"
|
||||
"hash": "c7766afaea3e187824698cae2b090af9f49ded98cd1824f1ac91cc5dbe709bc1"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE native_trigger\n SET enabled = $1\n WHERE\n workspace_id = $2\n AND service_name = $3\n AND external_id = $4\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fa42f89d9494056e1e7ad904b844203f1132aa5ae5eff9194c45e0259f4bee76"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE native_trigger DROP COLUMN IF EXISTS enabled;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE native_trigger ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -149,7 +149,7 @@ mcp_oauth_server_code: code(char), client_id(char), user_email(char), workspace_
|
||||
FK: (client_id) -> mcp_oauth_server_client(client_id)
|
||||
metrics: id(char), value(jsonb), created_at(ts)
|
||||
mqtt_trigger: mqtt_resource_path(char), subscribe_topics(jsonb[]), client_version(mqtt_client_version), v5_config(jsonb), v3_config(jsonb), client_id(char), path(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), server_id(char), last_server_ping(ts), error(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), labels(text[])
|
||||
native_trigger: external_id(char), workspace_id(char), service_name(native_trigger_service), script_path(char), is_flow(bool), webhook_token_hash(char), service_config(jsonb), error(text), created_at(ts), updated_at(ts)
|
||||
native_trigger: external_id(char), workspace_id(char), service_name(native_trigger_service), script_path(char), is_flow(bool), webhook_token_hash(char), service_config(jsonb), error(text), created_at(ts), updated_at(ts), enabled(bool)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
nats_trigger: path(char), nats_resource_path(char), subjects(char), stream_name(char), consumer_name(char), use_jetstream(bool), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), server_id(char), last_server_ping(ts), error(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), labels(text[])
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
|
||||
@@ -18,15 +18,16 @@ use windmill_common::{
|
||||
variables::{build_crypt, encrypt},
|
||||
};
|
||||
use windmill_native_triggers::{
|
||||
classify_read_failure, decrypt_oauth_data, delete_native_trigger,
|
||||
delete_workspace_integration, get_workspace_integration,
|
||||
classify_read_failure, decrypt_oauth_data, delete_native_trigger, delete_workspace_integration,
|
||||
get_workspace_integration,
|
||||
github::GitHub,
|
||||
google::{parse_stop_channel_params, should_renew_channel},
|
||||
http_error_status, list_native_triggers, map_external_error,
|
||||
grant_refused, http_error_status, list_native_triggers, map_external_error,
|
||||
native_trigger_is_enabled,
|
||||
nextcloud::NextCloud,
|
||||
grant_refused, require_native_integration_use, store_native_trigger,
|
||||
store_workspace_integration, External, ExternalReadFailure, HttpRequestError,
|
||||
NativeTriggerConfig, OAuthConfig, ServiceName,
|
||||
require_native_integration_use, set_native_trigger_enabled, store_native_trigger,
|
||||
store_workspace_integration, update_native_trigger, External, ExternalReadFailure,
|
||||
HttpRequestError, NativeTriggerConfig, OAuthConfig, ServiceName,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -456,6 +457,7 @@ async fn test_delete_integration_full_cascade(db: Pool<Postgres>) -> anyhow::Res
|
||||
&trigger_config,
|
||||
json!({"triggerType": "drive"}),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -546,6 +548,7 @@ async fn test_cleanup_preserves_triggers(db: Pool<Postgres>) -> anyhow::Result<(
|
||||
&trigger_config,
|
||||
json!({"triggerType": "drive"}),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -603,6 +606,7 @@ async fn test_rename_moves_native_trigger(db: Pool<Postgres>) -> anyhow::Result<
|
||||
},
|
||||
json!({"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent"}),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
// An unrelated trigger already sitting on the target path must not be reported as moved.
|
||||
@@ -619,6 +623,7 @@ async fn test_rename_moves_native_trigger(db: Pool<Postgres>) -> anyhow::Result<
|
||||
},
|
||||
json!({"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent"}),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -784,7 +789,10 @@ fn test_refresh_failures_blame_only_the_grant_they_refuse() {
|
||||
let ok = Some(StatusCode::OK);
|
||||
assert!(grant_refused(ok, r#"{"error":"bad_refresh_token"}"#));
|
||||
assert!(grant_refused(ok, r#"{"error":"invalid_grant"}"#));
|
||||
assert!(!grant_refused(ok, r#"{"access_token":"t","token_type":"bearer"}"#));
|
||||
assert!(!grant_refused(
|
||||
ok,
|
||||
r#"{"access_token":"t","token_type":"bearer"}"#
|
||||
));
|
||||
}
|
||||
|
||||
/// A service that is busy or broken has not refused anything, and callers react differently to
|
||||
@@ -826,7 +834,9 @@ fn test_transient_service_failures_are_not_refusals() {
|
||||
body: r#"{"message":"Must have admin rights to Repository."}"#.to_string(),
|
||||
});
|
||||
assert!(
|
||||
map_external_error(refused).to_string().contains("admin rights"),
|
||||
map_external_error(refused)
|
||||
.to_string()
|
||||
.contains("admin rights"),
|
||||
"a real 403 keeps its guidance"
|
||||
);
|
||||
}
|
||||
@@ -850,3 +860,115 @@ fn test_only_service_failures_degrade_the_read() {
|
||||
"a non-provider error must pass through unmapped"
|
||||
);
|
||||
}
|
||||
|
||||
/// The pause switch a webhook delivery is gated on. A trigger arrives enabled, survives an
|
||||
/// unrelated edit, and an unknown one reads as enabled so a delivery Windmill cannot place is
|
||||
/// never silently dropped.
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_native_trigger_enabled_toggle(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
insert_test_script(&db, "f/test/handler").await?;
|
||||
let config = NativeTriggerConfig {
|
||||
script_path: "f/test/handler".to_string(),
|
||||
is_flow: false,
|
||||
webhook_token: "abcdefghij1234567890".to_string(),
|
||||
};
|
||||
store_native_trigger(
|
||||
&db,
|
||||
"test-workspace",
|
||||
ServiceName::Nextcloud,
|
||||
"ext-1",
|
||||
&config,
|
||||
json!({"event": "OCA\\Files\\Event\\LoadAdditionalScriptsEvent"}),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
native_trigger_is_enabled(&db, "test-workspace", ServiceName::Nextcloud, "ext-1").await?,
|
||||
"a new trigger fires"
|
||||
);
|
||||
|
||||
assert!(
|
||||
set_native_trigger_enabled(
|
||||
&db,
|
||||
"test-workspace",
|
||||
ServiceName::Nextcloud,
|
||||
"ext-1",
|
||||
false
|
||||
)
|
||||
.await?
|
||||
);
|
||||
assert!(
|
||||
!native_trigger_is_enabled(&db, "test-workspace", ServiceName::Nextcloud, "ext-1").await?
|
||||
);
|
||||
|
||||
// Saving a configuration must not resume a trigger someone paused.
|
||||
update_native_trigger(
|
||||
&db,
|
||||
"test-workspace",
|
||||
ServiceName::Nextcloud,
|
||||
"ext-1",
|
||||
&config,
|
||||
None,
|
||||
Some("edited"),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!native_trigger_is_enabled(&db, "test-workspace", ServiceName::Nextcloud, "ext-1").await?,
|
||||
"an edit leaves the pause in place"
|
||||
);
|
||||
|
||||
// A recreate registers a fresh trigger and must be able to come up already paused, in one
|
||||
// write, rather than being enabled for as long as it takes a second call to arrive.
|
||||
store_native_trigger(
|
||||
&db,
|
||||
"test-workspace",
|
||||
ServiceName::Nextcloud,
|
||||
"ext-2",
|
||||
&config,
|
||||
json!({}),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!native_trigger_is_enabled(&db, "test-workspace", ServiceName::Nextcloud, "ext-2").await?
|
||||
);
|
||||
|
||||
// The conflict branch is a re-registration of a trigger that already exists, so it carries no
|
||||
// opinion about the pause.
|
||||
store_native_trigger(
|
||||
&db,
|
||||
"test-workspace",
|
||||
ServiceName::Nextcloud,
|
||||
"ext-2",
|
||||
&config,
|
||||
json!({}),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!native_trigger_is_enabled(&db, "test-workspace", ServiceName::Nextcloud, "ext-2").await?,
|
||||
"re-registering leaves the pause in place"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!set_native_trigger_enabled(
|
||||
&db,
|
||||
"test-workspace",
|
||||
ServiceName::Nextcloud,
|
||||
"unknown",
|
||||
true
|
||||
)
|
||||
.await?,
|
||||
"nothing to toggle"
|
||||
);
|
||||
assert!(
|
||||
native_trigger_is_enabled(&db, "test-workspace", ServiceName::Nextcloud, "unknown").await?,
|
||||
"a trigger Windmill has no row for is not treated as paused"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18952,6 +18952,49 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/native_triggers/{service_name}/setenabled/{external_id}:
|
||||
post:
|
||||
summary: set enabled state of native trigger
|
||||
description: |
|
||||
Enables or disables a native trigger. A disabled trigger stays registered on the
|
||||
external service but starts no job when it fires.
|
||||
Requires write access to the script or flow that the trigger is associated with.
|
||||
operationId: setNativeTriggerEnabled
|
||||
tags:
|
||||
- native_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: service_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/NativeServiceName"
|
||||
- name: external_id
|
||||
in: path
|
||||
required: true
|
||||
description: The external ID of the trigger from the external service
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: updated enabled state
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- enabled
|
||||
responses:
|
||||
"200":
|
||||
description: native trigger enabled state updated
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/native_triggers/{service_name}/list:
|
||||
get:
|
||||
summary: list native triggers
|
||||
@@ -35848,6 +35891,9 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Short summary to be displayed when listed
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether the trigger starts a job when it fires
|
||||
required:
|
||||
- external_id
|
||||
- workspace_id
|
||||
@@ -35855,6 +35901,7 @@ components:
|
||||
- script_path
|
||||
- is_flow
|
||||
- service_config
|
||||
- enabled
|
||||
|
||||
NativeTriggerWithExternal:
|
||||
type: object
|
||||
@@ -35886,6 +35933,9 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Short summary to be displayed when listed
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether the trigger starts a job when it fires
|
||||
external_data:
|
||||
type: object
|
||||
nullable: true
|
||||
@@ -35906,6 +35956,7 @@ components:
|
||||
- script_path
|
||||
- is_flow
|
||||
- service_config
|
||||
- enabled
|
||||
- external_data
|
||||
|
||||
WorkspaceIntegrations:
|
||||
@@ -35991,6 +36042,12 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Short summary to be displayed when listed
|
||||
enabled:
|
||||
type: boolean
|
||||
description: >-
|
||||
Whether the trigger starts a job when it fires. Honoured on create only, so a
|
||||
trigger can be registered already paused; an update ignores it and setenabled is
|
||||
the only way to change an existing trigger's state. Defaults to true.
|
||||
required:
|
||||
- script_path
|
||||
- is_flow
|
||||
|
||||
@@ -6511,7 +6511,7 @@ pub async fn run_flow_by_path(
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
args: RawWebhookArgs,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
let (args, trigger_metadata) = get_args_and_trigger_metadata(
|
||||
let (args, trigger_metadata) = match get_args_and_trigger_metadata(
|
||||
&db,
|
||||
&authed,
|
||||
RunnableId::from_flow_path(flow_path.to_path()),
|
||||
@@ -6519,7 +6519,15 @@ pub async fn run_flow_by_path(
|
||||
&w_id,
|
||||
args,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
{
|
||||
WebhookRun::Run(args, trigger_metadata) => (args, trigger_metadata),
|
||||
// 200 rather than an error: services drop a webhook that keeps failing, and disabling a
|
||||
// trigger in Windmill must not cost it its registration.
|
||||
WebhookRun::TriggerDisabled => {
|
||||
return Ok((StatusCode::OK, NATIVE_TRIGGER_DISABLED_MSG.to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let (uuid, _, _, _) = push_flow_job_by_path_into_queue(
|
||||
authed,
|
||||
@@ -6944,7 +6952,7 @@ pub async fn run_script_by_path(
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
args: RawWebhookArgs,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
let (args, trigger_metadata) = get_args_and_trigger_metadata(
|
||||
let (args, trigger_metadata) = match get_args_and_trigger_metadata(
|
||||
&db,
|
||||
&authed,
|
||||
RunnableId::from_script_path(script_path.to_path()),
|
||||
@@ -6952,7 +6960,13 @@ pub async fn run_script_by_path(
|
||||
&w_id,
|
||||
args,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
{
|
||||
WebhookRun::Run(args, trigger_metadata) => (args, trigger_metadata),
|
||||
WebhookRun::TriggerDisabled => {
|
||||
return Ok((StatusCode::OK, NATIVE_TRIGGER_DISABLED_MSG.to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let (uuid, _, _) = push_script_job_by_path_into_queue(
|
||||
authed,
|
||||
@@ -6970,6 +6984,16 @@ pub async fn run_script_by_path(
|
||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||
}
|
||||
|
||||
/// What a webhook delivery resolved to: the arguments to run with, or nothing to run.
|
||||
pub enum WebhookRun {
|
||||
Run(PushArgsOwned, Option<TriggerMetadata>),
|
||||
/// The native trigger this delivery belongs to is disabled.
|
||||
TriggerDisabled,
|
||||
}
|
||||
|
||||
const NATIVE_TRIGGER_DISABLED_MSG: &str =
|
||||
"This trigger is disabled in Windmill, so no job was created";
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn get_args_and_trigger_metadata(
|
||||
db: &DB,
|
||||
@@ -6978,14 +7002,21 @@ pub async fn get_args_and_trigger_metadata(
|
||||
run_query: &RunJobQuery,
|
||||
w_id: &str,
|
||||
args: RawWebhookArgs,
|
||||
) -> error::Result<(PushArgsOwned, Option<TriggerMetadata>)> {
|
||||
) -> error::Result<WebhookRun> {
|
||||
use windmill_common::triggers::TriggerMetadata;
|
||||
|
||||
// Build trigger metadata if this is a native trigger request
|
||||
#[cfg(feature = "native_trigger")]
|
||||
let (trigger_metadata, native_args) = if let Some(service_name_str) = &run_query.service_name {
|
||||
use crate::native_triggers::{prepare_native_trigger_args, ServiceName};
|
||||
use crate::native_triggers::{
|
||||
native_trigger_is_enabled, prepare_native_trigger_args, ServiceName,
|
||||
};
|
||||
let service_name = ServiceName::try_from(service_name_str.to_owned())?;
|
||||
if let Some(external_id) = run_query.trigger_external_id.as_deref() {
|
||||
if !native_trigger_is_enabled(db, w_id, service_name, external_id).await? {
|
||||
return Ok(WebhookRun::TriggerDisabled);
|
||||
}
|
||||
}
|
||||
let metadata = Some(TriggerMetadata::new(
|
||||
run_query.trigger_external_id.clone(),
|
||||
service_name.as_job_trigger_kind(),
|
||||
@@ -7021,7 +7052,7 @@ pub async fn get_args_and_trigger_metadata(
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok((args, trigger_metadata))
|
||||
Ok(WebhookRun::Run(args, trigger_metadata))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -1434,9 +1434,11 @@ pub(crate) async fn tarball_workspace(
|
||||
// Native triggers (Nextcloud, Google Drive, GitHub) are never
|
||||
// cloned into a fork — a fork only has one if its owner created
|
||||
// it there, so it's always "fork-only" and keeps its own mode.
|
||||
// No parent-value substitution applies; we only strip the
|
||||
// webhook token hash.
|
||||
let native_ignore_keys = vec!["webhook_token_hash"];
|
||||
// No parent-value substitution applies; we strip the webhook
|
||||
// token hash, and `enabled`, which is operational state a sync
|
||||
// deliberately does not carry — whether a trigger is paused
|
||||
// belongs to the workspace it runs in, not to the code.
|
||||
let native_ignore_keys = vec!["webhook_token_hash", "enabled"];
|
||||
|
||||
for trigger in native_triggers {
|
||||
let trigger_str = &to_string_without_metadata(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
classify_read_failure, decrypt_oauth_data, delete_native_trigger, delete_token_by_hash,
|
||||
get_native_trigger, list_native_triggers, lock::TriggerLock, map_external_error,
|
||||
map_external_error_with, rotate_webhook_token, store_native_trigger,
|
||||
sync::EXTERNAL_TRIGGER_MISSING_ERROR, update_native_trigger_error,
|
||||
map_external_error_with, rotate_webhook_token, set_native_trigger_enabled,
|
||||
store_native_trigger, sync::EXTERNAL_TRIGGER_MISSING_ERROR, update_native_trigger_error,
|
||||
update_native_trigger_if_runnable_unchanged, webhook_token_label, webhook_token_scopes,
|
||||
External, ExternalReadFailure, NativeTrigger, NativeTriggerConfig, NativeTriggerData,
|
||||
ServiceName,
|
||||
@@ -239,6 +239,7 @@ async fn create_native_trigger<T: External>(
|
||||
&config,
|
||||
service_config,
|
||||
data.summary.as_deref(),
|
||||
data.enabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -603,6 +604,87 @@ async fn delete_native_trigger_handler<T: External>(
|
||||
Ok(format!("Native trigger deleted"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetEnabledPayload {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Pause or resume a trigger, without touching its registration on the external service.
|
||||
///
|
||||
/// Leaving the webhook registered is what makes this reversible: services drop or deactivate a
|
||||
/// subscription that keeps failing, so a paused trigger keeps answering deliveries normally and
|
||||
/// simply starts no job.
|
||||
async fn set_native_trigger_enabled_handler<T: External>(
|
||||
Extension(service_name): Extension<ServiceName>,
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((workspace_id, external_id)): Path<(String, String)>,
|
||||
Json(payload): Json<SetEnabledPayload>,
|
||||
) -> Result<String> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let existing = get_native_trigger(&mut *tx, &workspace_id, service_name, &external_id)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("Native trigger not found: {}", external_id)))?;
|
||||
|
||||
check_scopes(&authed, || {
|
||||
format!("native_triggers:write:{}", &existing.script_path)
|
||||
})?;
|
||||
require_is_writer_on_runnable(
|
||||
&authed,
|
||||
&existing.script_path,
|
||||
existing.is_flow,
|
||||
&workspace_id,
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let updated = set_native_trigger_enabled(
|
||||
&mut *tx,
|
||||
&workspace_id,
|
||||
service_name,
|
||||
&external_id,
|
||||
payload.enabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// The read above takes no row lock, so a concurrent delete can land in between; reporting
|
||||
// success then would tell the caller a trigger that is gone had been paused.
|
||||
if !updated {
|
||||
return Err(Error::NotFound(format!(
|
||||
"Native trigger not found: {}",
|
||||
external_id
|
||||
)));
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
&format!(
|
||||
"native_triggers.{}.{}",
|
||||
service_name,
|
||||
if payload.enabled { "enable" } else { "disable" }
|
||||
),
|
||||
ActionKind::Update,
|
||||
&workspace_id,
|
||||
Some(&external_id),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!(
|
||||
"Native trigger {}",
|
||||
if payload.enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
async fn list_native_triggers_handler<T: External>(
|
||||
Extension(service_name): Extension<ServiceName>,
|
||||
authed: ApiAuthed,
|
||||
@@ -642,6 +724,10 @@ pub fn service_routes<T: External + 'static>(handler: T) -> Router {
|
||||
.route(
|
||||
"/delete/{external_id}",
|
||||
delete(delete_native_trigger_handler::<T>),
|
||||
)
|
||||
.route(
|
||||
"/setenabled/{external_id}",
|
||||
post(set_native_trigger_enabled_handler::<T>),
|
||||
);
|
||||
|
||||
standard_routes
|
||||
|
||||
@@ -226,6 +226,10 @@ pub struct NativeTrigger {
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub summary: Option<String>,
|
||||
/// Whether incoming webhooks for this trigger start a job. Operational state: a create sets
|
||||
/// its initial value and `setenabled` is its only mutator afterwards, so saving a
|
||||
/// configuration can never silently re-enable a trigger someone paused.
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -235,12 +239,20 @@ pub struct NativeTriggerConfig {
|
||||
pub webhook_token: String,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NativeTriggerData<C> {
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub service_config: C,
|
||||
pub summary: Option<String>,
|
||||
/// Honoured on create only, so a trigger can be registered already paused in one request.
|
||||
/// An update ignores it: `setenabled` is the only way to change an existing trigger's state.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
@@ -1179,11 +1191,15 @@ pub async fn store_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>
|
||||
config: &NativeTriggerConfig,
|
||||
service_config: C,
|
||||
summary: Option<&str>,
|
||||
enabled: bool,
|
||||
) -> Result<()> {
|
||||
use windmill_common::auth::hash_token;
|
||||
|
||||
let webhook_token_hash = hash_token(&config.webhook_token);
|
||||
|
||||
// `enabled` is set by the INSERT alone: writing it here rather than in a follow-up statement
|
||||
// is what keeps a trigger created paused from ever being visible, and therefore runnable, in
|
||||
// any other state. The conflict branch leaves it untouched for the mirror-image reason.
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO native_trigger (
|
||||
@@ -1194,9 +1210,10 @@ pub async fn store_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>
|
||||
is_flow,
|
||||
webhook_token_hash,
|
||||
service_config,
|
||||
summary
|
||||
summary,
|
||||
enabled
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9
|
||||
)
|
||||
ON CONFLICT (external_id, workspace_id, service_name)
|
||||
DO UPDATE SET script_path = $4, is_flow = $5, webhook_token_hash = $6, service_config = $7, summary = $8, error = NULL, updated_at = NOW()
|
||||
@@ -1209,6 +1226,7 @@ pub async fn store_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>
|
||||
webhook_token_hash,
|
||||
sqlx::types::Json(service_config) as _,
|
||||
summary,
|
||||
enabled,
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
@@ -1405,7 +1423,8 @@ pub async fn get_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
error,
|
||||
created_at,
|
||||
updated_at,
|
||||
summary
|
||||
summary,
|
||||
enabled
|
||||
FROM
|
||||
native_trigger
|
||||
WHERE
|
||||
@@ -1444,7 +1463,8 @@ pub async fn get_native_trigger_by_script<'c, E: sqlx::Executor<'c, Database = P
|
||||
error,
|
||||
created_at,
|
||||
updated_at,
|
||||
summary
|
||||
summary,
|
||||
enabled
|
||||
FROM
|
||||
native_trigger
|
||||
WHERE
|
||||
@@ -1491,7 +1511,8 @@ pub async fn list_native_triggers<'c, E: sqlx::Executor<'c, Database = Postgres>
|
||||
nt.error,
|
||||
nt.created_at,
|
||||
nt.updated_at,
|
||||
nt.summary
|
||||
nt.summary,
|
||||
nt.enabled
|
||||
FROM
|
||||
native_trigger nt
|
||||
WHERE
|
||||
@@ -1555,6 +1576,71 @@ pub async fn update_native_trigger_error<'c, E: sqlx::Executor<'c, Database = Po
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pause or resume a trigger. Returns `false` when there is no such trigger.
|
||||
///
|
||||
/// Callers MUST have verified write access to the trigger's runnable: this writes operational
|
||||
/// state and performs no authorization of its own.
|
||||
pub async fn set_native_trigger_enabled<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
db: E,
|
||||
workspace_id: &str,
|
||||
service_name: ServiceName,
|
||||
external_id: &str,
|
||||
enabled: bool,
|
||||
) -> Result<bool> {
|
||||
// `updated_at` is the row version `record_reregistration` conditions on, so leave it alone:
|
||||
// pausing a trigger must not make a registration that is mid-flight discard its result.
|
||||
let updated = sqlx::query!(
|
||||
r#"
|
||||
UPDATE native_trigger
|
||||
SET enabled = $1
|
||||
WHERE
|
||||
workspace_id = $2
|
||||
AND service_name = $3
|
||||
AND external_id = $4
|
||||
"#,
|
||||
enabled,
|
||||
workspace_id,
|
||||
service_name as ServiceName,
|
||||
external_id,
|
||||
)
|
||||
.execute(db)
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
Ok(updated > 0)
|
||||
}
|
||||
|
||||
/// Whether a webhook arriving for this trigger should start a job.
|
||||
///
|
||||
/// A trigger Windmill no longer knows about counts as enabled: the token in the URL is what
|
||||
/// authorizes the run, and this is a pause switch, not a second authorization check. It reads
|
||||
/// nothing a caller could not already learn from the trigger it is delivering for, so it needs no
|
||||
/// authorization of its own — but it also grants none, and must not be used as one.
|
||||
pub async fn native_trigger_is_enabled<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
db: E,
|
||||
workspace_id: &str,
|
||||
service_name: ServiceName,
|
||||
external_id: &str,
|
||||
) -> Result<bool> {
|
||||
let enabled = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT enabled
|
||||
FROM native_trigger
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
AND service_name = $2
|
||||
AND external_id = $3
|
||||
"#,
|
||||
workspace_id,
|
||||
service_name as ServiceName,
|
||||
external_id,
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
Ok(enabled.unwrap_or(true))
|
||||
}
|
||||
|
||||
pub async fn update_native_trigger_service_config<
|
||||
'c,
|
||||
E: sqlx::Executor<'c, Database = Postgres>,
|
||||
@@ -1701,6 +1787,11 @@ pub async fn delete_workspace_integration(
|
||||
///
|
||||
/// `external_id` is optional because during CREATE we don't have it yet
|
||||
/// (it's returned by the external service). During UPDATE, we have it.
|
||||
///
|
||||
/// Every registered URL MUST end up carrying it: it is the only thing a delivery identifies its
|
||||
/// trigger by, so a service that leaves it out ships a disable switch that silently does nothing.
|
||||
/// A handler that returns `None` from `service_config_from_create_response` gets this for free —
|
||||
/// `create_native_trigger` then runs the `update` cycle that re-registers with the assigned id.
|
||||
pub fn generate_webhook_service_url(
|
||||
base_url: &str,
|
||||
w_id: &str,
|
||||
|
||||
@@ -135,6 +135,7 @@ async fn reregister_one<T: External>(
|
||||
is_flow: trigger.is_flow,
|
||||
service_config,
|
||||
summary: trigger.summary.clone(),
|
||||
enabled: trigger.enabled,
|
||||
};
|
||||
|
||||
// The token is scoped to the runnable path and only its hash is kept, so pointing the webhook
|
||||
|
||||
@@ -224,9 +224,12 @@ export async function pushTrigger<K extends TriggerType>(
|
||||
}
|
||||
}
|
||||
|
||||
// `enabled` is operational state a sync deliberately does not carry: the server strips it from
|
||||
// the workspace export and the push below never sends it, so a created trigger comes up enabled
|
||||
// and pausing one stays a local decision.
|
||||
type NativeTriggerFile = Omit<
|
||||
NativeTrigger,
|
||||
"external_id" | "workspace_id" | "error"
|
||||
"external_id" | "workspace_id" | "error" | "enabled"
|
||||
>;
|
||||
|
||||
export async function pushNativeTrigger(
|
||||
@@ -264,6 +267,7 @@ export async function pushNativeTrigger(
|
||||
service_config: result.service_config,
|
||||
error: result.error,
|
||||
summary: result.summary,
|
||||
enabled: result.enabled,
|
||||
};
|
||||
log.debug(`Native trigger ${serviceName}/${externalId} exists on remote`);
|
||||
} catch {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import GitHubTriggerForm from './services/github/GitHubTriggerForm.svelte'
|
||||
import TriggerEditorToolbar from '$lib/components/triggers/TriggerEditorToolbar.svelte'
|
||||
import { handleConfigChange, type Trigger } from '$lib/components/triggers/utils'
|
||||
import type { TriggerMode } from '$lib/gen'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { Snippet } from 'svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
@@ -107,6 +108,7 @@
|
||||
let loadError = $state<string | undefined>(undefined)
|
||||
let externalError = $state<string | undefined>(undefined)
|
||||
let retryEdit = $state<(() => void) | undefined>(undefined)
|
||||
let enabled = $state(true)
|
||||
|
||||
export function openNew(
|
||||
nis_flow?: boolean,
|
||||
@@ -136,6 +138,7 @@
|
||||
loadError = undefined
|
||||
externalError = undefined
|
||||
retryEdit = undefined
|
||||
enabled = true
|
||||
}
|
||||
|
||||
export function openRecreate(nativeTrigger: ExtendedNativeTrigger) {
|
||||
@@ -163,6 +166,7 @@
|
||||
loadError = undefined
|
||||
externalError = undefined
|
||||
retryEdit = undefined
|
||||
enabled = nativeTrigger.enabled
|
||||
}
|
||||
|
||||
export async function openEdit(
|
||||
@@ -196,6 +200,7 @@
|
||||
scriptPath = ''
|
||||
initialScriptPath = ''
|
||||
summary = ''
|
||||
enabled = true
|
||||
|
||||
try {
|
||||
const fullTrigger = await NativeTriggerService.getNativeTrigger({
|
||||
@@ -211,6 +216,7 @@
|
||||
summary = fullTrigger.summary ?? ''
|
||||
externalData = fullTrigger.external_data
|
||||
externalError = fullTrigger.external_error ?? undefined
|
||||
enabled = fullTrigger.enabled
|
||||
|
||||
// Apply default values if provided (for draft triggers)
|
||||
if (defaultValues) {
|
||||
@@ -293,13 +299,41 @@
|
||||
}
|
||||
})
|
||||
|
||||
async function handleToggleMode(newMode: TriggerMode): Promise<boolean | void> {
|
||||
if (isNew || !externalId) {
|
||||
return false
|
||||
}
|
||||
const previous = enabled
|
||||
const next = newMode === 'enabled'
|
||||
enabled = next
|
||||
try {
|
||||
await NativeTriggerService.setNativeTriggerEnabled({
|
||||
workspace: $workspaceStore!,
|
||||
serviceName: service,
|
||||
externalId,
|
||||
requestBody: { enabled: next }
|
||||
})
|
||||
} catch (err: any) {
|
||||
enabled = previous
|
||||
sendUserToast(
|
||||
`Failed to ${next ? 'enable' : 'disable'} trigger: ${err.body ?? err.message}`,
|
||||
true
|
||||
)
|
||||
return false
|
||||
}
|
||||
sendUserToast(`${next ? 'Enabled' : 'Disabled'} ${serviceInfo?.serviceDisplayName} trigger`)
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
loading = true
|
||||
const saveCfg = getSaveCfg()
|
||||
const newExternalId = await saveNativeTriggerFromCfg(
|
||||
service,
|
||||
externalId ?? '',
|
||||
saveCfg,
|
||||
// A recreate registers a fresh webhook under a new external id, so it would otherwise
|
||||
// come back enabled: the create has to carry the pause, or the replacement is live
|
||||
// before anything can pause it again.
|
||||
isRecreate ? { ...saveCfg, enabled } : saveCfg,
|
||||
!isNew,
|
||||
$workspaceStore!,
|
||||
usedTriggerKinds
|
||||
@@ -396,7 +430,7 @@
|
||||
<TriggerEditorToolbar
|
||||
{trigger}
|
||||
permissions={loadingConfig || !can_write ? 'none' : 'create'}
|
||||
mode="enabled"
|
||||
mode={enabled ? 'enabled' : 'disabled'}
|
||||
{allowDraft}
|
||||
edit={!isNew}
|
||||
isLoading={loading}
|
||||
@@ -406,7 +440,7 @@
|
||||
{onReset}
|
||||
{onDelete}
|
||||
{cloudDisabled}
|
||||
onToggleMode={() => {}}
|
||||
onToggleMode={handleToggleMode}
|
||||
disableSuspendedMode={true}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { NativeTriggerService } from '$lib/gen/services.gen'
|
||||
import type { NativeServiceName } from '$lib/gen/types.gen'
|
||||
import type { NativeServiceName, TriggerMode } from '$lib/gen/types.gen'
|
||||
import type { ExtendedNativeTrigger } from './utils'
|
||||
import { getServiceConfig } from './utils'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { canWrite, sendUserToast } from '$lib/utils'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import TriggerModeToggle from '$lib/components/triggers/TriggerModeToggle.svelte'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
|
||||
@@ -25,9 +26,10 @@
|
||||
onEdit?: (trigger: TriggerW) => void
|
||||
onRecreate?: (trigger: TriggerW) => void
|
||||
onSync?: () => Promise<void>
|
||||
onUpdate?: () => void
|
||||
}
|
||||
|
||||
let { service, triggers = [], loading = false, onEdit, onRecreate }: Props = $props()
|
||||
let { service, triggers = [], loading = false, onEdit, onRecreate, onUpdate }: Props = $props()
|
||||
|
||||
const serviceConfig = $derived(getServiceConfig(service))
|
||||
let deleteConfirmationOpen = $state(false)
|
||||
@@ -45,6 +47,29 @@
|
||||
isDeleting = false
|
||||
}
|
||||
|
||||
async function onToggleMode(trigger: TriggerW, mode: TriggerMode): Promise<boolean> {
|
||||
const enabled = mode === 'enabled'
|
||||
try {
|
||||
await NativeTriggerService.setNativeTriggerEnabled({
|
||||
workspace: $workspaceStore!,
|
||||
serviceName: service,
|
||||
externalId: trigger.external_id,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
} catch (err: any) {
|
||||
sendUserToast(
|
||||
`Failed to ${enabled ? 'enable' : 'disable'} trigger: ${err.body ?? err.message}`,
|
||||
true
|
||||
)
|
||||
return false
|
||||
}
|
||||
sendUserToast(
|
||||
`${enabled ? 'Enabled' : 'Disabled'} ${serviceConfig?.serviceDisplayName} trigger ${trigger.external_id}`
|
||||
)
|
||||
onUpdate?.()
|
||||
return true
|
||||
}
|
||||
|
||||
async function confirmDeleteTrigger() {
|
||||
if (!triggerToDelete) return
|
||||
|
||||
@@ -132,6 +157,13 @@
|
||||
</a>
|
||||
|
||||
<div class="flex gap-2 items-center justify-end">
|
||||
<TriggerModeToggle
|
||||
canWrite={canWrite(trigger.script_path, {}, $userStore)}
|
||||
triggerMode={trigger.enabled ? 'enabled' : 'disabled'}
|
||||
onToggleMode={(mode) => onToggleMode(trigger, mode)}
|
||||
hideToggleLabels
|
||||
hideDropdown
|
||||
/>
|
||||
<Button
|
||||
on:click={() => onEdit?.(trigger)}
|
||||
unifiedSize="md"
|
||||
|
||||
@@ -216,6 +216,8 @@ export async function saveNativeTriggerFromCfg(
|
||||
service_config: triggerCfg.service_config,
|
||||
summary: triggerCfg.summary
|
||||
}
|
||||
// Only a create can set it: an update ignores the field, and `setenabled` owns it thereafter.
|
||||
const createBody: NativeTriggerData = { ...requestBody, enabled: triggerCfg.enabled ?? true }
|
||||
|
||||
const serviceName = NATIVE_TRIGGER_SERVICES[service].serviceDisplayName
|
||||
|
||||
@@ -233,7 +235,7 @@ export async function saveNativeTriggerFromCfg(
|
||||
const response = await NativeTriggerService.createNativeTrigger({
|
||||
workspace: workspace,
|
||||
serviceName: service,
|
||||
requestBody
|
||||
requestBody: createBody
|
||||
})
|
||||
externalId = response.external_id
|
||||
sendUserToast(`${serviceName} trigger ${externalId} created`)
|
||||
|
||||
@@ -291,6 +291,7 @@
|
||||
onEdit={(trigger) => editor?.openEdit(trigger.external_id, trigger.is_flow)}
|
||||
onRecreate={(trigger) => editor?.openRecreate(trigger)}
|
||||
onSync={syncTriggers}
|
||||
onUpdate={loadTriggers}
|
||||
/>
|
||||
{:else}
|
||||
<NoItemFound />
|
||||
|
||||
Reference in New Issue
Block a user