mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: stamp webhook trigger_kind on token-driven job runs (#10431)
* feat: stamp ui vs webhook trigger_kind on direct job runs * fix: gate ui trigger kind on min worker version and dedupe display names * docs: state that the ui trigger kind attributes rather than proves * refactor: fold the trigger fallback into one trigger_or_fallback helper * feat: hold trigger_kind as a tolerant label on the worker paths * chore: refresh the sqlx offline cache for the trigger_kind label queries * chore: update ee-repo-ref to 7de7daff5eed410e0c815ad6b292d2b4303f02f2 This commit updates the EE repository reference after PR #700 was merged in windmill-ee-private. Previous ee-repo-ref: 974ab910d9a30c5565e1198ee312acc6d11239f3 New ee-repo-ref: 7de7daff5eed410e0c815ad6b292d2b4303f02f2 Automated by sync-ee-ref workflow. * fix: keep the API job structs tolerant of unknown trigger kinds too * chore: point ee-repo-ref at the merged EE main --------- Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,\n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
|
||||
"query": "SELECT\n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,\n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -137,7 +137,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "trigger_kind: JobTriggerKind",
|
||||
"name": "trigger_kind: TriggerKindLabel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
@@ -236,5 +236,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "67e25a7c19ea0ffaf7ea5303fcd04af5a7eb488c76f783e690af0c2153b1d6a8"
|
||||
"hash": "09ae123099f05fe3db8bd67ca2c0d778a27619dd7fad71ba656f6b54c4915d1e"
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n id,\n q.runnable_settings_handle,\n q.workspace_id,\n j.runnable_id as \"runnable_id: ScriptHash\",\n scheduled_for,\n parent_job,\n flow_innermost_root_job,\n runnable_path,\n kind as \"kind: JobKind\",\n started_at,\n permissioned_as,\n created_by,\n script_lang as \"script_lang: ScriptLang\",\n permissioned_as_email,\n flow_step_id,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n trigger,\n q.priority,\n concurrent_limit,\n q.tag,\n cache_ttl,\n cache_ignore_s3_path,\n r.ping as last_ping,\n worker,\n memory_peak,\n running\n FROM v2_job_queue q\n JOIN v2_job j USING (id)\n LEFT JOIN v2_job_runtime r USING (id)\n LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1",
|
||||
"query": "SELECT\n id,\n q.runnable_settings_handle,\n q.workspace_id,\n j.runnable_id as \"runnable_id: ScriptHash\",\n scheduled_for,\n parent_job,\n flow_innermost_root_job,\n runnable_path,\n kind as \"kind: JobKind\",\n started_at,\n permissioned_as,\n created_by,\n script_lang as \"script_lang: ScriptLang\",\n permissioned_as_email,\n flow_step_id,\n trigger_kind as \"trigger_kind: TriggerKindLabel\",\n trigger,\n q.priority,\n concurrent_limit,\n q.tag,\n cache_ttl,\n cache_ignore_s3_path,\n r.ping as last_ping,\n worker,\n memory_peak,\n running\n FROM v2_job_queue q\n JOIN v2_job j USING (id)\n LEFT JOIN v2_job_runtime r USING (id)\n LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -142,7 +142,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "trigger_kind: JobTriggerKind",
|
||||
"name": "trigger_kind: TriggerKindLabel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
@@ -259,5 +259,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3771b690c5966272b1f42c9965bb6a8f961c119516e4c33dc928cd3b4f4edbc"
|
||||
"hash": "9d331b63c901071a7e82c0a4a6859c77a37ef784fdac78ca1e7a971e141e780f"
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
|
||||
"query": "SELECT\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: TriggerKindLabel\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -227,7 +227,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"name": "trigger_kind: JobTriggerKind",
|
||||
"name": "trigger_kind: TriggerKindLabel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
@@ -313,5 +313,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d4211392e174a0e8f89c7fcebdf120e5b0f629f9f04e08a2982df33ff23ac7a9"
|
||||
"hash": "d0df9d1fda20f0366505bf6abf67446a5e8e23d87eebc46bbb90bdbdf128935b"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
68aedf0deb0200f7e7612175177bc9d579ce11a6
|
||||
7de7daff5eed410e0c815ad6b292d2b4303f02f2
|
||||
|
||||
@@ -21,6 +21,7 @@ fn scoped_authed(scopes: Vec<&str>) -> ApiAuthed {
|
||||
scopes: Some(scopes.into_iter().map(str::to_string).collect()),
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ fn make_authed() -> windmill_api_auth::ApiAuthed {
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ use windmill_common::DB;
|
||||
|
||||
use windmill_common::{
|
||||
auth::{
|
||||
get_folders_for_user, get_groups_for_user, hash_token, safe_token_prefix, JWTAuthClaims,
|
||||
get_folders_for_user, get_groups_for_user, hash_token, is_session_label, safe_token_prefix,
|
||||
JWTAuthClaims,
|
||||
},
|
||||
error::{Error, JsonResult},
|
||||
jwt,
|
||||
@@ -196,6 +197,7 @@ impl AuthCache {
|
||||
tracing::error!("JWT auth error: workspace_id mismatch");
|
||||
return None;
|
||||
}
|
||||
let is_session_token = is_session_label(claims.label.as_deref());
|
||||
let (username_override, username_override_is_token_label) =
|
||||
username_override_from_label(claims.label);
|
||||
|
||||
@@ -213,6 +215,7 @@ impl AuthCache {
|
||||
scopes: claims.scopes,
|
||||
username_override,
|
||||
username_override_is_token_label,
|
||||
is_session_token,
|
||||
token_prefix: claims.audit_span,
|
||||
read_only: false,
|
||||
};
|
||||
@@ -267,6 +270,7 @@ impl AuthCache {
|
||||
(Some(owner), Some(email), super_admin, _, label, read_only)
|
||||
if w_id.is_some() =>
|
||||
{
|
||||
let is_session_token = is_session_label(label.as_deref());
|
||||
let (username_override, username_override_is_token_label) =
|
||||
username_override_from_label(label);
|
||||
if let Some((prefix, name)) = owner.split_once('/') {
|
||||
@@ -312,6 +316,7 @@ impl AuthCache {
|
||||
scopes: None,
|
||||
username_override,
|
||||
username_override_is_token_label,
|
||||
is_session_token,
|
||||
token_prefix: Some(safe_token_prefix(token)),
|
||||
read_only,
|
||||
})
|
||||
@@ -363,6 +368,7 @@ impl AuthCache {
|
||||
scopes: None,
|
||||
username_override,
|
||||
username_override_is_token_label,
|
||||
is_session_token,
|
||||
token_prefix: Some(safe_token_prefix(token)),
|
||||
read_only,
|
||||
})
|
||||
@@ -391,6 +397,7 @@ impl AuthCache {
|
||||
}
|
||||
}
|
||||
(_, Some(email), super_admin, scopes, label, read_only) => {
|
||||
let is_session_token = is_session_label(label.as_deref());
|
||||
let (username_override, username_override_is_token_label) =
|
||||
username_override_from_label(label);
|
||||
if w_id.is_some() {
|
||||
@@ -436,6 +443,7 @@ impl AuthCache {
|
||||
scopes,
|
||||
username_override,
|
||||
username_override_is_token_label,
|
||||
is_session_token,
|
||||
token_prefix: Some(safe_token_prefix(token)),
|
||||
read_only,
|
||||
})
|
||||
@@ -458,6 +466,7 @@ impl AuthCache {
|
||||
scopes,
|
||||
username_override,
|
||||
username_override_is_token_label,
|
||||
is_session_token,
|
||||
token_prefix: Some(safe_token_prefix(token)),
|
||||
read_only,
|
||||
}),
|
||||
@@ -482,6 +491,7 @@ impl AuthCache {
|
||||
scopes,
|
||||
username_override,
|
||||
username_override_is_token_label,
|
||||
is_session_token,
|
||||
token_prefix: Some(safe_token_prefix(token)),
|
||||
read_only,
|
||||
})
|
||||
@@ -518,6 +528,7 @@ impl AuthCache {
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: Some(safe_token_prefix(token)),
|
||||
read_only: false,
|
||||
};
|
||||
@@ -726,6 +737,7 @@ fn no_auth_admin_authed() -> ApiAuthed {
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ use windmill_common::{
|
||||
},
|
||||
db::{Authable, Authed, AuthedRef},
|
||||
error::{self, Error, Result},
|
||||
jobs::JobTriggerKind,
|
||||
triggers::TriggerMetadata,
|
||||
users::username_to_permissioned_as,
|
||||
DB,
|
||||
};
|
||||
@@ -64,6 +66,10 @@ pub struct ApiAuthed {
|
||||
/// end-user override passes a `created_by` through verbatim, and that may itself be a
|
||||
/// `label-*` string. Only `username_override_from_label` sets it.
|
||||
pub username_override_is_token_label: bool,
|
||||
/// Whether the request authenticated with the session token minted at browser login.
|
||||
/// Only `trigger_or_fallback` reads it — see `is_session_label` for why it attributes
|
||||
/// rather than proves, and must not gate authority.
|
||||
pub is_session_token: bool,
|
||||
pub token_prefix: Option<String>,
|
||||
pub read_only: bool,
|
||||
}
|
||||
@@ -101,6 +107,25 @@ impl ApiAuthed {
|
||||
self.username_override = username_override;
|
||||
self.username_override_is_token_label = false;
|
||||
}
|
||||
|
||||
/// The `trigger_kind` a run started through a `/jobs/run*` route is stamped with: a trigger
|
||||
/// that built its own metadata always wins, and a run driven by any other token — webhooks,
|
||||
/// the CLI, the SDKs — is `webhook`, matching the `wm_trigger.kind` the preprocessor already
|
||||
/// reports for these routes. Derived from the token, never from the request, because the
|
||||
/// column is authority-bearing for other kinds (`app` marks a file as app-produced).
|
||||
///
|
||||
/// A browser session is left unstamped rather than marked [`JobTriggerKind::Ui`]: that label
|
||||
/// is one a worker built before this release cannot decode, and it would strand the jobs
|
||||
/// carrying it. `webhook` has always been decodable, so it is safe to write today.
|
||||
pub fn trigger_or_fallback(&self, trigger: Option<TriggerMetadata>) -> Option<TriggerMetadata> {
|
||||
if trigger.is_some() {
|
||||
return trigger;
|
||||
}
|
||||
if self.is_session_token {
|
||||
return None;
|
||||
}
|
||||
Some(TriggerMetadata::new(None, JobTriggerKind::Webhook))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiAuthed> for Authed {
|
||||
@@ -130,6 +155,7 @@ impl From<Authed> for ApiAuthed {
|
||||
scopes: value.scopes,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: value.token_prefix,
|
||||
read_only: false,
|
||||
}
|
||||
@@ -883,6 +909,7 @@ pub async fn fetch_api_authed_from_permissioned_as(
|
||||
scopes: authed.scopes,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: authed.token_prefix,
|
||||
read_only: false,
|
||||
};
|
||||
@@ -1284,6 +1311,50 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A browser session is the one shape left unstamped, so the Runs page can say "a token
|
||||
/// started this" without claiming the converse. Every other label — every shape a member
|
||||
/// can pass to `create_token` — is `webhook`.
|
||||
#[test]
|
||||
fn only_a_browser_session_is_left_unstamped() {
|
||||
let kind_of = |label: Option<&str>| {
|
||||
ApiAuthed {
|
||||
is_session_token: windmill_common::auth::is_session_label(label),
|
||||
..Default::default()
|
||||
}
|
||||
.trigger_or_fallback(None)
|
||||
.map(|t| t.trigger_kind.to_string())
|
||||
};
|
||||
|
||||
assert_eq!(kind_of(Some("session")), None);
|
||||
|
||||
for label in [
|
||||
Some("my-personal-token"),
|
||||
Some("webhook-f/svc/my_script"),
|
||||
Some("Ephemeral lsp token"),
|
||||
Some("ephemeral-script"),
|
||||
Some("ephemeral-webhook-google-abc12"),
|
||||
Some("mcp-oauth-mcp-client-9f3a1c"),
|
||||
Some(""),
|
||||
// A label-less token: the job WM_TOKEN, and any token created without one.
|
||||
None,
|
||||
] {
|
||||
assert_eq!(kind_of(label).as_deref(), Some("webhook"), "label {label:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A trigger that built its own metadata must survive the fallback, or a scheduled or
|
||||
/// routed run started under a personal token would be re-attributed to a webhook.
|
||||
#[test]
|
||||
fn a_real_trigger_wins_over_the_token_fallback() {
|
||||
let authed = ApiAuthed::default();
|
||||
let schedule =
|
||||
TriggerMetadata::new(Some("u/alice/nightly".to_string()), JobTriggerKind::Schedule);
|
||||
|
||||
let kept = authed.trigger_or_fallback(Some(schedule)).unwrap();
|
||||
assert_eq!(kept.trigger_kind.to_string(), "schedule");
|
||||
assert_eq!(kept.trigger_path.as_deref(), Some("u/alice/nightly"));
|
||||
}
|
||||
|
||||
// Regression tests for the Preview path traversal: a Preview's path skips the
|
||||
// DB `proper_id` CHECK and reaches the worker, where it builds on-disk module
|
||||
// dirs. Traversal must be rejected even for admins, who otherwise bypass the
|
||||
|
||||
@@ -51,6 +51,7 @@ fn test_authed() -> ApiAuthed {
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
}
|
||||
|
||||
@@ -770,7 +770,7 @@ pub async fn run_flow<'c>(
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
None,
|
||||
trigger,
|
||||
authed.trigger_or_fallback(trigger),
|
||||
run_query.suspended_mode,
|
||||
)
|
||||
.await?;
|
||||
@@ -990,7 +990,7 @@ pub async fn push_script_job_by_path_into_queue<'c>(
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
None,
|
||||
trigger,
|
||||
authed.trigger_or_fallback(trigger),
|
||||
run_query.suspended_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -455,6 +455,7 @@ impl From<UnifiedJob> for Job {
|
||||
resolved_at: None,
|
||||
resolution_note: None,
|
||||
resolved_automatically: None,
|
||||
trigger_kind: None,
|
||||
},
|
||||
)),
|
||||
"QueuedJob" => Job::QueuedJob(JobExtended::new(
|
||||
@@ -505,6 +506,7 @@ impl From<UnifiedJob> for Job {
|
||||
runnable_settings_handle: uj.runnable_settings_handle,
|
||||
labels: uj.labels,
|
||||
is_retry: uj.is_retry,
|
||||
trigger_kind: None,
|
||||
},
|
||||
)),
|
||||
t => panic!("job type {} not valid", t),
|
||||
|
||||
@@ -9747,6 +9747,7 @@ async fn load_workspace_authed(
|
||||
scopes: base_authed.scopes.clone(),
|
||||
username_override: base_authed.username_override.clone(),
|
||||
username_override_is_token_label: base_authed.username_override_is_token_label,
|
||||
is_session_token: base_authed.is_session_token,
|
||||
token_prefix: base_authed.token_prefix.clone(),
|
||||
read_only: base_authed.read_only,
|
||||
});
|
||||
@@ -9777,6 +9778,7 @@ async fn load_workspace_authed(
|
||||
scopes: base_authed.scopes.clone(),
|
||||
username_override: base_authed.username_override.clone(),
|
||||
username_override_is_token_label: base_authed.username_override_is_token_label,
|
||||
is_session_token: base_authed.is_session_token,
|
||||
token_prefix: base_authed.token_prefix.clone(),
|
||||
read_only: base_authed.read_only,
|
||||
})
|
||||
|
||||
@@ -25778,6 +25778,8 @@ components:
|
||||
type: boolean
|
||||
is_retry:
|
||||
type: boolean
|
||||
trigger_kind:
|
||||
$ref: "#/components/schemas/JobTriggerKind"
|
||||
worker:
|
||||
type: string
|
||||
required:
|
||||
@@ -25919,6 +25921,8 @@ components:
|
||||
true when a succeeding retry resolved this rather than a person. Explicit rather
|
||||
than inferred from an absent resolved_by, which is also absent for a manual
|
||||
resolution outside enterprise
|
||||
trigger_kind:
|
||||
$ref: "#/components/schemas/JobTriggerKind"
|
||||
worker:
|
||||
type: string
|
||||
required:
|
||||
@@ -27776,6 +27780,7 @@ components:
|
||||
- asset
|
||||
- freshness
|
||||
- app
|
||||
- ui
|
||||
|
||||
TriggerMode:
|
||||
description: job trigger mode
|
||||
|
||||
@@ -1527,7 +1527,7 @@ macro_rules! get_job_query {
|
||||
END
|
||||
ELSE '{{\"reason\": \"WINDMILL_TOO_BIG\"}}'::jsonb END as args, flow_status, workflow_as_code_status, \
|
||||
{logs} as logs, {code} as raw_code, canceled_by is not null as canceled, canceled_by, canceled_reason, kind as job_kind, \
|
||||
CASE WHEN trigger_kind = 'schedule'::job_trigger_kind THEN trigger END AS schedule_path, permissioned_as, \
|
||||
CASE WHEN trigger_kind = 'schedule'::job_trigger_kind THEN trigger END AS schedule_path, v2_job.trigger_kind, permissioned_as, \
|
||||
{flow} as raw_flow, flow_step_id IS NOT NULL AS is_flow_step, script_lang as language, \
|
||||
{lock} as raw_lock, permissioned_as_email as email, visible_to_owner, memory_peak as mem_peak, v2_job.tag, v2_job.priority, preprocessed, worker,\
|
||||
{additional_fields} \
|
||||
@@ -6208,7 +6208,7 @@ pub async fn restart_flow(
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
run_query.suspended_mode,
|
||||
)
|
||||
.await?;
|
||||
@@ -6790,7 +6790,7 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
run_query.suspended_mode,
|
||||
)
|
||||
.await?;
|
||||
@@ -6935,7 +6935,7 @@ pub async fn run_wait_result_script_by_path_internal(
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
run_query.suspended_mode,
|
||||
)
|
||||
.await?;
|
||||
@@ -7061,7 +7061,7 @@ pub async fn run_wait_result_script_by_hash(
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
run_query.suspended_mode,
|
||||
)
|
||||
.await?;
|
||||
@@ -7543,7 +7543,7 @@ async fn run_preview_script(
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
@@ -7909,7 +7909,7 @@ async fn run_bundle_preview_script(
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
@@ -8559,7 +8559,7 @@ async fn run_preview_flow_job(
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
@@ -8826,7 +8826,7 @@ async fn run_dynamic_select(
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
authed.trigger_or_fallback(None),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
@@ -8974,7 +8974,7 @@ pub async fn run_job_by_hash_inner(
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
None,
|
||||
trigger,
|
||||
authed.trigger_or_fallback(trigger),
|
||||
run_query.suspended_mode,
|
||||
)
|
||||
.await?;
|
||||
@@ -10952,6 +10952,7 @@ mod approval_view_gate_tests {
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
}
|
||||
|
||||
@@ -329,6 +329,7 @@ async fn inject_agent_authed(
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
username_override_is_token_label: false,
|
||||
is_session_token: false,
|
||||
token_prefix: None,
|
||||
read_only: false,
|
||||
},
|
||||
|
||||
@@ -59,6 +59,13 @@ pub fn is_server_minted_label(label: &str) -> bool {
|
||||
|| label.starts_with("mcp-oauth-")
|
||||
}
|
||||
|
||||
/// Whether `label` is the one minted for a browser session at login. [`is_server_minted_label`]
|
||||
/// stops a member minting it directly, but `/users/refresh_token` hands one to any authenticated
|
||||
/// caller, so this attributes a request to the UI without proving it: never gate authority on it.
|
||||
pub fn is_session_label(label: Option<&str>) -> bool {
|
||||
label == Some("session")
|
||||
}
|
||||
|
||||
/// Hash a raw token using SHA-256 (hex-encoded, 64 chars).
|
||||
/// Used to store and look up tokens without keeping plaintext in the DB.
|
||||
pub fn hash_token(token: &str) -> String {
|
||||
|
||||
@@ -41,7 +41,7 @@ use windmill_common::audit::AuditAuthor;
|
||||
use windmill_common::auth::JobPerms;
|
||||
#[cfg(feature = "benchmark")]
|
||||
use windmill_common::bench::BenchmarkIter;
|
||||
use windmill_common::jobs::{JobTriggerKind, EMAIL_ERROR_HANDLER_USER_EMAIL};
|
||||
use windmill_common::jobs::{JobTriggerKind, TriggerKindLabel, EMAIL_ERROR_HANDLER_USER_EMAIL};
|
||||
use windmill_common::min_version::{
|
||||
MIN_VERSION_SUPPORTS_DEBOUNCING, MIN_VERSION_SUPPORTS_DEBOUNCING_V2,
|
||||
};
|
||||
@@ -3004,7 +3004,7 @@ pub struct MiniPulledJob {
|
||||
pub preprocessed: Option<bool>,
|
||||
pub script_entrypoint_override: Option<String>,
|
||||
pub trigger: Option<String>,
|
||||
pub trigger_kind: Option<JobTriggerKind>,
|
||||
pub trigger_kind: Option<TriggerKindLabel>,
|
||||
pub visible_to_owner: bool,
|
||||
pub permissioned_as_end_user_email: Option<String>,
|
||||
pub runnable_settings_handle: Option<i64>,
|
||||
@@ -3080,7 +3080,7 @@ pub struct MiniCompletedJob {
|
||||
pub script_lang: Option<ScriptLang>,
|
||||
pub permissioned_as_email: String,
|
||||
pub flow_step_id: Option<String>,
|
||||
pub trigger_kind: Option<JobTriggerKind>,
|
||||
pub trigger_kind: Option<TriggerKindLabel>,
|
||||
pub trigger: Option<String>,
|
||||
pub priority: Option<i16>,
|
||||
pub concurrent_limit: Option<i32>,
|
||||
@@ -3107,7 +3107,7 @@ impl From<QueuedJobV2> for MiniCompletedJob {
|
||||
script_lang: job.script_lang,
|
||||
permissioned_as_email: job.permissioned_as_email,
|
||||
flow_step_id: job.flow_step_id,
|
||||
trigger_kind: job.trigger_kind,
|
||||
trigger_kind: job.trigger_kind.map(Into::into),
|
||||
trigger: job.trigger,
|
||||
priority: job.priority,
|
||||
concurrent_limit: job.concurrent_limit,
|
||||
@@ -3196,12 +3196,12 @@ impl MiniCompletedJob {
|
||||
}
|
||||
|
||||
fn schedule_path(
|
||||
trigger_kind: &Option<JobTriggerKind>,
|
||||
trigger_kind: &Option<TriggerKindLabel>,
|
||||
trigger: &Option<String>,
|
||||
) -> Option<String> {
|
||||
if trigger_kind
|
||||
.as_ref()
|
||||
.is_some_and(|t| matches!(t, JobTriggerKind::Schedule))
|
||||
.is_some_and(|t| t.is(JobTriggerKind::Schedule))
|
||||
{
|
||||
trigger.clone()
|
||||
} else {
|
||||
@@ -3279,11 +3279,10 @@ impl MiniPulledJob {
|
||||
preprocessed: job.preprocessed.clone(),
|
||||
script_entrypoint_override: job.script_entrypoint_override.clone(),
|
||||
trigger: job.schedule_path.clone(),
|
||||
trigger_kind: if job.schedule_path.is_some() {
|
||||
Some(JobTriggerKind::Schedule)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
trigger_kind: job
|
||||
.schedule_path
|
||||
.is_some()
|
||||
.then(|| JobTriggerKind::Schedule.into()),
|
||||
visible_to_owner: job.visible_to_owner.clone(),
|
||||
permissioned_as_end_user_email: None,
|
||||
}
|
||||
@@ -3493,7 +3492,7 @@ pub async fn get_mini_pulled_job<'c>(
|
||||
preprocessed,
|
||||
script_entrypoint_override,
|
||||
trigger,
|
||||
trigger_kind as \"trigger_kind: JobTriggerKind\",
|
||||
trigger_kind as \"trigger_kind: TriggerKindLabel\",
|
||||
visible_to_owner,
|
||||
NULL as permissioned_as_end_user_email
|
||||
FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
|
||||
@@ -3520,7 +3519,7 @@ pub struct QueuedJobV2 {
|
||||
pub script_lang: Option<ScriptLang>,
|
||||
pub permissioned_as_email: String,
|
||||
pub flow_step_id: Option<String>,
|
||||
pub trigger_kind: Option<JobTriggerKind>,
|
||||
pub trigger_kind: Option<TriggerKindLabel>,
|
||||
pub trigger: Option<String>,
|
||||
pub priority: Option<i16>,
|
||||
pub concurrent_limit: Option<i32>,
|
||||
@@ -3562,7 +3561,7 @@ pub async fn get_queued_job_v2<'c>(
|
||||
script_lang as "script_lang: ScriptLang",
|
||||
permissioned_as_email,
|
||||
flow_step_id,
|
||||
trigger_kind as "trigger_kind: JobTriggerKind",
|
||||
trigger_kind as "trigger_kind: TriggerKindLabel",
|
||||
trigger,
|
||||
q.priority,
|
||||
concurrent_limit,
|
||||
@@ -5062,7 +5061,7 @@ pub fn get_mini_completed_job<'a, 'e, A: sqlx::Acquire<'e, Database = Postgres>
|
||||
MiniCompletedJob,
|
||||
"SELECT
|
||||
j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,
|
||||
j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle
|
||||
j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle
|
||||
FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id
|
||||
WHERE j.id = $1 AND j.workspace_id = $2",
|
||||
id,
|
||||
|
||||
@@ -36,7 +36,7 @@ mod native_retry {
|
||||
script_lang: Some(ScriptLang::Deno),
|
||||
permissioned_as_email: "test@windmill.dev".to_string(),
|
||||
flow_step_id: None,
|
||||
trigger_kind: Some(JobTriggerKind::Schedule),
|
||||
trigger_kind: Some(JobTriggerKind::Schedule.into()),
|
||||
trigger: Some(SCHED.to_string()),
|
||||
priority: None,
|
||||
concurrent_limit: None,
|
||||
|
||||
@@ -83,7 +83,7 @@ mod schedule_push {
|
||||
script_lang: None,
|
||||
permissioned_as_email: schedule.email.clone(),
|
||||
flow_step_id: None,
|
||||
trigger_kind: Some(JobTriggerKind::Schedule),
|
||||
trigger_kind: Some(JobTriggerKind::Schedule.into()),
|
||||
trigger: Some(schedule.path.clone()),
|
||||
priority: None,
|
||||
concurrent_limit: None,
|
||||
|
||||
@@ -37,10 +37,13 @@ async fn get_suspended_trigger(
|
||||
path: &str,
|
||||
) -> Result<SuspendedTrigger> {
|
||||
// Only trigger kinds backed by a `<kind>_trigger` table support reassignment.
|
||||
// `app` (and webhook/schedule) have no such table, so reject them with a clear
|
||||
// `app`/`ui` (and webhook/schedule) have no such table, so reject them with a clear
|
||||
// error instead of failing on a missing-relation database error below.
|
||||
match trigger_kind {
|
||||
JobTriggerKind::Webhook | JobTriggerKind::Schedule | JobTriggerKind::App => {
|
||||
JobTriggerKind::Webhook
|
||||
| JobTriggerKind::Schedule
|
||||
| JobTriggerKind::App
|
||||
| JobTriggerKind::Ui => {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"{} triggers do not support job reassignment",
|
||||
trigger_kind
|
||||
|
||||
@@ -55,11 +55,17 @@ pub enum JobTriggerKind {
|
||||
// direct `/jobs/run` cannot set it, so it distinguishes files an app actually
|
||||
// produced from files a viewer forged by running a declared runnable directly.
|
||||
App,
|
||||
// A run started from the browser (the `/jobs/run*` request carried the session token
|
||||
// minted at login). Nothing writes this yet — it exists so every binary from this release
|
||||
// can decode it, which is what a later release needs before it can start stamping it.
|
||||
// When that happens: attribution, not authority, unlike `App` — a member can have a
|
||||
// session token minted for a script (`/users/refresh_token`), so nothing may gate on it.
|
||||
Ui,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobTriggerKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let kind = match self {
|
||||
impl JobTriggerKind {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
JobTriggerKind::Webhook => "webhook",
|
||||
JobTriggerKind::Http => "http",
|
||||
JobTriggerKind::Websocket => "websocket",
|
||||
@@ -80,8 +86,52 @@ impl std::fmt::Display for JobTriggerKind {
|
||||
JobTriggerKind::Asset => "asset",
|
||||
JobTriggerKind::Freshness => "freshness",
|
||||
JobTriggerKind::App => "app",
|
||||
};
|
||||
write!(f, "{}", kind)
|
||||
JobTriggerKind::Ui => "ui",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobTriggerKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// `v2_job.trigger_kind` as a worker reads it back, kept as the raw label rather than as
|
||||
/// [`JobTriggerKind`]. A worker has to keep running jobs stamped with a kind added after it was
|
||||
/// built, and decoding into the enum turns an unfamiliar label into a hard error — sqlx on the
|
||||
/// pull query, serde on the agent-worker payload — which strands the job instead of running it.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct TriggerKindLabel(pub String);
|
||||
|
||||
impl TriggerKindLabel {
|
||||
pub fn is(&self, kind: JobTriggerKind) -> bool {
|
||||
self.0 == kind.as_str()
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JobTriggerKind> for TriggerKindLabel {
|
||||
fn from(kind: JobTriggerKind) -> Self {
|
||||
Self(kind.as_str().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl sqlx::Type<sqlx::Postgres> for TriggerKindLabel {
|
||||
fn type_info() -> sqlx::postgres::PgTypeInfo {
|
||||
sqlx::postgres::PgTypeInfo::with_name("JOB_TRIGGER_KIND")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for TriggerKindLabel {
|
||||
fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
Ok(Self(
|
||||
<&str as sqlx::Decode<sqlx::Postgres>>::decode(value)?.to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +302,13 @@ pub struct QueuedJob {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(default)]
|
||||
pub is_retry: Option<bool>,
|
||||
// How the run was started. NULL on every job pushed before the API began stamping it, and
|
||||
// on the paths that still don't, so the run page treats it as "unknown" rather than
|
||||
// "manual". `#[sqlx(default)]` lets the queries that don't select it omit the column, and
|
||||
// the label type keeps a kind added after this binary was built readable instead of fatal.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(default)]
|
||||
pub trigger_kind: Option<TriggerKindLabel>,
|
||||
}
|
||||
|
||||
impl QueuedJob {
|
||||
@@ -328,6 +385,7 @@ impl Default for QueuedJob {
|
||||
runnable_settings_handle: None,
|
||||
labels: None,
|
||||
is_retry: None,
|
||||
trigger_kind: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,6 +473,13 @@ pub struct CompletedJob {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(default)]
|
||||
pub resolved_automatically: Option<bool>,
|
||||
// How the run was started. NULL on every job pushed before the API began stamping it, and
|
||||
// on the paths that still don't, so the run page treats it as "unknown" rather than
|
||||
// "manual". `#[sqlx(default)]` lets the queries that don't select it omit the column, and
|
||||
// the label type keeps a kind added after this binary was built readable instead of fatal.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(default)]
|
||||
pub trigger_kind: Option<TriggerKindLabel>,
|
||||
}
|
||||
|
||||
impl CompletedJob {
|
||||
@@ -660,7 +725,7 @@ pub fn generate_dynamic_input_key(workspace_id: &str, path: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_valid_entrypoint_name;
|
||||
use super::{is_valid_entrypoint_name, JobTriggerKind, TriggerKindLabel};
|
||||
|
||||
#[test]
|
||||
fn valid_entrypoint_names_are_accepted() {
|
||||
@@ -705,4 +770,22 @@ mod tests {
|
||||
// Over-long names are rejected.
|
||||
assert!(!is_valid_entrypoint_name(&"a".repeat(256)));
|
||||
}
|
||||
|
||||
/// The point of [`TriggerKindLabel`]: a worker built before a trigger kind existed must
|
||||
/// still be able to read — and therefore run — a job stamped with it. Decoding into
|
||||
/// [`JobTriggerKind`] instead makes an unfamiliar label a hard error and strands the job.
|
||||
#[test]
|
||||
fn an_unknown_trigger_kind_label_survives_deserialization() {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Job {
|
||||
trigger_kind: Option<TriggerKindLabel>,
|
||||
}
|
||||
|
||||
let job: Job = serde_json::from_str(r#"{"trigger_kind":"a_kind_from_the_future"}"#).unwrap();
|
||||
assert_eq!(job.trigger_kind.as_ref().map(|k| k.as_str()), Some("a_kind_from_the_future"));
|
||||
assert!(!job.trigger_kind.unwrap().is(JobTriggerKind::Schedule));
|
||||
|
||||
let job: Job = serde_json::from_str(r#"{"trigger_kind":"schedule"}"#).unwrap();
|
||||
assert!(job.trigger_kind.unwrap().is(JobTriggerKind::Schedule));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +528,7 @@ pub async fn get_reserved_variables(
|
||||
};
|
||||
|
||||
let tested_runnable = match (&job.trigger_kind, &job.trigger) {
|
||||
(Some(windmill_common::jobs::JobTriggerKind::CiTest), Some(t)) => Some(t.clone()),
|
||||
(Some(k), Some(t)) if k.is(windmill_common::jobs::JobTriggerKind::CiTest) => Some(t.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ async fn process_jc(
|
||||
span.record("root_job", root_job.to_string().as_str());
|
||||
}
|
||||
if let Some(trigger_kind) = jc.job.trigger_kind.as_ref() {
|
||||
span.record("trigger_kind", trigger_kind.to_string().as_str());
|
||||
span.record("trigger_kind", trigger_kind.as_str());
|
||||
}
|
||||
if let Some(trigger) = jc.job.trigger.as_ref() {
|
||||
span.record("trigger", trigger.as_str());
|
||||
|
||||
@@ -1576,7 +1576,7 @@ pub fn create_span_with_name(
|
||||
span.record("hostname", hostname);
|
||||
}
|
||||
if let Some(trigger_kind) = arc_job.trigger_kind.as_ref() {
|
||||
span.record("trigger_kind", trigger_kind.to_string().as_str());
|
||||
span.record("trigger_kind", trigger_kind.as_str());
|
||||
}
|
||||
if let Some(trigger) = arc_job.trigger.as_ref() {
|
||||
span.record("trigger", trigger.as_str());
|
||||
@@ -1686,7 +1686,7 @@ pub fn log_context_for_job(
|
||||
flow_step_id: arc_job.flow_step_id.clone(),
|
||||
parent_job: arc_job.parent_job.map(|id| id.to_string()),
|
||||
root_job: arc_job.flow_innermost_root_job.map(|id| id.to_string()),
|
||||
trigger_kind: arc_job.trigger_kind.as_ref().map(|k| k.to_string()),
|
||||
trigger_kind: arc_job.trigger_kind.as_ref().map(|k| k.as_str().to_string()),
|
||||
trigger: arc_job.trigger.clone(),
|
||||
hostname: hostname.map(|h| h.to_string()),
|
||||
inbound_traceparent: job_inbound_traceparent(arc_job),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Job } from '$lib/gen'
|
||||
import { triggerIconMap } from '$lib/components/triggers/utils'
|
||||
import { triggerDisplayNamesMap, triggerIconMap } from '$lib/components/triggers/utils'
|
||||
import { formatMemory } from '$lib/utils'
|
||||
import { flowPathToHref } from '$lib/scripts'
|
||||
import { Calendar, Bot } from 'lucide-svelte'
|
||||
@@ -125,14 +125,13 @@ export function getTriggerInfo(job: Job): { type: string; icon: any; detail?: st
|
||||
}
|
||||
|
||||
// Check for trigger type from job trigger_kind if available
|
||||
if ('trigger_kind' in job) {
|
||||
const triggerKind = (job as any).trigger_kind
|
||||
if (triggerKind && triggerIconMap[triggerKind]) {
|
||||
return {
|
||||
type: getTriggerDisplayName(triggerKind),
|
||||
icon: triggerIconMap[triggerKind],
|
||||
detail: triggerKind
|
||||
}
|
||||
const triggerKind = job.trigger_kind
|
||||
if (triggerKind && triggerKind in triggerIconMap) {
|
||||
// No detail: unlike the schedule branch above, the kind carries no path to add,
|
||||
// and repeating it would render as "Webhook: webhook".
|
||||
return {
|
||||
type: triggerDisplayNamesMap[triggerKind],
|
||||
icon: triggerIconMap[triggerKind]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,28 +153,6 @@ export function getTriggerInfo(job: Job): { type: string; icon: any; detail?: st
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets human-readable display name for trigger kinds
|
||||
*/
|
||||
function getTriggerDisplayName(triggerKind: string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
webhook: 'Webhook',
|
||||
http: 'HTTP',
|
||||
websocket: 'WebSocket',
|
||||
postgres: 'PostgreSQL',
|
||||
kafka: 'Kafka',
|
||||
nats: 'NATS',
|
||||
mqtt: 'MQTT',
|
||||
sqs: 'SQS',
|
||||
gcp: 'GCP Pub/Sub',
|
||||
email: 'Email',
|
||||
schedule: 'Schedule',
|
||||
app: 'App',
|
||||
ui: 'UI'
|
||||
}
|
||||
return displayNames[triggerKind] || triggerKind.toUpperCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Field configurations for all possible fields
|
||||
*/
|
||||
@@ -556,6 +533,12 @@ export function getRelevantFields(job: Job): FieldConfig[] {
|
||||
// Always show schedule_path when it exists, regardless of category configuration
|
||||
return job.schedule_path !== null && job.schedule_path !== undefined
|
||||
}
|
||||
if (fieldName === 'trigger_info' && job.trigger_kind) {
|
||||
// A stamped trigger kind is how the run was started, whatever the category — but
|
||||
// only one `getTriggerInfo` can name, or the row renders empty. A schedule is
|
||||
// left out because its own field already says the same thing.
|
||||
return !job.schedule_path && job.trigger_kind in triggerIconMap
|
||||
}
|
||||
return fieldsPresence[fieldName]
|
||||
})
|
||||
.map((fieldName) => fieldConfigs[fieldName])
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
Terminal,
|
||||
Timer,
|
||||
Zap,
|
||||
LayoutDashboard
|
||||
LayoutDashboard,
|
||||
MousePointerClick
|
||||
} from 'lucide-svelte'
|
||||
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
|
||||
import NatsIcon from '$lib/components/icons/NatsIcon.svelte'
|
||||
@@ -101,6 +102,8 @@ export const jobTriggerKinds: JobTriggerKind[] = [
|
||||
'asset',
|
||||
'freshness',
|
||||
'app'
|
||||
// `ui` is deliberately absent: the backend does not stamp it yet, so offering it here
|
||||
// would be a filter that can only ever return nothing.
|
||||
]
|
||||
|
||||
export type Trigger = {
|
||||
@@ -138,13 +141,14 @@ export const triggerIconMap = {
|
||||
nextcloud: NextcloudIcon,
|
||||
google: GoogleIcon,
|
||||
github: GithubIcon,
|
||||
// Job-attribution-only kinds (no trigger CRUD page): the pipeline asset
|
||||
// cascade, the freshness watchdog, and app-component runs. Needed so the Runs
|
||||
// filter and job detail render these trigger kinds instead of a blank label /
|
||||
// no icon.
|
||||
// Job-attribution-only kinds (no trigger CRUD page): the pipeline asset cascade,
|
||||
// the freshness watchdog and app-component runs. Needed so the Runs filter and job
|
||||
// detail render these trigger kinds instead of a blank label / no icon. `ui` is
|
||||
// mapped ahead of the backend stamping it, so the day it does nothing renders blank.
|
||||
asset: Zap,
|
||||
freshness: Timer,
|
||||
app: LayoutDashboard
|
||||
app: LayoutDashboard,
|
||||
ui: MousePointerClick
|
||||
}
|
||||
|
||||
export const triggerDisplayNamesMap = {
|
||||
@@ -169,10 +173,11 @@ export const triggerDisplayNamesMap = {
|
||||
github: 'GitHub',
|
||||
asset: 'Asset cascade',
|
||||
freshness: 'Freshness',
|
||||
app: 'App'
|
||||
// `asset` / `freshness` / `app` are job-attribution-only (JobTriggerKind, not
|
||||
// TriggerType) — hence the union in the satisfies below.
|
||||
} as const satisfies Record<TriggerType | 'asset' | 'freshness' | 'app', string>
|
||||
app: 'App',
|
||||
ui: 'UI'
|
||||
// `asset` / `freshness` / `app` / `ui` are job-attribution-only (JobTriggerKind,
|
||||
// not TriggerType) — hence the union in the satisfies below.
|
||||
} as const satisfies Record<TriggerType | 'asset' | 'freshness' | 'app' | 'ui', string>
|
||||
|
||||
/**
|
||||
* Converts a TriggerType to a CaptureTriggerKind when a mapping exists
|
||||
|
||||
Reference in New Issue
Block a user