feat: add optional labels to scripts, flows, apps, schedules, triggers (#8609)

* feat: add optional labels to scripts, flows, apps, raw apps, schedules, and triggers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update sqlx cache, make labels optional in openapi, regenerate system prompts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add minimal labels input UI to script, flow, and schedule editors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce gap between summary and labels input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to script/flow detail pages and summary/path popover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move labels inside SummaryPathDisplay trigger for clickable area, reduce gap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: display labels inline to the right of summary, not below

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase gap between summary and labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to resources/variables, make labels nullable, add home page label filter badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to workspace export/import, resources, variables + test coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: make migration idempotent, regenerate sqlx cache after merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass labels in script create and flow create/update API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels input UI to resource and variable editors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove negative margin from LabelsInput to prevent overlap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add top and left margin to LabelsInput for better spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce left margin on LabelsInput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: widen label input to w-32

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use inline-flex so LabelsInput doesn't stretch full width

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove flex-wrap so label input stays on same line as badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add label filter presets to resources, variables, and schedules search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use max-w-32 on label input to prevent stretching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pull labels closer to summary with negative top margin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase negative margin to pull labels even closer to summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass labels in schedule create/update API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use COALESCE to preserve existing labels when not provided in schedule/flow update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to CreateResource, EditResource, CreateVariable, EditVariable in OpenAPI spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label badges on resource and variable list pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label badges on schedule and all trigger list pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add folder and label presets to schedules search filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: apply user_folders_only filter on all workspaces including admins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add label presets to resources and variables search filters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: derive folder presets from loaded items, not all workspace folders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add label query parameter to resource and variable list endpoints in OpenAPI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label filter badges inline with folder filters on home page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "feat: display label filter badges inline with folder filters on home page"

This reverts commit 6767a50aa6.

* feat: support comma-separated label filters (allowMultiple) in all list endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: append label presets with comma for allowMultiple filters instead of duplicating key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: hide label presets that are already in the comma-separated filter value

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace unsafe manual SQL ARRAY construction with parameterized queries, add labels to ScriptWDraft

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: complete down migration, add labels to Resource/Variable OpenAPI schemas, remove type cast, add label length validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels field to Schedule test fixture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels field to Rust client struct constructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: regenerate sqlx cache with --all-features for EE builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx cache and package-lock after merge with main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: squash two migrations into one, use IF NOT EXISTS for idempotency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: track label changes in SummaryPathDisplay to enable save button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use JSON string comparison for label dirty tracking in popover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: navigate to script by path after save from popover to load new version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update initialLabels after save so subsequent label changes enable save again

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use onchange callback for label dirty tracking instead of derived comparison

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reload script by path after label save to fetch new version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: propagate script/flow labels to jobs at push time

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: show script/flow labels on runs page, merge with wm_labels for completed jobs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: change job labels type from JSONB to text[], show labels on job detail page, fix type mismatch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to QueuedJob struct, fix get_job queries to return v2_job.labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace +Label text with icon only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add tag icon before labels on job detail page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move tag icon inside badge on job detail page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use blue badge with tag icon in RunBadges, remove duplicate labels from JobDetailHeader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: set icon position to left so tag icon renders in badge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: render Tag icon inline in badge children instead of via icon prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: retry icon prop with small badge and position left

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add hover tooltip showing "Label: X" on job label badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: include v2_job.labels in runs page label filter and broad search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx cache and system prompts after merge with main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to EE JobPayload constructions, regenerate sqlx cache with --all-features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: regenerate sqlx cache CE-only (without EE symlinks that cause conflicts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update remaining wm_labels JSONB queries to use text[] merge expression

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify job labels to just read v2_job.labels (wm_labels already merged at completion)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: consistent label badge spacing with gap-0.5 wrapper and px-0.5 on badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels: None to test utils JobPayload construction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to all test fixture JobPayload/NewFlow/EditApp constructions, regenerate sqlx cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix vertical content shift by fixing container and input height to h-5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: npm_check errors - unused imports, combinedItems order, flow.labels type, badge px-1 padding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused FolderService imports, fix label badge alignment in RunBadges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore deleted service imports in variables page, remove empty loadFolders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI with updated ee-repo-ref

* chore: update ee-repo-ref to merged EE companion PR

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger fresh CI run for updated ee-repo-ref

* fix: match label badge size with other badges in RunBadges using {large} prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove icon from RunBadges label badge to fix vertical alignment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: shorten "Job kind" to "Kind" in run badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add small inline tag icon (10px, -mt-px) to label badge without disrupting height

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add "Label: X" hover tooltip to all label badges, show hidden labels on +N hover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add tag icon and "Label: X" tooltip to home page label filter badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show LabelsInput even when path is hidden in ResourceEditor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels input to new resource creation drawer (AppConnectInner)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* iterate

* fix: add LabelsInput to all resource creation steps in AppConnectInner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce LabelsInput top margin from -mt-3 to -mt-1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase negative margin to -mt-2 for tighter spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: split the difference with -mt-1.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: adjust to -mt-1 for label spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: per-site label spacing via class prop instead of global negative margin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: make label badges clickable to toggle label filter on resources, variables, schedules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use proper array indexOf for label filter toggle, set undefined correctly on removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use delete instead of undefined to properly clear label filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add /labels/list endpoint and autocomplete dropdown to LabelsInput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use inline preventDefault for Svelte 5 event handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add "Create new" option in label autocomplete, regenerate sqlx cache with update_sqlx.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add GIN indexes on labels column for all 16 tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove CONCURRENTLY from GIN index creation in migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add comprehensive label coverage for pull, edit, removal across all item types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify job label filters to only use v2_job.labels, remove wm_labels back-compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add integration tests for job label propagation, display, and filtering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review findings — missing labels in fetch_script_for_update, app rename, escape key bug

- Add `labels` to SELECT in `fetch_script_for_update` to prevent lost labels on script clone
- Pass `labels` in app branch of `moveRenameManager.ts` so app renames preserve labels
- Clear `inputValue` before `adding = false` in LabelsInput escape handler to prevent accidental label add via onblur
- Fix `test_job_label_filter` to complete jobs via SQL (label filtering only works on completed jobs)
- Add `test_wm_labels_from_result_merged_with_static_labels` integration test using Bun

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-04-03 13:39:32 -04:00
committed by GitHub
parent b960598431
commit c4c9ef5fd7
114 changed files with 2643 additions and 349 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO schedule (\n workspace_id, path, schedule, timezone, edited_by, script_path,\n is_flow, args, enabled, email, permissioned_as,\n on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery, on_recovery_times, on_recovery_extra_args,\n on_success, on_success_extra_args,\n ws_error_handler_muted, retry, summary, no_flow_overlap,\n tag, paused_until, cron_version, description, dynamic_skip\n ) VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10, $11,\n $12, $13, $14, $15,\n $16, $17, $18,\n $19, $20,\n $21, $22, $23, $24,\n $25, $26, $27, $28, $29\n )\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n INSERT INTO schedule (\n workspace_id, path, schedule, timezone, edited_by, script_path,\n is_flow, args, enabled, email, permissioned_as,\n on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery, on_recovery_times, on_recovery_extra_args,\n on_success, on_success_extra_args,\n ws_error_handler_muted, retry, summary, no_flow_overlap,\n tag, paused_until, cron_version, description, dynamic_skip, labels\n ) VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10, $11,\n $12, $13, $14, $15,\n $16, $17, $18,\n $19, $20,\n $21, $22, $23, $24,\n $25, $26, $27, $28, $29, $30\n )\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip,\n labels\n ",
"describe": {
"columns": [
{
@@ -162,6 +162,11 @@
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
},
{
"ordinal": 32,
"name": "labels",
"type_info": "TextArray"
}
],
"parameters": {
@@ -194,7 +199,8 @@
"Timestamptz",
"Text",
"Text",
"Varchar"
"Varchar",
"TextArray"
]
},
"nullable": [
@@ -229,8 +235,9 @@
true,
true,
true,
true,
true
]
},
"hash": "dd20f94d560238096390371c98ded1f80825a11cd61c0bb431678ad9ab4a138e"
"hash": "2effe885aafaa0c5fc1f969b8c6ffbd38c53fb586251ba780b75a007e5a47ed6"
}
@@ -1,19 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, created_by, edited_at)\n VALUES ($1, $2, $3, $4, $5, $6, now()) ON CONFLICT (workspace_id, path)\n DO UPDATE SET value = EXCLUDED.value, description = EXCLUDED.description, resource_type = EXCLUDED.resource_type, edited_at = now()",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "39426bd3018b390ea2073419884cf6cb506c75e84c65438c9026831eb10d340b"
}
@@ -45,31 +45,36 @@
},
{
"ordinal": 8,
"name": "labels",
"type_info": "TextArray"
},
{
"ordinal": 9,
"name": "is_expired",
"type_info": "Bool"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "is_refreshed",
"type_info": "Bool"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "refresh_error",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "is_linked",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_oauth?",
"type_info": "Bool"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "account",
"type_info": "Int4"
}
@@ -89,6 +94,7 @@
false,
true,
true,
true,
null,
null,
true,
@@ -42,6 +42,11 @@
"ordinal": 7,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "labels",
"type_info": "TextArray"
}
],
"parameters": {
@@ -57,6 +62,7 @@
false,
false,
true,
true,
true
]
},
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth, expires_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth, expires_at, labels)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
"describe": {
"columns": [],
"parameters": {
@@ -12,10 +12,11 @@
"Varchar",
"Int4",
"Bool",
"Timestamptz"
"Timestamptz",
"TextArray"
]
},
"nullable": []
},
"hash": "2be66f23536223549db9b50025932b6b1bad90b8fa47d97acb7d75aa3c37ef86"
"hash": "48efd8f89df9c1dd8f8a4eb2b2640fcb380f2470c8488371b207a42cb7d4cd26"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE variable SET labels = $1 WHERE path = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"TextArray",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "4c333861e736b8138162f0ff3bf2844c71b95c75bd8f173eac51336ab176ccc0"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script\n (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, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "51f09f073842a6990535b887d8267fab305c21e4d7703bedbadf405b5c2d7582"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17 \n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed -- 27,\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17 \n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed, -- 27,\n labels -- 44\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27, $44)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
"describe": {
"columns": [],
"parameters": {
@@ -132,10 +132,11 @@
"Bool",
"Varchar",
"Bool",
"Int8"
"Int8",
"TextArray"
]
},
"nullable": []
},
"hash": "14276a040cb4db88d71fccdc3579e8c0bb132b70668301b535872d1632753e30"
"hash": "5425e2c5e29fc5145dde5ea53d5307ca90d8dd76da2ca560b310b12820be2576"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE schedule SET\n enabled = $1,\n email = $2\n WHERE path = $3 AND workspace_id = $4\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n UPDATE schedule SET\n enabled = $1,\n email = $2\n WHERE path = $3 AND workspace_id = $4\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip,\n labels\n ",
"describe": {
"columns": [
{
@@ -162,6 +162,11 @@
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
},
{
"ordinal": 32,
"name": "labels",
"type_info": "TextArray"
}
],
"parameters": {
@@ -204,8 +209,9 @@
true,
true,
true,
true,
true
]
},
"hash": "ad96768ff61fab1cfb9421683bb13b64f5f157fafe114d50fb910dc36ebe0f91"
"hash": "642ca096c1c151fc01445593c8d2ed879a9a7be6ff044a9ce1b581ba725665fe"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)\n SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow\n WHERE path = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "676c758d9c4492dada50edd3ad06dc628e2ba56eab5e1a50c99481da9793759e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return,\n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,\n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,\n flow.tag,\n flow.dedicated_worker,\n flow.on_behalf_of_email,\n flow.edited_by\n FROM\n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE\n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return,\n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,\n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,\n flow.tag,\n flow.dedicated_worker,\n flow.on_behalf_of_email,\n flow.edited_by,\n flow.labels\n FROM\n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE\n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
"describe": {
"columns": [
{
@@ -42,6 +42,11 @@
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "labels",
"type_info": "TextArray"
}
],
"parameters": {
@@ -59,8 +64,9 @@
true,
true,
true,
false
false,
true
]
},
"hash": "209dc4c1b91eeab1c12ffcd9f9e16f315c689ca772c736b333dcdf07c8086087"
"hash": "6d992a933bb878733b7afd7a4295b9ad6f5276b60ce20e0378d6148976e02777"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, runnable_settings_handle, language as \"language: ScriptLang\", dedicated_worker, priority, timeout, on_behalf_of_email, created_by FROM script\n WHERE path = $1 AND workspace_id = $2 AND archived = false AND (lock IS NOT NULL OR $3 = false)\n ORDER BY created_at DESC LIMIT 1",
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, runnable_settings_handle, language as \"language: ScriptLang\", dedicated_worker, priority, timeout, on_behalf_of_email, created_by, labels FROM script\n WHERE path = $1 AND workspace_id = $2 AND archived = false AND (lock IS NOT NULL OR $3 = false)\n ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
@@ -114,6 +114,11 @@
"ordinal": 15,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "labels",
"type_info": "TextArray"
}
],
"parameters": {
@@ -139,8 +144,9 @@
true,
true,
true,
false
false,
true
]
},
"hash": "a33673ebc4d1eb4c3513987dbc43e2c80974598e1d9fe7203145bfc29928ba65"
"hash": "71767e6684957af5dff56a1bb64f980f712f92798d11feaeb0832962ad9ddb2e"
}
@@ -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, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules) 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, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39)",
"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, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules, labels) 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, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40)",
"describe": {
"columns": [],
"parameters": {
@@ -89,10 +89,11 @@
"Int4",
"Bool",
"Int8",
"Jsonb"
"Jsonb",
"TextArray"
]
},
"nullable": []
},
"hash": "4d983f1e3e63a1a70edf5d867d9f23f2069a7a4ba1dcc1331ecccdf1c6a95cb8"
"hash": "790d79ec7abe6ebe1092afd9de4c5fc383272d2057ac5b47a7425f095f4e8788"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE http_trigger SET labels = $1 WHERE workspace_id = $2 AND path = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"TextArray",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "86c0ef92652085b93979a8836bedb33a366a84426a115c75f58dcc43ab91705e"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script\n (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, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules, labels)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules, labels\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "a969194571dd3f12e628ce0f01b0ddc09bbcf4506eff2290664c295d16fec4ae"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app SET labels = $1 WHERE path = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"TextArray",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "b657b718ec381fbb1d41841c88118b88c4d7ea8f0cde663ed8f8226faf75b0f6"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow (\n workspace_id, path, summary, description,\n dependency_job, lock_error_logs, draft_only, tag,\n dedicated_worker, visible_to_runner_only, on_behalf_of_email,\n ws_error_handler_muted,\n value, schema, edited_by, edited_at\n ) VALUES (\n $1, $2, $3, $4,\n NULL, '', $5, $6,\n $7, $8, $9,\n $10,\n $11, $12::text::json, $13, now()\n )",
"query": "INSERT INTO flow (\n workspace_id, path, summary, description,\n dependency_job, lock_error_logs, draft_only, tag,\n dedicated_worker, visible_to_runner_only, on_behalf_of_email,\n ws_error_handler_muted,\n value, schema, edited_by, edited_at, labels\n ) VALUES (\n $1, $2, $3, $4,\n NULL, '', $5, $6,\n $7, $8, $9,\n $10,\n $11, $12::text::json, $13, now(), $14\n )",
"describe": {
"columns": [],
"parameters": {
@@ -17,10 +17,11 @@
"Bool",
"Jsonb",
"Text",
"Varchar"
"Varchar",
"TextArray"
]
},
"nullable": []
},
"hash": "6bde827da007b470b9d0acccfc3e00ce6aac650b9138a236f34c614eed753849"
"hash": "b6f95b3fd1d0431d96d0409424dbb20cb3020f93454978d078082482b86cbebe"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT DISTINCT unnest(labels) as \"label!\" FROM (\n SELECT labels FROM script WHERE workspace_id = $1 AND labels IS NOT NULL\n UNION ALL SELECT labels FROM flow WHERE workspace_id = $1 AND labels IS NOT NULL\n UNION ALL SELECT labels FROM resource WHERE workspace_id = $1 AND labels IS NOT NULL\n UNION ALL SELECT labels FROM variable WHERE workspace_id = $1 AND labels IS NOT NULL\n UNION ALL SELECT labels FROM schedule WHERE workspace_id = $1 AND labels IS NOT NULL\n UNION ALL SELECT labels FROM app WHERE workspace_id = $1 AND labels IS NOT NULL\n ) t ORDER BY 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "label!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "b782179261b4054289a1a595f6e06ee2035ae9811bc49abce903a04439baf800"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23,\n email = $24,\n edited_by = $25,\n permissioned_as = $26\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23,\n email = $24,\n edited_by = $25,\n permissioned_as = $26,\n labels = COALESCE($27, labels)\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip,\n labels\n ",
"describe": {
"columns": [
{
@@ -162,6 +162,11 @@
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
},
{
"ordinal": 32,
"name": "labels",
"type_info": "TextArray"
}
],
"parameters": {
@@ -191,7 +196,8 @@
"Varchar",
"Varchar",
"Varchar",
"Varchar"
"Varchar",
"TextArray"
]
},
"nullable": [
@@ -226,8 +232,9 @@
true,
true,
true,
true,
true
]
},
"hash": "54b4c762add9b1ebfdb2a6d5abd6d20e86dc0e6544f0bb22fa4ec68aa54a4dc8"
"hash": "bd2609f999054c5a2a85f2aafe4005bc22731c27f0bf27b3b06ec426a05eda2e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n flow\n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n ws_error_handler_muted = $8,\n value = $9,\n schema = $10::text::json,\n edited_by = $11,\n edited_at = now()\n WHERE\n path = $12 AND workspace_id = $13",
"query": "\n UPDATE\n flow\n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n ws_error_handler_muted = $8,\n value = $9,\n schema = $10::text::json,\n edited_by = $11,\n edited_at = now(),\n labels = COALESCE($14, labels)\n WHERE\n path = $12 AND workspace_id = $13",
"describe": {
"columns": [],
"parameters": {
@@ -17,10 +17,11 @@
"Text",
"Varchar",
"Text",
"Text"
"Text",
"TextArray"
]
},
"nullable": []
},
"hash": "77ac7257be02fb04c4b3213e2221e6f60621b4b2909d770de744ef5671e12ed9"
"hash": "c06796e8647cf278c6e0809562aaaa872403890eae3e606f379343671c2fff02"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels)\n SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels\n FROM flow\n WHERE path = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ddda19024473b4e5b1d450bf56c667056f6b3696dea7d73758e53f825bfce13e"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, created_by, edited_at, labels)\n VALUES ($1, $2, $3, $4, $5, $6, now(), $7) ON CONFLICT (workspace_id, path)\n DO UPDATE SET value = EXCLUDED.value, description = EXCLUDED.description, resource_type = EXCLUDED.resource_type, edited_at = now(), labels = EXCLUDED.labels",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar",
"Varchar",
"TextArray"
]
},
"nullable": []
},
"hash": "deac41298e8b0d0870e314fef0813c24dd55d63bda78a0a5f35ed6f22bea6bef"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app\n (workspace_id, path, summary, policy, versions, draft_only, custom_path)\n VALUES ($1, $2, $3, $4, '{}', $5, $6) RETURNING id",
"query": "INSERT INTO app\n (workspace_id, path, summary, policy, versions, draft_only, custom_path, labels)\n VALUES ($1, $2, $3, $4, '{}', $5, $6, $7) RETURNING id",
"describe": {
"columns": [
{
@@ -16,12 +16,13 @@
"Varchar",
"Jsonb",
"Bool",
"Text"
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "6b53f7c4bb73177316d6134698f3979f51b53dcd4d8ec50d312c9e7fe31ad5f5"
"hash": "e4836a1ee97e4723bddc28b94e23a54b6aa1f458f3ecca8f35432153e54b143d"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE resource SET labels = $1 WHERE path = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"TextArray",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "f6f850c419771b193f506ceca63de805cbe6bdee314ca40d912010db307bd13e"
}
+1 -1
View File
@@ -1 +1 @@
41b0d1cb312919109407640fc4bd7060cfe0e107
b30d25cf5f7ca7f94509fe9a732e22ab2186f929
@@ -0,0 +1,16 @@
ALTER TABLE script DROP COLUMN IF EXISTS labels;
ALTER TABLE flow DROP COLUMN IF EXISTS labels;
ALTER TABLE app DROP COLUMN IF EXISTS labels;
ALTER TABLE raw_app DROP COLUMN IF EXISTS labels;
ALTER TABLE schedule DROP COLUMN IF EXISTS labels;
ALTER TABLE http_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE websocket_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE kafka_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE nats_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE mqtt_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE postgres_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE sqs_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE gcp_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE email_trigger DROP COLUMN IF EXISTS labels;
ALTER TABLE resource DROP COLUMN IF EXISTS labels;
ALTER TABLE variable DROP COLUMN IF EXISTS labels;
@@ -0,0 +1,33 @@
ALTER TABLE script ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE flow ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE app ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE raw_app ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE schedule ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE http_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE websocket_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE kafka_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE nats_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE mqtt_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE postgres_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE sqs_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE gcp_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE email_trigger ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE resource ADD COLUMN IF NOT EXISTS labels text[];
ALTER TABLE variable ADD COLUMN IF NOT EXISTS labels text[];
CREATE INDEX IF NOT EXISTS idx_script_labels ON script USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_flow_labels ON flow USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_app_labels ON app USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_raw_app_labels ON raw_app USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_schedule_labels ON schedule USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_resource_labels ON resource USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_variable_labels ON variable USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_http_trigger_labels ON http_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_websocket_trigger_labels ON websocket_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_kafka_trigger_labels ON kafka_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_nats_trigger_labels ON nats_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_mqtt_trigger_labels ON mqtt_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_postgres_trigger_labels ON postgres_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_sqs_trigger_labels ON sqs_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_gcp_trigger_labels ON gcp_trigger USING GIN (labels) WHERE labels IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_email_trigger_labels ON email_trigger USING GIN (labels) WHERE labels IS NOT NULL;
+16 -16
View File
@@ -38,7 +38,7 @@ account: workspace_id(char), id(int), expires_at(ts), refresh_token(char), clien
agent_token_blacklist: token(char), expires_at(ts), blacklisted_at(ts), blacklisted_by(char)
ai_agent_memory: workspace_id(char), conversation_id(uuid), step_id(char), messages(jsonb), created_at(ts), updated_at(ts)
alerts: id(int), alert_type(char), message(text), created_at(ts), acknowledged(bool), workspace_id(text), acknowledged_workspace(bool), resource(text)
app: id(bigint), workspace_id(char), path(char), summary(char), policy(jsonb), versions(bigint[]), extra_perms(jsonb), draft_only(bool), custom_path(text)
app: id(bigint), workspace_id(char), path(char), summary(char), policy(jsonb), versions(bigint[]), extra_perms(jsonb), draft_only(bool), custom_path(text), labels(text[])
FK: (workspace_id) -> workspace(id)
app_bundles: app_version_id(bigint), w_id(char), file_type(char), data(bytes)
app_script: id(bigint), app(bigint), hash(char(64)), lock(text), code(text), code_sha256(char(64))
@@ -72,9 +72,9 @@ deployment_metadata: workspace_id(char), path(char), script_hash(bigint), app_ve
draft: workspace_id(char), path(char), typ(draft_type), value(json), created_at(ts)
FK: (workspace_id) -> workspace(id)
email_to_igroup: email(char), igroup(char)
email_trigger: path(char), local_part(char), workspaced_local_part(bool), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode)
email_trigger: path(char), local_part(char), workspaced_local_part(bool), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), labels(text[])
favorite: usr(char), workspace_id(char), path(char), favorite_kind(favorite_kind)
flow: workspace_id(char), path(char), summary(text), description(text), value(jsonb), edited_by(char), edited_at(ts), archived(bool), schema(json), extra_perms(jsonb), dependency_job(uuid), draft_only(bool), tag(char), ws_error_handler_muted(bool), dedicated_worker(bool), timeout(int), visible_to_runner_only(bool), concurrency_key(char), versions(bigint[]), on_behalf_of_email(text), lock_error_logs(text)
flow: workspace_id(char), path(char), summary(text), description(text), value(jsonb), edited_by(char), edited_at(ts), archived(bool), schema(json), extra_perms(jsonb), dependency_job(uuid), draft_only(bool), tag(char), ws_error_handler_muted(bool), dedicated_worker(bool), timeout(int), visible_to_runner_only(bool), concurrency_key(char), versions(bigint[]), on_behalf_of_email(text), lock_error_logs(text), labels(text[])
FK: (workspace_id) -> workspace(id)
flow_conversation: id(uuid), workspace_id(char), flow_path(char), title(char), created_at(ts), updated_at(ts), created_by(char)
FK: (workspace_id) -> workspace(id)
@@ -91,14 +91,14 @@ folder: name(char), workspace_id(char), display_name(char), owners(char), extra_
FK: (workspace_id) -> workspace(id)
folder_permission_history: id(bigint), workspace_id(char), folder_name(char), changed_by(char), changed_at(ts), change_type(char), affected(char)
FK: (workspace_id, folder_name) -> folder(workspace_id, name)
gcp_trigger: gcp_resource_path(char), topic_id(char), subscription_id(char), delivery_type(delivery_mode), delivery_config(jsonb), 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), subscription_mode(gcp_subscription_mode), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), auto_acknowledge_msg(bool), ack_deadline(int), mode(trigger_mode)
gcp_trigger: gcp_resource_path(char), topic_id(char), subscription_id(char), delivery_type(delivery_mode), delivery_config(jsonb), 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), subscription_mode(gcp_subscription_mode), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), auto_acknowledge_msg(bool), ack_deadline(int), mode(trigger_mode), labels(text[])
global_settings: name(char), value(jsonb), updated_at(ts)
group_: workspace_id(char), name(char), summary(text), extra_perms(jsonb)
FK: (workspace_id) -> workspace(id)
group_permission_history: id(bigint), workspace_id(char), group_name(char), changed_by(char), changed_at(ts), change_type(char), member_affected(char)
FK: (workspace_id, group_name) -> group_(workspace_id, name)
healthchecks: id(bigint), check_type(char), healthy(bool), created_at(ts)
http_trigger: path(char), route_path(char), route_path_key(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), authentication_method(authentication_method), http_method(http_method), static_asset_config(jsonb), is_static_website(bool), workspaced_route(bool), wrap_body(bool), raw_string(bool), authentication_resource_path(char), summary(char), description(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), request_type(request_type), mode(trigger_mode)
http_trigger: path(char), route_path(char), route_path_key(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), authentication_method(authentication_method), http_method(http_method), static_asset_config(jsonb), is_static_website(bool), workspaced_route(bool), wrap_body(bool), raw_string(bool), authentication_resource_path(char), summary(char), description(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), request_type(request_type), mode(trigger_mode), labels(text[])
input: id(uuid), workspace_id(char), runnable_id(char), runnable_type(runnable_type), name(text), args(jsonb), created_at(ts), created_by(char), is_public(bool)
FK: (workspace_id) -> workspace(id)
instance_group: name(char), summary(char), id(char), scim_display_name(char), external_id(char)
@@ -111,7 +111,7 @@ job_stats: workspace_id(char), job_id(uuid), metric_id(char), metric_name(char),
FK: (workspace_id) -> workspace(id)
kafka_pending_commits: id(bigint), workspace_id(char), kafka_trigger_path(char), topic(char), partition(int), offset(bigint), created_at(ts)
FK: (workspace_id, kafka_trigger_path) -> kafka_trigger(workspace_id, path)
kafka_trigger: path(char), kafka_resource_path(char), topics(char), group_id(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), filters(jsonb[]), auto_commit(bool)
kafka_trigger: path(char), kafka_resource_path(char), topics(char), group_id(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), filters(jsonb[]), auto_commit(bool), labels(text[])
log_file: hostname(char), log_ts(ts), ok_lines(bigint), err_lines(bigint), mode(log_mode), worker_group(char), file_path(char), json_fmt(bool)
magic_link: email(char), token(char), expiration(ts)
mcp_oauth_client: mcp_server_url(text), client_id(text), client_secret(text), client_secret_expires_at(ts), token_endpoint(text), created_at(ts)
@@ -121,10 +121,10 @@ mcp_oauth_server_client: client_id(char), client_name(char), redirect_uris(text[
mcp_oauth_server_code: code(char), client_id(char), user_email(char), workspace_id(char), scopes(text[]), redirect_uri(text), code_challenge(char), code_challenge_method(char), created_at(ts), expires_at(ts)
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)
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)
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)
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)
notify_event: id(bigint), channel(text), payload(text), created_at(ts)
otel_traces: trace_id(bytes), span_id(bytes), trace_state(text), parent_span_id(bytes), flags(int), name(text), kind(int), start_time_unix_nano(bigint), end_time_unix_nano(bigint), attributes(jsonb), dropped_attributes_count(int), events(jsonb), dropped_events_count(int), links(jsonb), dropped_links_count(int), status(jsonb)
@@ -133,23 +133,23 @@ parallel_monitor_lock: parent_flow_id(uuid), job_id(uuid), last_ping(ts)
password: email(char), password_hash(char), login_type(char), super_admin(bool), verified(bool), name(char), company(char), first_time_user(bool), username(char), devops(bool)
pending_user: email(char), created_at(ts), username(char)
pip_resolution_cache: hash(char), expiration(ts), lockfile(text)
postgres_trigger: path(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), postgres_resource_path(char), error(text), server_id(char), last_server_ping(ts), replication_slot_name(char), publication_name(char), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode)
postgres_trigger: path(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), postgres_resource_path(char), error(text), server_id(char), last_server_ping(ts), replication_slot_name(char), publication_name(char), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), labels(text[])
FK: (workspace_id) -> workspace(id)
raw_app: path(char), version(int), workspace_id(char), summary(char), edited_at(ts), data(text), extra_perms(jsonb)
raw_app: path(char), version(int), workspace_id(char), summary(char), edited_at(ts), data(text), extra_perms(jsonb), labels(text[])
FK: (workspace_id) -> workspace(id)
resource: workspace_id(char), path(char), value(jsonb), description(text), resource_type(char), extra_perms(jsonb), edited_at(ts), created_by(char)
resource: workspace_id(char), path(char), value(jsonb), description(text), resource_type(char), extra_perms(jsonb), edited_at(ts), created_by(char), labels(text[])
FK: (workspace_id) -> workspace(id)
resource_type: workspace_id(char), name(char), schema(jsonb), description(text), edited_at(ts), created_by(char), format_extension(char), is_fileset(bool)
FK: (workspace_id) -> workspace(id)
resume_job: id(uuid), job(uuid), flow(uuid), created_at(ts), value(jsonb), approver(char), resume_id(int), approved(bool)
FK: (flow) -> v2_job_queue(id)
runnable_settings: hash(bigint), debouncing_settings(bigint), concurrency_settings(bigint)
schedule: workspace_id(char), path(char), edited_by(char), edited_at(ts), schedule(char), enabled(bool), script_path(char), args(jsonb), extra_perms(jsonb), is_flow(bool), email(char), error(text), timezone(char), on_failure(char), on_recovery(char), on_failure_times(int), on_failure_exact(bool), on_failure_extra_args(jsonb), on_recovery_times(int), on_recovery_extra_args(jsonb), ws_error_handler_muted(bool), retry(jsonb), summary(char), no_flow_overlap(bool), tag(char), paused_until(ts), on_success(char), on_success_extra_args(jsonb), cron_version(text), description(text), dynamic_skip(char)
schedule: workspace_id(char), path(char), edited_by(char), edited_at(ts), schedule(char), enabled(bool), script_path(char), args(jsonb), extra_perms(jsonb), is_flow(bool), email(char), error(text), timezone(char), on_failure(char), on_recovery(char), on_failure_times(int), on_failure_exact(bool), on_failure_extra_args(jsonb), on_recovery_times(int), on_recovery_extra_args(jsonb), ws_error_handler_muted(bool), retry(jsonb), summary(char), no_flow_overlap(bool), tag(char), paused_until(ts), on_success(char), on_success_extra_args(jsonb), cron_version(text), description(text), dynamic_skip(char), labels(text[])
FK: (workspace_id) -> workspace(id)
script: workspace_id(char), hash(bigint), path(char), parent_hashes(bigint[]), summary(text), description(text), content(text), created_by(char), created_at(ts), archived(bool), schema(json), deleted(bool), is_template(bool), extra_perms(jsonb), lock(text), lock_error_logs(text), language(script_lang), kind(script_kind), tag(char), draft_only(bool), envs(char), concurrent_limit(int), concurrency_time_window_s(int), cache_ttl(int), dedicated_worker(bool), ws_error_handler_muted(bool), priority(smallint), timeout(int), delete_after_use(bool), restart_unless_cancelled(bool), concurrency_key(char), visible_to_runner_only(bool), auto_kind(varchar), codebase(char), has_preprocessor(bool), on_behalf_of_email(text), schema_validation(bool), assets(jsonb), debounce_key(char), debounce_delay_s(int), cache_ignore_s3_path(bool), runnable_settings_handle(bigint)
script: workspace_id(char), hash(bigint), path(char), parent_hashes(bigint[]), summary(text), description(text), content(text), created_by(char), created_at(ts), archived(bool), schema(json), deleted(bool), is_template(bool), extra_perms(jsonb), lock(text), lock_error_logs(text), language(script_lang), kind(script_kind), tag(char), draft_only(bool), envs(char), concurrent_limit(int), concurrency_time_window_s(int), cache_ttl(int), dedicated_worker(bool), ws_error_handler_muted(bool), priority(smallint), timeout(int), delete_after_use(bool), restart_unless_cancelled(bool), concurrency_key(char), visible_to_runner_only(bool), auto_kind(varchar), codebase(char), has_preprocessor(bool), on_behalf_of_email(text), schema_validation(bool), assets(jsonb), debounce_key(char), debounce_delay_s(int), cache_ignore_s3_path(bool), runnable_settings_handle(bigint), labels(text[])
FK: (workspace_id) -> workspace(id)
skip_workspace_diff_tally: workspace_id(char), added_at(ts)
sqs_trigger: path(char), queue_url(char), aws_resource_path(char), message_attributes(text[]), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), error(text), server_id(char), last_server_ping(ts), aws_auth_resource_type(aws_auth_resource_type), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode)
sqs_trigger: path(char), queue_url(char), aws_resource_path(char), message_attributes(text[]), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), error(text), server_id(char), last_server_ping(ts), aws_auth_resource_type(aws_auth_resource_type), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), labels(text[])
FK: (workspace_id) -> workspace(id)
trashbin: id(bigint), workspace_id(char), item_kind(char), item_path(char), item_data(jsonb), deleted_by(char), deleted_at(ts), expires_at(ts)
FK: (workspace_id) -> workspace(id)
@@ -173,9 +173,9 @@ v2_job_runtime: id(uuid), ping(ts), memory_peak(int)
FK: (id) -> v2_job_queue(id)
v2_job_status: id(uuid), flow_status(jsonb), flow_leaf_jobs(jsonb), workflow_as_code_status(jsonb)
FK: (id) -> v2_job_queue(id)
variable: workspace_id(char), path(char), value(char), is_secret(bool), description(char), extra_perms(jsonb), account(int), is_oauth(bool), expires_at(ts)
variable: workspace_id(char), path(char), value(char), is_secret(bool), description(char), extra_perms(jsonb), account(int), is_oauth(bool), expires_at(ts), labels(text[])
FK: (workspace_id) -> workspace(id)
websocket_trigger: path(char), url(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), filters(jsonb[]), initial_messages(jsonb[]), url_runnable_args(jsonb), can_return_message(bool), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), can_return_error_result(bool), mode(trigger_mode)
websocket_trigger: path(char), url(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), filters(jsonb[]), initial_messages(jsonb[]), url_runnable_args(jsonb), can_return_message(bool), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), can_return_error_result(bool), mode(trigger_mode), labels(text[])
windmill_migrations: name(text), created_at(ts)
worker_group_job_stats: hour(bigint), worker_group(text), script_lang(char), workspace_id(char), job_count(int), total_duration_ms(bigint)
FK: (workspace_id) -> workspace(id)
+2
View File
@@ -450,6 +450,7 @@ def main():
on_behalf_of_email: None,
preserve_on_behalf_of: None,
ws_error_handler_muted: None,
labels: None,
})
.send()
.await
@@ -511,6 +512,7 @@ def main():
deployment_message: None,
custom_path: None,
preserve_on_behalf_of: None,
labels: None,
})
.send()
.await
+3 -3
View File
@@ -162,7 +162,7 @@ export async function main(path: string, email: string, job_id: string, is_flow:
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
debouncing_settings: DebouncingSettings::default(), labels: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
@@ -285,7 +285,7 @@ async fn test_error_handler_muted_on_script(db: Pool<Postgres>) -> anyhow::Resul
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
debouncing_settings: DebouncingSettings::default(), labels: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
@@ -380,7 +380,7 @@ async fn test_error_handler_not_triggered_on_success(db: Pool<Postgres>) -> anyh
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
debouncing_settings: DebouncingSettings::default(), labels: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
+10 -10
View File
@@ -49,7 +49,7 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -89,7 +89,7 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
})
.run_until_complete_with(db, false, port, |id| async move {
let job = sqlx::query!("SELECT preprocessed FROM v2_job WHERE id = $1", id)
@@ -424,7 +424,7 @@ mod job_payload {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: false,
version: 1443253234253454,
version: 1443253234253454, labels: None,
})
.run_until_complete(&db, false, port)
.await
@@ -473,7 +473,7 @@ mod job_payload {
path: "f/system/hello_with_preprocessor".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253456,
version: 1443253234253456, labels: None,
})
.run_until_complete_with(db, false, port, |id| async move {
let job = sqlx::query!("SELECT preprocessed FROM v2_job WHERE id = $1", id)
@@ -543,7 +543,7 @@ mod job_payload {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253454,
version: 1443253234253454, labels: None,
})
.run_until_complete(&db, false, port)
.await
@@ -789,7 +789,7 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -839,7 +839,7 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -889,7 +889,7 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -939,7 +939,7 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -991,7 +991,7 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
+311 -41
View File
@@ -64,8 +64,8 @@ async fn test_list_jobs_without_include_args(db: Pool<Postgres>) -> anyhow::Resu
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -121,8 +121,8 @@ async fn test_list_jobs_with_include_args(db: Pool<Postgres>) -> anyhow::Result<
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -192,8 +192,8 @@ async fn test_list_jobs_completed_with_include_args(db: Pool<Postgres>) -> anyho
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -265,8 +265,8 @@ async fn test_list_jobs_mixed_queue_and_completed(db: Pool<Postgres>) -> anyhow:
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -286,8 +286,8 @@ async fn test_list_jobs_mixed_queue_and_completed(db: Pool<Postgres>) -> anyhow:
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -349,7 +349,9 @@ async fn test_list_jobs_mixed_queue_and_completed(db: Pool<Postgres>) -> anyhow:
/// Test list_jobs with multiple queued jobs and include_args
#[sqlx::test(fixtures("base"))]
async fn test_list_jobs_multiple_queued_with_include_args(db: Pool<Postgres>) -> anyhow::Result<()> {
async fn test_list_jobs_multiple_queued_with_include_args(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
@@ -369,8 +371,8 @@ async fn test_list_jobs_multiple_queued_with_include_args(db: Pool<Postgres>) ->
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -387,8 +389,8 @@ async fn test_list_jobs_multiple_queued_with_include_args(db: Pool<Postgres>) ->
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -411,9 +413,13 @@ async fn test_list_jobs_multiple_queued_with_include_args(db: Pool<Postgres>) ->
let jobs: Vec<ListJobsResponse> = response.json().await?;
// Find both jobs
let job1 = jobs.iter().find(|j| j.id == job1_id.to_string())
let job1 = jobs
.iter()
.find(|j| j.id == job1_id.to_string())
.expect("should find job1");
let job2 = jobs.iter().find(|j| j.id == job2_id.to_string())
let job2 = jobs
.iter()
.find(|j| j.id == job2_id.to_string())
.expect("should find job2");
// Both should have args
@@ -461,8 +467,8 @@ async fn test_queue_list_without_include_args(db: Pool<Postgres>) -> anyhow::Res
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -480,10 +486,7 @@ async fn test_queue_list_without_include_args(db: Pool<Postgres>) -> anyhow::Res
.send()
.await?;
assert!(
response.status().is_success(),
"queue/list should succeed"
);
assert!(response.status().is_success(), "queue/list should succeed");
let jobs: Vec<QueueJobResponse> = response.json().await?;
@@ -524,8 +527,8 @@ async fn test_queue_list_with_include_args(db: Pool<Postgres>) -> anyhow::Result
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -594,8 +597,8 @@ async fn test_queue_list_multiple_jobs_with_include_args(db: Pool<Postgres>) ->
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -612,8 +615,8 @@ async fn test_queue_list_multiple_jobs_with_include_args(db: Pool<Postgres>) ->
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -631,10 +634,7 @@ async fn test_queue_list_multiple_jobs_with_include_args(db: Pool<Postgres>) ->
.send()
.await?;
assert!(
response.status().is_success(),
"queue/list should succeed"
);
assert!(response.status().is_success(), "queue/list should succeed");
let jobs: Vec<QueueJobResponse> = response.json().await?;
@@ -694,8 +694,8 @@ async fn test_completed_list_without_include_args(db: Pool<Postgres>) -> anyhow:
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -760,8 +760,8 @@ async fn test_completed_list_with_include_args(db: Pool<Postgres>) -> anyhow::Re
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -835,8 +835,8 @@ async fn test_completed_list_multiple_jobs_with_include_args(
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -856,8 +856,8 @@ async fn test_completed_list_multiple_jobs_with_include_args(
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
@@ -912,3 +912,273 @@ async fn test_completed_list_multiple_jobs_with_include_args(
Ok(())
}
// =============================================================================
// Labels integration tests
// =============================================================================
#[sqlx::test(fixtures("base"))]
async fn test_job_labels_propagated_at_push_time(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let client = windmill_api_client::create_client(
&format!("http://localhost:{port}"),
"SECRET_TOKEN".to_string(),
);
// Push a script job with labels
let job_id = RunJob::from(JobPayload::ScriptHash {
hash: windmill_common::scripts::ScriptHash(0),
path: "u/admin/test_labels_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Python3,
priority: None,
apply_preprocessor: false,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
labels: Some(vec!["prod".to_string(), "deploy".to_string()]),
})
.push(&db)
.await;
// Verify labels in queue listing
let response = client
.client()
.get(format!("{}/w/test-workspace/jobs/list", client.baseurl()))
.send()
.await?;
assert!(response.status().is_success());
let jobs: Vec<serde_json::Value> = response.json().await?;
let job = jobs
.iter()
.find(|j| j["id"].as_str() == Some(&job_id.to_string()))
.expect("should find job");
let labels = job["labels"].as_array().expect("labels should be an array");
assert!(labels.contains(&json!("prod")));
assert!(labels.contains(&json!("deploy")));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_job_without_labels_has_no_labels_field(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let client = windmill_api_client::create_client(
&format!("http://localhost:{port}"),
"SECRET_TOKEN".to_string(),
);
// Push a job without labels
let job_id = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content: "def main(): return 1".to_string(),
path: None,
language: ScriptLang::Python3,
lock: None,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.push(&db)
.await;
let response = client
.client()
.get(format!("{}/w/test-workspace/jobs/list", client.baseurl()))
.send()
.await?;
assert!(response.status().is_success());
let jobs: Vec<serde_json::Value> = response.json().await?;
let job = jobs
.iter()
.find(|j| j["id"].as_str() == Some(&job_id.to_string()))
.expect("should find job");
// Labels should be null/missing for jobs without labels
assert!(
job.get("labels").is_none() || job["labels"].is_null(),
"job without labels should not have labels field"
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_job_label_filter(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let client = windmill_api_client::create_client(
&format!("http://localhost:{port}"),
"SECRET_TOKEN".to_string(),
);
// Push two jobs with different labels, then complete them via SQL
// Label filtering only works on completed jobs
let job_prod_id = RunJob::from(JobPayload::ScriptHash {
hash: windmill_common::scripts::ScriptHash(0),
path: "u/admin/prod_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Bun,
priority: None,
apply_preprocessor: false,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
labels: Some(vec!["prod".to_string()]),
})
.push(&db)
.await;
let job_staging_id = RunJob::from(JobPayload::ScriptHash {
hash: windmill_common::scripts::ScriptHash(0),
path: "u/admin/staging_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Bun,
priority: None,
apply_preprocessor: false,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
labels: Some(vec!["staging".to_string()]),
})
.push(&db)
.await;
// Complete both jobs directly via SQL so label filter can find them
for job_id in &[job_prod_id, job_staging_id] {
sqlx::query(
"INSERT INTO v2_job_completed (workspace_id, id, result, status, duration_ms) VALUES ('test-workspace', $1, '{}'::jsonb, 'success', 0)",
)
.bind(job_id)
.execute(&db)
.await?;
}
// Filter by label=prod
let response = client
.client()
.get(format!(
"{}/w/test-workspace/jobs/list?label=prod",
client.baseurl()
))
.send()
.await?;
assert!(response.status().is_success());
let jobs: Vec<serde_json::Value> = response.json().await?;
// Should find the prod job
assert!(
jobs.iter()
.any(|j| j["id"].as_str() == Some(&job_prod_id.to_string())),
"prod job should appear in label=prod filter"
);
// Should NOT find the staging job
assert!(
!jobs
.iter()
.any(|j| j["id"].as_str() == Some(&job_staging_id.to_string())),
"staging job should not appear in label=prod filter"
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_wm_labels_from_result_merged_with_static_labels(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let client = windmill_api_client::create_client(
&format!("http://localhost:{port}"),
"SECRET_TOKEN".to_string(),
);
// Use Code(RawCode) to run a Bun script that returns wm_labels,
// then set static labels on the job row before execution
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content: r#"export async function main() { return { wm_labels: ["runtime-label"] }; }"#
.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}));
let completed = job
.run_until_complete_with(&db, false, port, |uuid| {
let db = db.clone();
async move {
// Set static labels before the worker picks up the job
sqlx::query(
"UPDATE v2_job SET labels = ARRAY['static-label']::text[] WHERE id = $1",
)
.bind(uuid)
.execute(&db)
.await
.expect("should set labels");
}
})
.await;
// Fetch the job via API and check labels
let response = client
.client()
.get(format!(
"{}/w/test-workspace/jobs_u/get/{}",
client.baseurl(),
completed.id
))
.send()
.await?;
assert!(response.status().is_success());
let job: serde_json::Value = response.json().await?;
let labels = job["labels"].as_array().expect("labels should be an array");
// Should contain both the static label and runtime label from wm_labels
assert!(
labels.contains(&json!("static-label")),
"should contain static label set at push time, got: {:?}",
labels
);
assert!(
labels.contains(&json!("runtime-label")),
"should contain runtime label from wm_labels in result, got: {:?}",
labels
);
Ok(())
}
+1 -1
View File
@@ -179,7 +179,7 @@ export async function main(path: string, email: string, job_id: string, is_flow:
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
debouncing_settings: DebouncingSettings::default(), labels: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
+27 -15
View File
@@ -150,7 +150,8 @@ async fn list_flows(
"favorite.path IS NOT NULL as starred",
"draft.path IS NOT NULL as has_draft",
"draft_only",
"ws_error_handler_muted"
"ws_error_handler_muted",
"o.labels"
])
.left()
.join("favorite")
@@ -196,6 +197,11 @@ async fn list_flows(
if let Some(dw) = &lq.dedicated_worker {
sqlb.and_where_eq("dedicated_worker", dw);
}
if let Some(label) = &lq.label {
for l in label.split(',') {
sqlb.and_where("o.labels @> ARRAY[?]".bind(&l.trim()));
}
}
if lq.with_deployment_msg.unwrap_or(false) {
sqlb.join("deployment_metadata dm")
@@ -489,13 +495,13 @@ async fn create_flow(
dependency_job, lock_error_logs, draft_only, tag,
dedicated_worker, visible_to_runner_only, on_behalf_of_email,
ws_error_handler_muted,
value, schema, edited_by, edited_at
value, schema, edited_by, edited_at, labels
) VALUES (
$1, $2, $3, $4,
NULL, '', $5, $6,
$7, $8, $9,
$10,
$11, $12::text::json, $13, now()
$11, $12::text::json, $13, now(), $14
)"#,
w_id,
nf.path,
@@ -514,6 +520,7 @@ async fn create_flow(
sqlx::types::Json(&nf.value) as _,
schema_str,
&authed.username,
nf.labels.as_deref() as Option<&[String]>,
)
.execute(&mut *tx)
.await?;
@@ -763,7 +770,7 @@ async fn get_flow_version(
let mut tx = user_db.begin(&authed).await?;
let flow = sqlx::query_as::<_, Flow>(
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.draft_only, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.draft_only, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, flow.labels, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
FROM flow
LEFT JOIN flow_version ON flow_version.path = flow.path AND flow_version.workspace_id = flow.workspace_id
WHERE flow.path = $1 AND flow.workspace_id = $2 AND flow_version.id = $3",
@@ -821,6 +828,7 @@ async fn get_flow_version_by_id(
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow.labels,
flow_version.schema,
flow_version.value,
flow_version.created_at as edited_at,
@@ -960,7 +968,8 @@ async fn update_flow(
value = $9,
schema = $10::text::json,
edited_by = $11,
edited_at = now()
edited_at = now(),
labels = COALESCE($14, labels)
WHERE
path = $12 AND workspace_id = $13",
if is_new_path { flow_path } else { &nf.path },
@@ -980,6 +989,7 @@ async fn update_flow(
authed.username,
flow_path,
w_id,
nf.labels.as_deref() as Option<&[String]>,
)
.execute(&mut *tx)
.await
@@ -991,8 +1001,8 @@ async fn update_flow(
// if new path, must clone flow to new path and delete old flow for flow_version foreign key constraint
sqlx::query!(
"INSERT INTO flow
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)
SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels)
SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels
FROM flow
WHERE path = $2 AND workspace_id = $3",
nf.path,
@@ -1336,11 +1346,12 @@ async fn get_flow_by_path(
flow.ws_error_handler_muted,
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow.on_behalf_of_email,
flow.labels,
flow_version.id AS version_id,
flow_version.schema,
flow_version.value,
flow_version.created_at AS edited_at,
flow_version.schema,
flow_version.value,
flow_version.created_at AS edited_at,
flow_version.created_by AS edited_by,
favorite.path IS NOT NULL AS starred
FROM flow
@@ -1376,12 +1387,13 @@ async fn get_flow_by_path(
flow.ws_error_handler_muted,
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow.on_behalf_of_email,
flow.labels,
flow_version.id AS version_id,
flow_version.schema,
flow_version.schema,
flow_version.value,
flow_version.created_at AS edited_at,
flow_version.created_by AS edited_by,
flow_version.created_at AS edited_at,
flow_version.created_by AS edited_by,
NULL AS starred
FROM flow
LEFT JOIN flow_version
@@ -613,6 +613,7 @@ pub async fn run_flow<'c>(
on_behalf_of_email,
edited_by,
early_return,
labels,
..
} = flow_version_info;
@@ -656,6 +657,7 @@ pub async fn run_flow<'c>(
version,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
labels,
},
PushArgs { args: &args.args, extra: args.extra },
authed.display_username(),
+6 -10
View File
@@ -295,34 +295,30 @@ pub fn filter_list_completed_query(
let p = v.replace("*", "%");
if label.negated {
format!(
"NOT EXISTS (SELECT 1 FROM jsonb_array_elements_text(result->'wm_labels') lbl WHERE jsonb_typeof(result->'wm_labels') = 'array' AND lbl LIKE {})", quote(&p)
"NOT EXISTS (SELECT 1 FROM unnest(v2_job.labels) lbl WHERE lbl LIKE {})", quote(&p)
)
} else {
format!(
"EXISTS (SELECT 1 FROM jsonb_array_elements_text(result->'wm_labels') lbl WHERE jsonb_typeof(result->'wm_labels') = 'array' AND lbl LIKE {})", quote(&p)
"EXISTS (SELECT 1 FROM unnest(v2_job.labels) lbl WHERE lbl LIKE {})", quote(&p)
)
}
})
.collect();
let sep = if label.negated { " AND " } else { " OR " };
if !label.negated {
sqlb.and_where("result ? 'wm_labels'");
}
sqlb.and_where(format!("({})", clauses.join(sep)));
} else if label.negated {
let clauses: Vec<_> = label
.values
.iter()
.map(|v| format!("NOT (result->'wm_labels' ? {})", quote(v)))
.map(|v| format!("NOT (v2_job.labels @> ARRAY[{}])", quote(v)))
.collect();
sqlb.and_where(format!("({})", clauses.join(" AND ")));
} else {
let clauses: Vec<_> = label
.values
.iter()
.map(|v| format!("result->'wm_labels' ? {}", quote(v)))
.map(|v| format!("v2_job.labels @> ARRAY[{}]", quote(v)))
.collect();
sqlb.and_where("result ? 'wm_labels'");
sqlb.and_where(format!("({})", clauses.join(" OR ")));
}
}
@@ -545,7 +541,7 @@ pub fn filter_list_completed_query(
let pat = format!("%{}%", escape_ilike_pattern(bf));
sqlb.and_where(
"(runnable_path ILIKE ? OR v2_job.tag ILIKE ? OR trigger ILIKE ? OR trigger_kind::text ILIKE ? \
OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(result->'wm_labels') lbl WHERE jsonb_typeof(result->'wm_labels') = 'array' AND lbl ILIKE ?))"
OR EXISTS (SELECT 1 FROM unnest(v2_job.labels) lbl WHERE lbl ILIKE ?))"
.bind(&pat).bind(&pat).bind(&pat).bind(&pat).bind(&pat)
);
}
@@ -946,7 +942,7 @@ mod tests {
false,
);
let sql = build_sql(sqlb);
assert!(sql.contains("wm_labels"));
assert!(sql.contains("v2_job.labels"));
}
#[test]
+5 -4
View File
@@ -252,7 +252,7 @@ pub struct ListableCompletedJob {
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<serde_json::Value>,
pub labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<serde_json::Value>,
}
@@ -293,7 +293,7 @@ pub struct UnifiedJob {
pub concurrent_limit: Option<i32>,
pub concurrency_time_window_s: Option<i32>,
pub priority: Option<i16>,
pub labels: Option<serde_json::Value>,
pub labels: Option<Vec<String>>,
pub self_wait_time_ms: Option<i64>,
pub aggregate_wait_time_ms: Option<i64>,
pub preprocessed: Option<bool>,
@@ -334,7 +334,7 @@ const CJ_FIELDS: &[&str] = &[
"null as concurrent_limit",
"null as concurrency_time_window_s",
"v2_job.priority",
"v2_job_completed.result->'wm_labels' as labels",
"v2_job.labels",
"self_wait_time_ms",
"aggregate_wait_time_ms",
"v2_job.preprocessed",
@@ -375,7 +375,7 @@ const QJ_FIELDS: &[&str] = &[
"v2_job.concurrent_limit",
"v2_job.concurrency_time_window_s",
"v2_job.priority",
"null as labels",
"v2_job.labels",
"self_wait_time_ms",
"aggregate_wait_time_ms",
"v2_job.preprocessed",
@@ -482,6 +482,7 @@ impl From<UnifiedJob> for Job {
priority: uj.priority,
preprocessed: uj.preprocessed,
runnable_settings_handle: uj.runnable_settings_handle,
labels: uj.labels,
},
)),
t => panic!("job type {} not valid", t),
+27 -8
View File
@@ -99,6 +99,8 @@ pub struct NewSchedule {
pub dynamic_skip: Option<String>,
pub permissioned_as: Option<String>,
pub preserve_permissioned_as: Option<bool>,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
@@ -248,7 +250,7 @@ async fn create_schedule(
on_recovery, on_recovery_times, on_recovery_extra_args,
on_success, on_success_extra_args,
ws_error_handler_muted, retry, summary, no_flow_overlap,
tag, paused_until, cron_version, description, dynamic_skip
tag, paused_until, cron_version, description, dynamic_skip, labels
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11,
@@ -256,7 +258,7 @@ async fn create_schedule(
$16, $17, $18,
$19, $20,
$21, $22, $23, $24,
$25, $26, $27, $28, $29
$25, $26, $27, $28, $29, $30
)
RETURNING
workspace_id,
@@ -290,7 +292,8 @@ async fn create_schedule(
tag,
paused_until,
cron_version,
dynamic_skip
dynamic_skip,
labels
"#,
w_id,
ns.path,
@@ -324,7 +327,8 @@ async fn create_schedule(
ns.paused_until,
ns.cron_version.clone().unwrap_or_else(|| "v2".to_string()),
ns.description,
ns.dynamic_skip
ns.dynamic_skip,
ns.labels.as_deref() as Option<&[String]>
)
.fetch_one(&mut *tx)
.await
@@ -467,7 +471,8 @@ async fn edit_schedule(
dynamic_skip = $23,
email = $24,
edited_by = $25,
permissioned_as = $26
permissioned_as = $26,
labels = COALESCE($27, labels)
WHERE path = $19 AND workspace_id = $20
RETURNING
workspace_id,
@@ -501,7 +506,8 @@ async fn edit_schedule(
tag,
paused_until,
cron_version,
dynamic_skip
dynamic_skip,
labels
"#,
es.schedule,
es.timezone,
@@ -532,7 +538,8 @@ async fn edit_schedule(
es.dynamic_skip,
resolved_email,
resolved_edited_by,
resolved_permissioned_as
resolved_permissioned_as,
es.labels.as_deref() as Option<&[String]>
)
.fetch_one(&mut *tx)
.await
@@ -608,6 +615,7 @@ pub struct ListScheduleQuery {
// filter on summary (pattern match)
pub summary: Option<String>,
pub broad_filter: Option<String>,
pub label: Option<String>,
}
#[derive(sqlx::FromRow, Serialize, Deserialize, Debug, Clone)]
@@ -623,6 +631,8 @@ pub struct ScheduleLight {
pub is_flow: bool,
pub summary: Option<String>,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
async fn list_schedule(
authed: ApiAuthed,
@@ -645,6 +655,7 @@ async fn list_schedule(
"is_flow",
"summary",
"extra_perms",
"labels",
])
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
@@ -685,6 +696,11 @@ async fn list_schedule(
.bind(&pat).bind(&pat).bind(&pat).bind(&pat).bind(&pat)
);
}
if let Some(label) = &lsq.label {
for l in label.split(',') {
sqlb.and_where("labels @> ARRAY[?]".bind(&l.trim()));
}
}
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let rows = sqlx::query_as::<_, ScheduleLight>(&sql)
.fetch_all(&mut *tx)
@@ -845,7 +861,8 @@ pub async fn set_enabled(
tag,
paused_until,
cron_version,
dynamic_skip
dynamic_skip,
labels
"#,
payload.enabled,
authed.email,
@@ -1221,6 +1238,8 @@ pub struct EditSchedule {
pub dynamic_skip: Option<String>,
pub permissioned_as: Option<String>,
pub preserve_permissioned_as: Option<bool>,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
pub use windmill_queue::schedule::clear_schedule;
+15 -5
View File
@@ -128,6 +128,8 @@ pub struct ScriptWDraft<SR> {
#[serde(skip_serializing_if = "Option::is_none")]
#[sqlx(json(nullable))]
pub modules: Option<HashMap<String, ScriptModule>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
#[serde(flatten)]
#[sqlx(flatten)]
pub runnable_settings: SR,
@@ -183,6 +185,7 @@ impl ScriptWDraft<ScriptRunnableSettingsHandle> {
on_behalf_of_email: self.on_behalf_of_email,
assets: self.assets,
modules: self.modules,
labels: self.labels,
})
}
}
@@ -306,7 +309,8 @@ async fn list_scripts(
"ws_error_handler_muted",
"auto_kind",
"codebase IS NOT NULL as use_codebase",
"kind"
"kind",
"o.labels"
])
.left()
.join("favorite")
@@ -384,6 +388,11 @@ async fn list_scripts(
if let Some(dw) = &lq.dedicated_worker {
sqlb.and_where_eq("dedicated_worker", dw);
}
if let Some(label) = &lq.label {
for l in label.split(',') {
sqlb.and_where("o.labels @> ARRAY[?]".bind(&l.trim()));
}
}
if authed.is_operator {
sqlb.and_where_eq("kind", quote("script"));
} else if let Some(lowercased_kinds) = lowercased_kinds {
@@ -930,8 +939,8 @@ async fn create_script_internal<'c>(
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, priority, restart_unless_cancelled, \
delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules) \
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, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39)",
delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules, labels) \
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, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40)",
&w_id,
&hash.0,
ns.path,
@@ -974,7 +983,8 @@ async fn create_script_internal<'c>(
guarded_debounce_delay_s,
ns.cache_ignore_s3_path,
runnable_settings_handle,
ns.modules.as_ref().and_then(|m| serde_json::to_value(m).ok())
ns.modules.as_ref().and_then(|m| serde_json::to_value(m).ok()),
ns.labels.as_deref() as Option<&[String]>
)
.execute(&mut *tx)
.await?;
@@ -1424,7 +1434,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft<ScriptRunnableSettingsHandle>>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, runnable_settings_handle, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, has_preprocessor, on_behalf_of_email, assets, modules, debounce_key, debounce_delay_s FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, runnable_settings_handle, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, has_preprocessor, on_behalf_of_email, assets, modules, debounce_key, debounce_delay_s, labels 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
ORDER BY script.created_at DESC LIMIT 1",
+272
View File
@@ -4372,6 +4372,12 @@ paths:
type: string
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: label
description: Filter by label
in: query
required: false
schema:
type: string
responses:
"200":
description: variable list
@@ -5495,6 +5501,12 @@ paths:
in: query
schema:
type: string
- name: label
description: Filter by label
in: query
required: false
schema:
type: string
responses:
"200":
description: resource list
@@ -6468,6 +6480,12 @@ paths:
If false, show only scripts with dedicated_worker disabled.
schema:
type: boolean
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
@@ -8050,6 +8068,12 @@ paths:
If false, show only flows with dedicated_worker disabled.
schema:
type: boolean
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: All flow
@@ -8573,6 +8597,12 @@ paths:
in: query
schema:
type: boolean
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: All raw apps
@@ -8672,6 +8702,12 @@ paths:
in: query
schema:
type: boolean
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: All apps
@@ -8724,6 +8760,10 @@ paths:
preserve_on_behalf_of:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original on_behalf_of value in the policy instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- value
@@ -8772,6 +8812,10 @@ paths:
preserve_on_behalf_of:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original on_behalf_of value in the policy instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- value
@@ -9086,6 +9130,10 @@ paths:
preserve_on_behalf_of:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original on_behalf_of value in the policy instead of overwriting it."
labels:
type: array
items:
type: string
responses:
"200":
description: app updated
@@ -9128,6 +9176,10 @@ paths:
preserve_on_behalf_of:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original on_behalf_of value in the policy instead of overwriting it."
labels:
type: array
items:
type: string
js:
type: string
css:
@@ -11913,6 +11965,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: schedule list
@@ -12156,6 +12214,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: http trigger list
@@ -12351,6 +12415,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: websocket trigger list
@@ -12545,6 +12615,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: kafka trigger list
@@ -12780,6 +12856,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: nats trigger list
@@ -12969,6 +13051,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: sqs trigger list
@@ -13413,6 +13501,12 @@ paths:
in: query
schema:
type: boolean
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: native triggers list
@@ -13723,6 +13817,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: mqtt trigger list
@@ -13912,6 +14012,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: gcp trigger list
@@ -14416,6 +14522,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: postgres trigger list
@@ -14605,6 +14717,12 @@ paths:
in: query
schema:
type: string
- name: label
in: query
required: false
schema:
type: string
description: Filter by label
responses:
"200":
description: email trigger list
@@ -19414,6 +19532,11 @@ components:
description: "Additional script modules keyed by relative file path"
additionalProperties:
$ref: "#/components/schemas/ScriptModule"
labels:
type: array
items:
type: string
default: []
required:
- hash
@@ -19432,6 +19555,7 @@ components:
- starred
- has_preprocessor
NewScript:
type: object
properties:
@@ -19537,6 +19661,10 @@ components:
description: "Additional script modules keyed by relative file path"
additionalProperties:
$ref: "#/components/schemas/ScriptModule"
labels:
type: array
items:
type: string
required:
- path
@@ -20383,6 +20511,10 @@ components:
expires_at:
type: string
format: date-time
labels:
type: array
items:
type: string
required:
- workspace_id
- path
@@ -20431,6 +20563,10 @@ components:
type: string
description: The expiration date of the variable
format: date-time
labels:
type: array
items:
type: string
required:
- path
- value
@@ -20452,6 +20588,10 @@ components:
description:
type: string
description: The new description of the variable
labels:
type: array
items:
type: string
AuditLog:
type: object
@@ -20844,6 +20984,10 @@ components:
resource_type:
type: string
description: The resource_type associated with the resource
labels:
type: array
items:
type: string
required:
- path
- value
@@ -20862,6 +21006,10 @@ components:
resource_type:
type: string
description: The new resource_type to be associated with the resource
labels:
type: array
items:
type: string
Resource:
type: object
@@ -20886,6 +21034,10 @@ components:
edited_at:
type: string
format: date-time
labels:
type: array
items:
type: string
required:
- path
- resource_type
@@ -20924,6 +21076,10 @@ components:
edited_at:
type: string
format: date-time
labels:
type: array
items:
type: string
required:
- path
- resource_type
@@ -21075,6 +21231,11 @@ components:
type: string
nullable: true
description: Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)
labels:
type: array
items:
type: string
default: []
required:
- path
- edited_by
@@ -21088,6 +21249,7 @@ components:
- email
- permissioned_as
ScheduleWJobs:
allOf:
- $ref: "#/components/schemas/Schedule"
@@ -21214,6 +21376,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- schedule
@@ -21309,6 +21475,10 @@ components:
type: boolean
nullable: true
description: "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity"
labels:
type: array
items:
type: string
required:
- schedule
- timezone
@@ -21373,6 +21543,11 @@ components:
mode:
$ref: "#/components/schemas/TriggerMode"
description: Trigger mode (enabled/disabled)
labels:
type: array
items:
type: string
default: []
required:
- path
- script_path
@@ -21384,6 +21559,7 @@ components:
- is_flow
- mode
AuthenticationMethod:
type: string
enum:
@@ -21671,6 +21847,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -21763,6 +21943,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -21974,6 +22158,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -22053,6 +22241,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -22228,6 +22420,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -22288,6 +22484,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -22430,6 +22630,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -22598,6 +22802,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- queue_url
- aws_resource_path
@@ -22650,6 +22858,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- queue_url
- aws_resource_path
@@ -22814,6 +23026,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -22862,6 +23078,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -23011,6 +23231,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -23088,6 +23312,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -23195,6 +23423,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -23250,6 +23482,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -23304,6 +23540,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -23336,6 +23576,10 @@ components:
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
- script_path
@@ -23746,6 +23990,11 @@ components:
type: boolean
on_behalf_of_email:
type: string
labels:
type: array
items:
type: string
default: []
required:
- path
- edited_by
@@ -23753,6 +24002,7 @@ components:
- archived
- extra_perms
OpenFlowWPath:
allOf:
- $ref: "../../openflow.openapi.yaml#/components/schemas/OpenFlow"
@@ -23777,6 +24027,10 @@ components:
preserve_on_behalf_of:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original on_behalf_of_email value instead of overwriting it."
labels:
type: array
items:
type: string
required:
- path
@@ -23871,6 +24125,11 @@ components:
enum: [viewer, publisher, anonymous]
raw_app:
type: boolean
labels:
type: array
items:
type: string
default: []
required:
- id
- workspace_id
@@ -23881,6 +24140,7 @@ components:
- edited_at
- execution_mode
ScopeDefinition:
type: object
properties:
@@ -23934,6 +24194,11 @@ components:
edited_at:
type: string
format: date-time
labels:
type: array
items:
type: string
default: []
required:
- workspace_id
- path
@@ -23942,6 +24207,7 @@ components:
- version
- edited_at
AppWithLastVersion:
type: object
properties:
@@ -23978,6 +24244,11 @@ components:
type: boolean
bundle_secret:
type: string
labels:
type: array
items:
type: string
default: []
required:
- id
- workspace_id
@@ -23992,6 +24263,7 @@ components:
- extra_perms
- raw_app
AppWithLastVersionWDraft:
allOf:
- $ref: "#/components/schemas/AppWithLastVersion"
+46 -15
View File
@@ -158,6 +158,8 @@ pub struct ListableApp {
pub deployment_msg: Option<String>,
#[serde(skip_serializing_if = "is_false")]
pub raw_app: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
fn is_false(b: &bool) -> bool {
@@ -190,6 +192,8 @@ pub struct AppWithLastVersion {
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub bundle_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(Serialize, FromRow)]
@@ -288,6 +292,8 @@ pub struct CreateApp {
pub deployment_message: Option<String>,
pub custom_path: Option<String>,
pub preserve_on_behalf_of: Option<bool>,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
@@ -299,6 +305,8 @@ pub struct EditApp {
pub deployment_message: Option<String>,
pub custom_path: Option<String>,
pub preserve_on_behalf_of: Option<bool>,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
#[derive(Serialize, FromRow)]
@@ -354,6 +362,7 @@ async fn list_apps(
"draft.path IS NOT NULL as has_draft",
"draft_only",
"app_version.raw_app",
"app.labels",
])
.left()
.join("favorite")
@@ -394,6 +403,12 @@ async fn list_apps(
sqlb.and_where("app.draft_only IS NOT TRUE");
}
if let Some(label) = &lq.label {
for l in label.split(',') {
sqlb.and_where("app.labels @> ARRAY[?]".bind(&l.trim()));
}
}
if lq.with_deployment_msg.unwrap_or(false) {
sqlb.join("deployment_metadata dm")
.left()
@@ -543,8 +558,8 @@ async fn get_app(
let app_o = if query.with_starred_info.unwrap_or(false) {
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, favorite.path IS NOT NULL as starred, app_version.raw_app
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, favorite.path IS NOT NULL as starred, app_version.raw_app, app.labels
FROM app
JOIN app_version
ON app_version.id = app.versions[array_upper(app.versions, 1)]
@@ -563,8 +578,8 @@ async fn get_app(
} else {
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, NULL as starred, app_version.raw_app
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, NULL as starred, app_version.raw_app, app.labels
FROM app, app_version
WHERE app.path = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
)
@@ -590,8 +605,8 @@ async fn get_app_lite(
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, coalesce(app_version_lite.value::json, app_version.value) as value,
app_version.created_at, app_version.created_by, NULL as starred, app_version.raw_app
app.extra_perms, coalesce(app_version_lite.value::json, app_version.value) as value,
app_version.created_at, app_version.created_by, NULL as starred, app_version.raw_app, app.labels
FROM app, app_version
LEFT JOIN app_version_lite ON app_version_lite.id = app_version.id
WHERE app.path = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
@@ -631,7 +646,8 @@ async fn get_app_w_draft(
app_version.created_by,
app.draft_only,
draft.value AS "draft",
app_version.raw_app
app_version.raw_app,
app.labels
FROM app
INNER JOIN app_version
ON app_version.id = app.versions[array_upper(app.versions, 1)]
@@ -773,9 +789,9 @@ async fn get_app_by_id(
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, app_version.raw_app
FROM app, app_version
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, app_version.raw_app, app.labels
FROM app, app_version
WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2",
)
.bind(&id)
@@ -802,7 +818,7 @@ async fn get_public_app_by_secret(
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
null as extra_perms, coalesce(app_version_lite.value::json, app_version.value::json) as value,
app_version.created_at, app_version.created_by, app_version.raw_app
app_version.created_at, app_version.created_by, app_version.raw_app, app.labels
FROM app, app_version
LEFT JOIN app_version_lite ON app_version_lite.id = app_version.id
WHERE app.id = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]")
@@ -1270,8 +1286,8 @@ async fn create_app_internal<'a>(
.await?;
let id = sqlx::query_scalar!(
"INSERT INTO app
(workspace_id, path, summary, policy, versions, draft_only, custom_path)
VALUES ($1, $2, $3, $4, '{}', $5, $6) RETURNING id",
(workspace_id, path, summary, policy, versions, draft_only, custom_path, labels)
VALUES ($1, $2, $3, $4, '{}', $5, $6, $7) RETURNING id",
w_id,
app.path,
app.summary,
@@ -1280,7 +1296,8 @@ async fn create_app_internal<'a>(
app.custom_path
.as_ref()
.map(|s| if s.is_empty() { None } else { Some(s) })
.flatten()
.flatten(),
app.labels.as_deref() as Option<&[String]>
)
.fetch_one(&mut *tx)
.await?;
@@ -1707,6 +1724,7 @@ async fn update_app_internal<'a>(
|| ns.path.is_some()
|| ns.summary.is_some()
|| ns.custom_path.is_some()
|| ns.labels.is_some()
{
let mut sqlb = SqlBuilder::update_table("app");
sqlb.and_where_eq("path", "?".bind(&path));
@@ -1794,7 +1812,20 @@ async fn update_app_internal<'a>(
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut *tx).await?;
not_found_if_none(npath_o, "App", path)?
let npath_val = not_found_if_none(npath_o, "App", path)?;
if let Some(nlabels) = &ns.labels {
sqlx::query!(
"UPDATE app SET labels = $1 WHERE path = $2 AND workspace_id = $3",
nlabels as &[String],
&npath_val,
w_id
)
.execute(&mut *tx)
.await?;
}
npath_val
} else {
path.to_owned()
};
+8 -3
View File
@@ -969,7 +969,8 @@ macro_rules! get_job_query {
("v2_job_completed", $($opts:tt)*) => {
get_job_query!(
@impl "v2_job_completed", ($($opts)*),
"v2_job_completed.duration_ms, v2_job_completed.completed_at, CASE WHEN status = 'success' OR status = 'skipped' THEN true ELSE false END as success, result_columns, deleted, status = 'skipped' as is_skipped, result->'wm_labels' as labels, \
"v2_job_completed.duration_ms, v2_job_completed.completed_at, CASE WHEN status = 'success' OR status = 'skipped' THEN true ELSE false END as success, result_columns, deleted, status = 'skipped' as is_skipped, \
v2_job.labels, \
CASE WHEN result is null or pg_column_size(result) < 90000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result",
"",
)
@@ -979,7 +980,7 @@ macro_rules! get_job_query {
@impl "v2_job_queue", ($($opts)*),
"scheduled_for, running, ping as last_ping, suspend, suspend_until, same_worker, pre_run_error, visible_to_owner, \
flow_innermost_root_job AS root_job, flow_leaf_jobs AS leaf_jobs, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl, cache_ignore_s3_path, runnable_settings_handle, \
script_entrypoint_override",
script_entrypoint_override, v2_job.labels",
"LEFT JOIN v2_job_runtime ON v2_job_runtime.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id",
)
};
@@ -4593,6 +4594,7 @@ pub async fn run_wait_result_script_by_hash(
has_preprocessor,
on_behalf_of_email,
created_by,
labels,
runnable_settings:
ScriptRunnableSettingsInline { concurrency_settings, debouncing_settings },
..
@@ -4643,6 +4645,7 @@ pub async fn run_wait_result_script_by_hash(
priority,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
labels,
},
PushArgs { args: &args.args, extra: args.extra },
authed.display_username(),
@@ -6373,6 +6376,7 @@ pub async fn run_job_by_hash_inner(
on_behalf_of_email,
created_by,
delete_after_use,
labels,
..
} = get_script_info_for_hash(Some(userdb_authed), &db, &w_id, hash)
.await?
@@ -6422,6 +6426,7 @@ pub async fn run_job_by_hash_inner(
priority,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
labels,
},
PushArgs { args: &args.args, extra: args.extra },
authed.display_username(),
@@ -7273,7 +7278,7 @@ async fn list_completed_jobs(
"v2_job_completed.memory_peak as mem_peak",
"v2_job.tag",
"v2_job.priority",
"v2_job_completed.result->'wm_labels' as labels",
"v2_job.labels",
args_field,
"'CompletedJob' as type",
],
+21
View File
@@ -563,6 +563,7 @@ pub async fn run_server(
.nest("/groups_history", group_history::workspaced_service())
.nest("/inputs", windmill_api_inputs::workspaced_service())
.nest("/internal_db", internal_db::workspaced_service())
.route("/labels/list", get(list_workspace_labels))
.nest("/job_metrics", job_metrics::workspaced_service())
.nest("/job_helpers", job_helpers_service)
.nest("/jobs", jobs::workspaced_service())
@@ -1090,6 +1091,26 @@ async fn ee_license() -> &'static str {
""
}
async fn list_workspace_labels(
Extension(db): Extension<DB>,
axum::extract::Path(w_id): axum::extract::Path<String>,
) -> windmill_common::error::JsonResult<Vec<String>> {
let labels = sqlx::query_scalar!(
"SELECT DISTINCT unnest(labels) as \"label!\" FROM (
SELECT labels FROM script WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM flow WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM resource WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM variable WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM schedule WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM app WHERE workspace_id = $1 AND labels IS NOT NULL
) t ORDER BY 1",
&w_id
)
.fetch_all(&db)
.await?;
Ok(axum::Json(labels))
}
#[cfg(feature = "enterprise")]
async fn ee_license() -> String {
use windmill_common::ee_oss::{LICENSE_KEY_ID, LICENSE_KEY_VALID};
+9
View File
@@ -39,6 +39,8 @@ pub struct ListableApp {
pub extra_perms: serde_json::Value,
pub starred: bool,
pub version: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
async fn list_apps(
@@ -59,6 +61,7 @@ async fn list_apps(
"app.extra_perms",
"app.version",
"favorite.path IS NOT NULL as starred",
"app.labels",
])
.left()
.join("favorite")
@@ -85,6 +88,12 @@ async fn list_apps(
sqlb.and_where_eq("app.path", "?".bind(path_exact));
}
if let Some(label) = &lq.label {
for l in label.split(',') {
sqlb.and_where("app.labels @> ARRAY[?]".bind(&l.trim()));
}
}
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableApp>(&sql)
@@ -107,6 +107,8 @@ struct ScriptMetadata {
pub concurrency_settings: ConcurrencySettings,
#[serde(flatten)]
pub debouncing_settings: DebouncingSettings,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
pub fn is_none_or_false(val: &Option<bool>) -> bool {
@@ -514,6 +516,7 @@ pub(crate) async fn tarball_workspace(
has_preprocessor: script.has_preprocessor,
on_behalf_of_email: script.on_behalf_of_email,
modules: script.modules,
labels: script.labels,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
archive
@@ -561,7 +564,7 @@ pub(crate) async fn tarball_workspace(
{
let flows = sqlx::query_as::<_, Flow>(
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.draft_only, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.draft_only, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, flow.labels, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
FROM flow
LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
WHERE flow.workspace_id = $1 AND flow.archived = false AND (flow.draft_only IS NULL OR flow.draft_only = false)",
@@ -611,7 +614,7 @@ pub(crate) async fn tarball_workspace(
let apps = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, app_version.raw_app from app, app_version
app_version.created_at, app_version.created_by, app_version.raw_app, app.labels from app, app_version
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]
AND (app.draft_only IS NULL OR app.draft_only = false)",
)
+10 -2
View File
@@ -95,6 +95,7 @@ pub async fn script_path_to_payload<'e>(
has_preprocessor,
on_behalf_of_email,
created_by,
labels,
..
} = get_latest_deployed_hash_for_path(db_authed, db.clone(), w_id, script_path)
.await?
@@ -123,6 +124,7 @@ pub async fn script_path_to_payload<'e>(
&& has_preprocessor.unwrap_or(false),
debouncing_settings,
concurrency_settings,
labels,
},
tag,
delete_after_use,
@@ -170,10 +172,16 @@ pub async fn get_payload_tag_from_prefixed_path(
None,
)
} else {
let FlowVersionInfo { dedicated_worker, tag, version, .. } =
let FlowVersionInfo { dedicated_worker, tag, version, labels, .. } =
get_latest_flow_version_info_for_path(None, &db, w_id, &path, true).await?;
(
JobPayload::Flow { path, dedicated_worker, apply_preprocessor: false, version },
JobPayload::Flow {
path,
dedicated_worker,
apply_preprocessor: false,
version,
labels,
},
tag,
None,
None,
+9 -2
View File
@@ -867,6 +867,7 @@ pub struct ScriptHashInfo<SR> {
pub has_preprocessor: Option<bool>,
pub on_behalf_of_email: Option<String>,
pub created_by: String,
pub labels: Option<Vec<String>>,
#[sqlx(flatten)]
pub runnable_settings: SR,
}
@@ -896,6 +897,7 @@ impl ScriptHashInfo<ScriptRunnableSettingsHandle> {
has_preprocessor: self.has_preprocessor,
on_behalf_of_email: self.on_behalf_of_email,
created_by: self.created_by,
labels: self.labels,
runnable_settings: ScriptRunnableSettingsInline {
concurrency_settings: concurrency_settings.maybe_fallback(
self.runnable_settings.concurrency_key,
@@ -1064,6 +1066,7 @@ async fn get_script_info_for_hash_inner<'e, E: sqlx::PgExecutor<'e>>(
has_preprocessor,
on_behalf_of_email,
created_by,
labels,
path
FROM script WHERE hash = $1 AND workspace_id = $2",
)
@@ -1083,6 +1086,7 @@ pub struct FlowVersionInfo {
pub on_behalf_of_email: Option<String>,
pub edited_by: String,
pub dedicated_worker: Option<bool>,
pub labels: Option<Vec<String>>,
}
struct CachedFlowPath(String);
@@ -1210,7 +1214,8 @@ pub fn get_flow_version_info_from_version<
flow.tag,
flow.dedicated_worker,
flow.on_behalf_of_email,
flow.edited_by
flow.edited_by,
flow.labels
FROM
flow_version
INNER JOIN flow
@@ -1291,9 +1296,10 @@ pub async fn get_latest_hash_for_path<'c, E: sqlx::PgExecutor<'c>>(
Option<String>,
String,
Option<i64>,
Option<Vec<String>>,
)> {
let r_o = sqlx::query!(
"select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, runnable_settings_handle, language as \"language: ScriptLang\", dedicated_worker, priority, timeout, on_behalf_of_email, created_by FROM script
"select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, runnable_settings_handle, language as \"language: ScriptLang\", dedicated_worker, priority, timeout, on_behalf_of_email, created_by, labels FROM script
WHERE path = $1 AND workspace_id = $2 AND archived = false AND (lock IS NOT NULL OR $3 = false)
ORDER BY created_at DESC LIMIT 1",
script_path,
@@ -1322,6 +1328,7 @@ pub async fn get_latest_hash_for_path<'c, E: sqlx::PgExecutor<'c>>(
script.on_behalf_of_email,
script.created_by,
script.runnable_settings_handle,
script.labels,
))
}
+6 -3
View File
@@ -114,6 +114,7 @@ pub async fn prefetch_cached_script(
on_behalf_of_email: script.on_behalf_of_email,
assets: script.assets,
modules: script.modules,
labels: script.labels,
runnable_settings: ScriptRunnableSettingsInline {
concurrency_settings: concurrency_settings.maybe_fallback(
script.runnable_settings.concurrency_key,
@@ -352,7 +353,8 @@ pub async fn fetch_script_for_update<'a>(
has_preprocessor,
on_behalf_of_email,
assets,
modules
modules,
labels
FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false ORDER BY created_at DESC LIMIT 1 FOR UPDATE",
)
.bind(path)
@@ -429,6 +431,7 @@ pub async fn clone_script<'c>(
assets: s.assets,
modules: s.modules,
auto_parent: None,
labels: s.labels,
};
let new_hash = hash_script(&ns);
@@ -447,14 +450,14 @@ pub async fn clone_script<'c>(
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, \
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, \
codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules)
codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules, labels)
SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, \
content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, \
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, \
codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules
codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules, labels
FROM script WHERE hash = $2 AND workspace_id = $3;
", new_hash, s.hash.0, w_id).execute(&mut *tx).await?;
+6
View File
@@ -45,6 +45,8 @@ pub struct ListableVariable {
pub refresh_error: Option<String>,
pub is_linked: Option<bool>,
pub expires_at: Option<chrono::DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, sqlx::FromRow)]
@@ -61,6 +63,8 @@ pub struct ExportableListableVariable {
pub is_oauth: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<chrono::DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
fn is_none_or_false(b: &Option<bool>) -> bool {
@@ -76,6 +80,8 @@ pub struct CreateVariable {
pub account: Option<i32>,
pub is_oauth: Option<bool>,
pub expires_at: Option<chrono::DateTime<Utc>>,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
pub async fn build_crypt(db: &DB, w_id: &str) -> crate::error::Result<MagicCrypt256> {
+11 -3
View File
@@ -1435,6 +1435,7 @@ async fn restart_job_if_perpetual_inner(
},
// TODO(debouncing): handle properly
debouncing_settings: DebouncingSettings::default(),
labels: None, // labels already set on original job
},
PushArgs::from(&args.0),
&queued_job.created_by,
@@ -4739,6 +4740,7 @@ async fn push_inner<'c, 'd>(
_low_level_priority: Option<i16>,
concurrency_settings: ConcurrencySettings,
debouncing_settings: DebouncingSettings,
labels: Option<Vec<String>>,
}
let mut preprocessed = None;
#[allow(unused)]
@@ -4756,6 +4758,7 @@ async fn push_inner<'c, 'd>(
_low_level_priority,
mut concurrency_settings,
debouncing_settings,
labels,
} = match job_payload {
JobPayload::ScriptHash {
hash,
@@ -4768,6 +4771,7 @@ async fn push_inner<'c, 'd>(
apply_preprocessor,
concurrency_settings,
debouncing_settings,
labels,
} => {
if apply_preprocessor {
preprocessed = Some(false);
@@ -4784,6 +4788,7 @@ async fn push_inner<'c, 'd>(
cache_ignore_s3_path,
dedicated_worker,
_low_level_priority: priority,
labels,
..Default::default()
}
}
@@ -5214,7 +5219,7 @@ async fn push_inner<'c, 'd>(
..Default::default()
}
}
JobPayload::Flow { path, dedicated_worker, apply_preprocessor, version } => {
JobPayload::Flow { path, dedicated_worker, apply_preprocessor, version, labels } => {
let mut ntx = tx.into_tx().await?;
// Do not use the lite version unless all workers are updated.
let data = if *DISABLE_FLOW_SCRIPT
@@ -5280,6 +5285,7 @@ async fn push_inner<'c, 'd>(
_low_level_priority: priority,
concurrency_settings,
debouncing_settings,
labels,
..Default::default()
}
}
@@ -5719,10 +5725,11 @@ async fn push_inner<'c, 'd>(
priority, -- 26
trigger_kind, -- 39
script_entrypoint_override, -- 12
preprocessed -- 27,
preprocessed, -- 27,
labels -- 44
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,
$19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,
($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)
($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27, $44)
),
inserted_runtime AS (
INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)
@@ -5778,6 +5785,7 @@ async fn push_inner<'c, 'd>(
end_user_email,
cache_ignore_s3_path,
runnable_settings_handle,
labels.as_deref() as Option<&[String]>,
)
.execute(&mut *tx)
.warn_after_seconds(1)
+11 -1
View File
@@ -102,6 +102,7 @@ async fn get_schedule_metadata<'c>(
on_behalf_of_email,
created_by,
_runnable_settings_handle,
_labels,
) = windmill_common::get_latest_hash_for_path(
&mut **tx,
&schedule.workspace_id,
@@ -294,7 +295,13 @@ pub async fn push_scheduled_job<'c>(
.await?;
let FlowVersionInfo {
version, tag, dedicated_worker, on_behalf_of_email, edited_by, ..
version,
tag,
dedicated_worker,
on_behalf_of_email,
edited_by,
labels,
..
} = get_flow_version_info_from_version(
&mut *tx,
version,
@@ -310,6 +317,7 @@ pub async fn push_scheduled_job<'c>(
dedicated_worker,
apply_preprocessor: false,
version,
labels,
},
tag,
None,
@@ -334,6 +342,7 @@ pub async fn push_scheduled_job<'c>(
on_behalf_of_email,
created_by,
runnable_settings_handle,
labels,
) = windmill_common::get_latest_hash_for_path(
&mut *tx,
&schedule.workspace_id,
@@ -409,6 +418,7 @@ pub async fn push_scheduled_job<'c>(
concurrent_limit,
concurrency_time_window_s,
),
labels,
},
if schedule.tag.as_ref().is_some_and(|x| x != "") {
schedule.tag.clone()
@@ -43,6 +43,7 @@ mod schedule_push {
paused_until: None,
cron_version: None,
dynamic_skip: None,
labels: None,
};
overrides(&mut s);
s
+31 -5
View File
@@ -125,6 +125,8 @@ pub struct Resource {
pub extra_perms: serde_json::Value,
pub created_by: Option<String>,
pub edited_at: Option<chrono::DateTime<chrono::Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -143,6 +145,8 @@ pub struct ListableResource {
pub is_expired: Option<bool>,
pub refresh_error: Option<String>,
pub account: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(Deserialize)]
@@ -151,12 +155,15 @@ pub struct CreateResource {
pub value: Option<Box<RawValue>>,
pub description: Option<String>,
pub resource_type: String,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
#[derive(Deserialize)]
struct EditResource {
path: Option<String>,
description: Option<String>,
value: Option<Box<RawValue>>,
labels: Option<Vec<String>>,
}
#[derive(Deserialize)]
@@ -169,6 +176,7 @@ pub struct ListResourceQuery {
// filter by matching a subset of the value using base64 encoded json subset
pub value: Option<String>,
pub broad_filter: Option<String>,
pub label: Option<String>,
}
#[derive(Serialize, FromRow)]
@@ -252,6 +260,7 @@ async fn list_resources(
"account.refresh_error",
"resource.created_by",
"resource.edited_at",
"resource.labels",
])
.left()
.join("variable")
@@ -313,6 +322,12 @@ async fn list_resources(
);
}
if let Some(label) = &lq.label {
for l in label.split(',') {
sqlb.and_where("resource.labels @> ARRAY[?]".bind(&l.trim()));
}
}
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableResource>(&sql)
@@ -821,15 +836,16 @@ async fn create_resource(
}
sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, description, resource_type, created_by, edited_at)
VALUES ($1, $2, $3, $4, $5, $6, now()) ON CONFLICT (workspace_id, path)
DO UPDATE SET value = EXCLUDED.value, description = EXCLUDED.description, resource_type = EXCLUDED.resource_type, edited_at = now()",
(workspace_id, path, value, description, resource_type, created_by, edited_at, labels)
VALUES ($1, $2, $3, $4, $5, $6, now(), $7) ON CONFLICT (workspace_id, path)
DO UPDATE SET value = EXCLUDED.value, description = EXCLUDED.description, resource_type = EXCLUDED.resource_type, edited_at = now(), labels = EXCLUDED.labels",
w_id,
resource.path,
raw_json as sqlx::types::Json<&RawValue>,
resource.description,
resource.resource_type,
authed.username
authed.username,
resource.labels.as_deref() as Option<&[String]>
)
.execute(&mut *tx)
.await?;
@@ -1196,7 +1212,6 @@ async fn update_resource(
if let Some(ndesc) = ns.description {
sqlb.set_str("description", ndesc);
}
sqlb.set_str("edited_at", "now()");
sqlb.returning("path");
@@ -1263,6 +1278,17 @@ async fn update_resource(
let npath = not_found_if_none(npath_o, "Resource", path)?;
if let Some(nlabels) = &ns.labels {
sqlx::query!(
"UPDATE resource SET labels = $1 WHERE path = $2 AND workspace_id = $3",
nlabels as &[String],
&npath,
&w_id
)
.execute(&mut *tx)
.await?;
}
audit_log(
&mut *tx,
&authed,
+24 -3
View File
@@ -102,6 +102,7 @@ struct ListVariableQuery {
// filter by matching the non-encrypted value (for non-secrets only)
pub value: Option<String>,
pub broad_filter: Option<String>,
pub label: Option<String>,
}
async fn list_variables(
@@ -130,6 +131,7 @@ async fn list_variables(
"resource.path IS NOT NULL as is_linked",
"account.refresh_token != '' as is_refreshed",
"variable.expires_at",
"variable.labels",
])
.left()
.join("account")
@@ -171,6 +173,12 @@ async fn list_variables(
);
}
if let Some(label) = &lq.label {
for l in label.split(',') {
sqlb.and_where("variable.labels @> ARRAY[?]".bind(&l.trim()));
}
}
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableVariable>(&sql)
@@ -424,8 +432,8 @@ async fn create_variable(
sqlx::query!(
"INSERT INTO variable
(workspace_id, path, value, is_secret, description, account, is_oauth, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
(workspace_id, path, value, is_secret, description, account, is_oauth, expires_at, labels)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
&w_id,
variable.path,
value,
@@ -433,7 +441,8 @@ async fn create_variable(
variable.description,
variable.account,
variable.is_oauth.unwrap_or(false),
variable.expires_at
variable.expires_at,
variable.labels.as_deref() as Option<&[String]>
)
.execute(&mut *tx)
.await?;
@@ -771,6 +780,7 @@ struct EditVariable {
is_secret: Option<bool>,
description: Option<String>,
account: Option<i32>,
labels: Option<Vec<String>>,
}
#[derive(Deserialize)]
@@ -958,6 +968,17 @@ async fn update_variable(
let npath = not_found_if_none(npath_o, "Variable", path)?;
if let Some(nlabels) = &ns.labels {
sqlx::query!(
"UPDATE variable SET labels = $1 WHERE path = $2 AND workspace_id = $3",
nlabels as &[String],
&npath,
&w_id
)
.execute(&mut *tx)
.await?;
}
audit_log(
&mut *tx,
&authed,
+2 -1
View File
@@ -478,7 +478,7 @@ pub async fn completed_job(uuid: Uuid, db: &Pool<Postgres>) -> CompletedJob {
j.flow_step_id IS NOT NULL AS is_flow_step, j.script_lang AS language, c.started_at,
c.status = 'skipped' AS is_skipped, j.raw_lock, j.permissioned_as_email AS email, j.visible_to_owner,
c.memory_peak AS mem_peak, j.tag, j.priority, NULL::TEXT AS logs, c.result_columns,
j.script_entrypoint_override, j.preprocessed, c.result->'wm_labels' as labels
j.script_entrypoint_override, j.preprocessed, j.labels
FROM v2_job_completed c JOIN v2_job j USING (id) WHERE j.id = $1",
)
.bind(uuid)
@@ -768,6 +768,7 @@ pub async fn run_deployed_relative_imports(
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
})
.push(&db2)
.await;
@@ -268,6 +268,18 @@ pub async fn create_many_http_triggers(
.await
.map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err))?;
if let Some(labels) = &new_http_trigger.base.labels {
sqlx::query!(
"UPDATE http_trigger SET labels = $1 WHERE workspace_id = $2 AND path = $3",
labels as &[String],
&w_id,
&new_http_trigger.base.path
)
.execute(&mut *tx)
.await
.map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err.into()))?;
}
audit_log(
&mut *tx,
&authed,
+34
View File
@@ -148,6 +148,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
"edited_at",
"extra_perms",
"mode",
"labels",
];
if Self::SUPPORTS_SERVER_STATE {
@@ -327,6 +328,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
"edited_at",
"extra_perms",
"mode",
"labels",
];
if Self::SUPPORTS_SERVER_STATE {
@@ -357,6 +359,12 @@ pub trait TriggerCrud: Send + Sync + 'static {
sqlb.and_where_like_left("path", path_start);
}
if let Some(label) = &query.label {
for l in label.split(',') {
sqlb.and_where("labels @> ARRAY[?]".bind(&l.trim()));
}
}
sqlb.offset(offset).limit(per_page);
}
@@ -417,6 +425,7 @@ async fn create_trigger<T: TriggerCrud>(
let mut tx = user_db.begin(&authed).await?;
let new_path = new_trigger.base.path.clone();
let labels = new_trigger.base.labels.clone();
let on_behalf_of_info = windmill_common::check_on_behalf_of_preservation(
new_trigger.base.permissioned_as.as_deref(),
new_trigger.base.preserve_permissioned_as.unwrap_or(false),
@@ -428,6 +437,18 @@ async fn create_trigger<T: TriggerCrud>(
.create_trigger(&db, &mut *tx, &authed, &workspace_id, new_trigger)
.await?;
if let Some(ref labels) = labels {
sqlx::query(&format!(
"UPDATE {} SET labels = $1 WHERE workspace_id = $2 AND path = $3",
T::TABLE_NAME
))
.bind(labels)
.bind(&workspace_id)
.bind(&new_path)
.execute(&mut *tx)
.await?;
}
audit_log(
&mut *tx,
&authed,
@@ -535,6 +556,7 @@ async fn update_trigger<T: TriggerCrud>(
let mut tx = user_db.begin(&authed).await?;
let new_path = edit_trigger.base.path.to_string();
let labels = edit_trigger.base.labels.clone();
let on_behalf_of_info = windmill_common::check_on_behalf_of_preservation(
edit_trigger.base.permissioned_as.as_deref(),
edit_trigger.base.preserve_permissioned_as.unwrap_or(false),
@@ -546,6 +568,18 @@ async fn update_trigger<T: TriggerCrud>(
.update_trigger(&db, &mut *tx, &authed, &workspace_id, path, edit_trigger)
.await?;
if let Some(ref labels) = labels {
sqlx::query(&format!(
"UPDATE {} SET labels = $1 WHERE workspace_id = $2 AND path = $3",
T::TABLE_NAME
))
.bind(labels)
.bind(&workspace_id)
.bind(&new_path)
.execute(&mut *tx)
.await?;
}
audit_log(
&mut *tx,
&authed,
+15 -1
View File
@@ -26,6 +26,7 @@ pub struct StandardTriggerQuery {
pub path: Option<String>,
pub is_flow: Option<bool>,
pub path_start: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, FromRow, Clone, Serialize, Deserialize)]
@@ -39,6 +40,8 @@ pub struct BaseTrigger {
pub permissioned_as: String,
pub edited_at: DateTime<Utc>,
pub extra_perms: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(Debug, FromRow, Clone, Serialize, Deserialize)]
@@ -109,6 +112,8 @@ pub struct BaseTriggerData {
/// If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using deploying user's
#[serde(skip_serializing_if = "Option::is_none")]
pub preserve_permissioned_as: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
impl BaseTriggerData {
@@ -165,7 +170,14 @@ impl StandardTriggerQuery {
impl Default for StandardTriggerQuery {
fn default() -> Self {
Self { page: Some(0), per_page: Some(100), path: None, path_start: None, is_flow: None }
Self {
page: Some(0),
per_page: Some(100),
path: None,
path_start: None,
is_flow: None,
label: None,
}
}
}
@@ -232,6 +244,7 @@ mod tests {
path: None,
is_flow: None,
path_start: None,
label: None,
};
assert_eq!(q.offset(), 100);
assert_eq!(q.limit(), 50);
@@ -245,6 +258,7 @@ mod tests {
path: None,
is_flow: None,
path_start: None,
label: None,
};
assert_eq!(q.offset(), 0);
assert_eq!(q.limit(), 100);
+1
View File
@@ -16,6 +16,7 @@ pub struct ListAppQuery {
pub path_start: Option<String>,
pub include_draft_only: Option<bool>,
pub with_deployment_msg: Option<bool>,
pub label: Option<String>,
}
#[derive(Deserialize)]
+7
View File
@@ -43,6 +43,8 @@ pub struct Flow {
pub visible_to_runner_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_behalf_of_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(Serialize, sqlx::FromRow)]
@@ -81,6 +83,8 @@ pub struct ListableFlow {
#[sqlx(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub deployment_msg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
@@ -100,6 +104,8 @@ pub struct NewFlow {
pub on_behalf_of_email: Option<String>,
pub preserve_on_behalf_of: Option<bool>,
pub ws_error_handler_muted: Option<bool>,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
impl NewFlow {
@@ -1093,6 +1099,7 @@ pub struct ListFlowQuery {
pub include_draft_only: Option<bool>,
pub with_deployment_msg: Option<bool>,
pub dedicated_worker: Option<bool>,
pub label: Option<String>,
}
pub fn add_virtual_items_if_necessary(modules: &mut Vec<FlowModule>) {
+6 -1
View File
@@ -194,6 +194,8 @@ pub struct QueuedJob {
pub preprocessed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runnable_settings_handle: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
impl QueuedJob {
@@ -268,6 +270,7 @@ impl Default for QueuedJob {
priority: None,
preprocessed: None,
runnable_settings_handle: None,
labels: None,
}
}
}
@@ -320,7 +323,7 @@ pub struct CompletedJob {
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<serde_json::Value>,
pub labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preprocessed: Option<bool>,
}
@@ -357,6 +360,7 @@ pub enum JobPayload {
apply_preprocessor: bool,
concurrency_settings: ConcurrencySettings,
debouncing_settings: DebouncingSettings,
labels: Option<Vec<String>>,
},
FlowNode {
id: FlowNodeId,
@@ -410,6 +414,7 @@ pub enum JobPayload {
dedicated_worker: Option<bool>,
apply_preprocessor: bool,
version: i64,
labels: Option<Vec<String>>,
},
RestartedFlow {
completed_job_id: Uuid,
+2
View File
@@ -55,6 +55,8 @@ pub struct Schedule {
pub cron_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_skip: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
impl Schedule {
+8
View File
@@ -377,6 +377,8 @@ pub struct Script<SR> {
#[serde(skip_serializing_if = "Option::is_none")]
#[sqlx(json(nullable))]
pub modules: Option<HashMap<String, ScriptModule>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
#[serde(flatten)]
#[sqlx(flatten)]
pub runnable_settings: SR,
@@ -442,6 +444,8 @@ pub struct ListableScript {
#[serde(skip_serializing_if = "Option::is_none")]
pub deployment_msg: Option<String>,
pub kind: ScriptKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
fn is_false(x: &bool) -> bool {
@@ -517,6 +521,8 @@ pub struct NewScript {
pub modules: Option<HashMap<String, ScriptModule>>,
#[serde(default)]
pub auto_parent: Option<bool>,
#[serde(default)]
pub labels: Option<Vec<String>>,
}
// IMPORTANT: update this Hash impl when adding fields to NewScript
@@ -553,6 +559,7 @@ impl Hash for NewScript {
self.on_behalf_of_email.hash(state);
self.preserve_on_behalf_of.hash(state);
self.assets.hash(state);
self.labels.hash(state);
if let Some(modules) = &self.modules {
let mut sorted: Vec<_> = modules.iter().collect();
sorted.sort_by_key(|(k, _)| *k);
@@ -638,6 +645,7 @@ pub struct ListScriptQuery {
#[serde(default, deserialize_with = "from_seq")]
pub languages: Option<Vec<ScriptLang>>,
pub dedicated_worker: Option<bool>,
pub label: Option<String>,
}
fn from_seq<'de, D>(deserializer: D) -> Result<Option<Vec<ScriptLang>>, D::Error>
@@ -2400,6 +2400,7 @@ pub async fn handle_wac_v2_output(
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
labels: None,
})
} else {
Err(error::Error::internal_err(
@@ -2582,6 +2583,7 @@ pub async fn handle_wac_v2_output(
dedicated_worker: flow_info.dedicated_worker,
apply_preprocessor: false,
version: flow_info.version,
labels: flow_info.labels.clone(),
};
let step_args: HashMap<String, Box<RawValue>> = step
.args
+8 -2
View File
@@ -5090,8 +5090,13 @@ async fn flow_to_payload(
} else {
None
};
let payload =
JobPayload::Flow { path, dedicated_worker: None, apply_preprocessor: false, version };
let payload = JobPayload::Flow {
path,
dedicated_worker: None,
apply_preprocessor: false,
version,
labels: None,
};
Ok(JobPayloadWithTag { payload, tag, delete_after_use, timeout: None, on_behalf_of })
}
@@ -5167,6 +5172,7 @@ pub async fn script_to_payload(
dedicated_worker,
priority,
apply_preprocessor: apply_preprocessor.unwrap_or(false),
labels: None,
},
tag_override.to_owned().or(tag),
delete_after_use,
+1
View File
@@ -478,6 +478,7 @@ export async function handleFile(
on_behalf_of_email: typed?.on_behalf_of_email,
envs: typed?.envs,
modules: modules,
labels: typed?.labels,
};
// console.log(requestBodyCommon.codebase);
+36
View File
@@ -5660,6 +5660,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
gcp_resource_path:
type: string
description: Path to the GCP resource containing service account credentials for
@@ -5757,6 +5761,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
route_path:
type: string
description: The URL route path that will trigger this endpoint (e.g., 'api/myendpoint').
@@ -5887,6 +5895,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
kafka_resource_path:
type: string
description: Path to the Kafka resource containing connection configuration
@@ -5986,6 +5998,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
mqtt_resource_path:
type: string
description: Path to the MQTT resource containing broker connection configuration
@@ -6076,6 +6092,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
nats_resource_path:
type: string
description: Path to the NATS resource containing connection configuration
@@ -6153,6 +6173,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
postgres_resource_path:
type: string
description: Path to the PostgreSQL resource containing connection configuration
@@ -6331,6 +6355,10 @@ properties:
type: string
description: Path to a script that validates scheduled datetimes. Receives scheduled_for
datetime and returns boolean to skip (true) or run (false)
labels:
type: array
items:
type: string
required:
- schedule
- script_path
@@ -6350,6 +6378,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
queue_url:
type: string
description: The full URL of the AWS SQS queue to poll for messages
@@ -6426,6 +6458,10 @@ properties:
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
url:
type: string
description: The WebSocket URL to connect to (can be a static URL or computed
+663
View File
@@ -0,0 +1,663 @@
/**
* Labels Export/Import Tests
*
* Verifies that labels are correctly preserved through workspace export/import
* and CLI sync roundtrips.
*/
import { expect, test, describe } from "bun:test";
import JSZip from "jszip";
import { createScriptFixture } from "./test_fixtures.ts";
describe("labels in workspace export", () => {
test("script metadata with labels includes them in JSON", () => {
const metadata = {
summary: "test script",
description: "a script with labels",
schema: {},
kind: "script",
labels: ["production", "team-alpha"],
};
const json = JSON.stringify(metadata);
const parsed = JSON.parse(json);
expect(parsed.labels).toEqual(["production", "team-alpha"]);
});
test("script metadata without labels omits the field", () => {
const metadata = {
summary: "test script",
description: "a script without labels",
schema: {},
kind: "script",
};
const json = JSON.stringify(metadata);
const parsed = JSON.parse(json);
expect(parsed.labels).toBeUndefined();
});
test("script metadata with null labels omits the field", () => {
const metadata: Record<string, unknown> = {
summary: "test script",
description: "a script with null labels",
schema: {},
kind: "script",
labels: null,
};
// Simulate serde skip_serializing_if behavior
const filtered = Object.fromEntries(
Object.entries(metadata).filter(([_, v]) => v != null)
);
const json = JSON.stringify(filtered);
const parsed = JSON.parse(json);
expect(parsed.labels).toBeUndefined();
});
test("script metadata with empty labels omits the field", () => {
const metadata: Record<string, unknown> = {
summary: "test script",
schema: {},
kind: "script",
labels: [],
};
// Empty arrays should also be omitted
const filtered = Object.fromEntries(
Object.entries(metadata).filter(
([_, v]) => v != null && !(Array.isArray(v) && v.length === 0)
)
);
const json = JSON.stringify(filtered);
const parsed = JSON.parse(json);
expect(parsed.labels).toBeUndefined();
});
test("labels survive zip roundtrip", async () => {
const scriptMeta = {
summary: "labeled script",
description: "",
schema: {},
kind: "script",
labels: ["deploy", "critical"],
};
// Create zip with script metadata
const zip = new JSZip();
zip.file("u/admin/my_script.py", 'def main():\n return "hello"');
zip.file(
"u/admin/my_script.script.json",
JSON.stringify(scriptMeta)
);
// Read back
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const metaStr = await loaded
.file("u/admin/my_script.script.json")!
.async("text");
const parsed = JSON.parse(metaStr);
expect(parsed.labels).toEqual(["deploy", "critical"]);
expect(parsed.summary).toEqual("labeled script");
});
test("flow with labels survives zip roundtrip", async () => {
const flowData = {
summary: "labeled flow",
description: "a flow with labels",
value: { modules: [] },
schema: {},
tag: "mytag",
labels: ["staging", "nightly"],
};
const zip = new JSZip();
zip.file("u/admin/my_flow.flow.json", JSON.stringify(flowData));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const metaStr = await loaded
.file("u/admin/my_flow.flow.json")!
.async("text");
const parsed = JSON.parse(metaStr);
expect(parsed.labels).toEqual(["staging", "nightly"]);
expect(parsed.tag).toEqual("mytag");
});
test("resource with labels survives zip roundtrip", async () => {
const resource = {
value: { key: "value" },
description: "test resource",
resource_type: "postgresql",
labels: ["prod-db"],
};
const zip = new JSZip();
zip.file("u/admin/my_db.resource.json", JSON.stringify(resource));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const str = await loaded
.file("u/admin/my_db.resource.json")!
.async("text");
const parsed = JSON.parse(str);
expect(parsed.labels).toEqual(["prod-db"]);
});
test("variable with labels survives zip roundtrip", async () => {
const variable = {
value: "secret123",
is_secret: false,
description: "test variable",
labels: ["env", "config"],
};
const zip = new JSZip();
zip.file("u/admin/my_var.variable.json", JSON.stringify(variable));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const str = await loaded
.file("u/admin/my_var.variable.json")!
.async("text");
const parsed = JSON.parse(str);
expect(parsed.labels).toEqual(["env", "config"]);
});
test("schedule with labels survives zip roundtrip", async () => {
const schedule = {
schedule: "0 0 * * *",
timezone: "UTC",
script_path: "u/admin/my_script",
is_flow: false,
summary: "daily run",
labels: ["cron", "daily"],
};
const zip = new JSZip();
zip.file(
"u/admin/daily_schedule.schedule.json",
JSON.stringify(schedule)
);
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const str = await loaded
.file("u/admin/daily_schedule.schedule.json")!
.async("text");
const parsed = JSON.parse(str);
expect(parsed.labels).toEqual(["cron", "daily"]);
});
test("items without labels don't have the field in exported JSON", async () => {
const zip = new JSZip();
zip.file(
"u/admin/no_labels.flow.json",
JSON.stringify({
summary: "no labels flow",
description: "",
value: { modules: [] },
schema: {},
})
);
zip.file(
"u/admin/no_labels.resource.json",
JSON.stringify({
value: {},
description: "",
resource_type: "c_test",
})
);
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const flowStr = await loaded
.file("u/admin/no_labels.flow.json")!
.async("text");
expect(JSON.parse(flowStr).labels).toBeUndefined();
const resStr = await loaded
.file("u/admin/no_labels.resource.json")!
.async("text");
expect(JSON.parse(resStr).labels).toBeUndefined();
});
});
describe("labels in script metadata YAML", () => {
test("createScriptFixture does not include labels by default", () => {
const fixture = createScriptFixture("test", "bun");
expect(fixture.metadataFile.content).not.toContain("labels");
});
test("labels in YAML metadata are preserved through parse", () => {
const yaml = `summary: "labeled script"
description: "test"
schema: {}
kind: script
labels:
- production
- team-alpha
`;
// YAML parse would produce { labels: ["production", "team-alpha"] }
// This is a structural test — actual YAML parsing is done by the CLI
expect(yaml).toContain("labels:");
expect(yaml).toContain(" - production");
expect(yaml).toContain(" - team-alpha");
});
});
describe("labels push request construction", () => {
test("script push includes labels from metadata", () => {
const typed = {
summary: "test",
description: "test desc",
schema: {},
kind: "script" as const,
labels: ["deploy"],
};
// Simulate how handleFile builds requestBodyCommon
const requestBody = {
content: 'export async function main() { return 1; }',
description: typed?.description ?? "",
language: "bun" as const,
path: "u/admin/test",
summary: typed?.summary ?? "",
kind: typed?.kind,
labels: typed?.labels,
};
expect(requestBody.labels).toEqual(["deploy"]);
});
test("script push omits labels when not in metadata", () => {
const typed = {
summary: "test",
description: "test desc",
schema: {},
kind: "script" as const,
};
const requestBody = {
content: 'export async function main() { return 1; }',
description: typed?.description ?? "",
language: "bun" as const,
path: "u/admin/test",
summary: typed?.summary ?? "",
kind: typed?.kind,
labels: (typed as any)?.labels,
};
expect(requestBody.labels).toBeUndefined();
});
test("flow push preserves labels via spread", () => {
const localFlow = {
summary: "test flow",
description: "",
value: { modules: [] },
schema: {},
labels: ["staging"],
};
const requestBody = {
path: "u/admin/test_flow",
deployment_message: "test deploy",
...localFlow,
};
expect(requestBody.labels).toEqual(["staging"]);
});
test("resource push preserves labels via spread", () => {
const localResource = {
value: { host: "localhost" },
description: "test db",
resource_type: "postgresql",
labels: ["prod"],
};
const requestBody = {
path: "u/admin/test_resource",
...localResource,
};
expect(requestBody.labels).toEqual(["prod"]);
});
test("variable push preserves labels via spread", () => {
const localVariable = {
value: "secret",
is_secret: true,
description: "test var",
labels: ["env"],
};
const requestBody = {
path: "u/admin/test_var",
...localVariable,
};
expect(requestBody.labels).toEqual(["env"]);
});
test("schedule push preserves labels via spread", () => {
const localSchedule = {
schedule: "0 * * * *",
timezone: "UTC",
script_path: "u/admin/my_script",
is_flow: false,
labels: ["hourly"],
};
const requestBody = {
path: "u/admin/my_schedule",
...localSchedule,
};
expect(requestBody.labels).toEqual(["hourly"]);
});
});
describe("labels edit and removal", () => {
test("adding a label to existing metadata preserves other fields", () => {
const original = {
summary: "my script",
description: "does things",
schema: {},
kind: "script" as const,
lock: "pkg==1.0",
};
// Simulate editing: add labels
const edited = { ...original, labels: ["new-label"] };
expect(edited.summary).toEqual("my script");
expect(edited.description).toEqual("does things");
expect(edited.lock).toEqual("pkg==1.0");
expect(edited.labels).toEqual(["new-label"]);
});
test("removing all labels results in undefined (not empty array)", () => {
const withLabels = {
summary: "test",
labels: ["a", "b"],
};
// Simulate removal: set to undefined so it's omitted from JSON
const cleared = { ...withLabels, labels: undefined };
const json = JSON.stringify(cleared);
const parsed = JSON.parse(json);
expect(parsed.labels).toBeUndefined();
expect(parsed.summary).toEqual("test");
});
test("setting labels to empty array serializes as empty array", () => {
const withLabels = {
summary: "test",
labels: [] as string[],
};
const json = JSON.stringify(withLabels);
const parsed = JSON.parse(json);
expect(parsed.labels).toEqual([]);
});
test("modifying labels preserves order and deduplicates", () => {
const labels = ["prod", "staging"];
// Add a new one
const added = [...labels, "dev"];
expect(added).toEqual(["prod", "staging", "dev"]);
// Remove one
const removed = added.filter((l) => l !== "staging");
expect(removed).toEqual(["prod", "dev"]);
// No duplicates
const withDup = [...removed, "prod"];
const deduped = [...new Set(withDup)];
expect(deduped).toEqual(["prod", "dev"]);
});
test("labels survive JSON roundtrip with special characters", () => {
const labels = ["my-label", "team_alpha", "v1.0", "with spaces"];
const json = JSON.stringify({ labels });
const parsed = JSON.parse(json);
expect(parsed.labels).toEqual(labels);
});
});
describe("labels in pull/export for all item types", () => {
test("script pull includes labels in metadata file", async () => {
const zip = new JSZip();
const metadata = {
summary: "pulled script",
description: "",
schema: {},
kind: "script",
labels: ["from-remote"],
};
zip.file("u/admin/pulled.py", "def main(): pass");
zip.file("u/admin/pulled.script.json", JSON.stringify(metadata));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const meta = JSON.parse(
await loaded.file("u/admin/pulled.script.json")!.async("text")
);
expect(meta.labels).toEqual(["from-remote"]);
expect(meta.summary).toEqual("pulled script");
});
test("flow pull includes labels", async () => {
const zip = new JSZip();
const flow = {
summary: "pulled flow",
description: "",
value: { modules: [] },
schema: {},
labels: ["ci", "nightly"],
};
zip.file("u/admin/pulled.flow.json", JSON.stringify(flow));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const parsed = JSON.parse(
await loaded.file("u/admin/pulled.flow.json")!.async("text")
);
expect(parsed.labels).toEqual(["ci", "nightly"]);
});
test("app pull includes labels", async () => {
const zip = new JSZip();
const app = {
summary: "pulled app",
value: {},
policy: {},
labels: ["dashboard"],
};
zip.file("u/admin/pulled.app.json", JSON.stringify(app));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const parsed = JSON.parse(
await loaded.file("u/admin/pulled.app.json")!.async("text")
);
expect(parsed.labels).toEqual(["dashboard"]);
});
test("resource pull includes labels", async () => {
const zip = new JSZip();
const resource = {
value: { host: "db.example.com" },
description: "production db",
resource_type: "postgresql",
labels: ["prod", "db"],
};
zip.file("u/admin/my_db.resource.json", JSON.stringify(resource));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const parsed = JSON.parse(
await loaded.file("u/admin/my_db.resource.json")!.async("text")
);
expect(parsed.labels).toEqual(["prod", "db"]);
});
test("variable pull includes labels", async () => {
const zip = new JSZip();
const variable = {
value: "api-key-123",
is_secret: false,
description: "api key",
labels: ["config"],
};
zip.file("u/admin/api_key.variable.json", JSON.stringify(variable));
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const parsed = JSON.parse(
await loaded.file("u/admin/api_key.variable.json")!.async("text")
);
expect(parsed.labels).toEqual(["config"]);
});
test("schedule pull includes labels", async () => {
const zip = new JSZip();
const schedule = {
schedule: "0 0 * * *",
timezone: "UTC",
script_path: "u/admin/daily",
is_flow: false,
summary: "daily cleanup",
labels: ["cron", "maintenance"],
};
zip.file(
"u/admin/daily_cleanup.schedule.json",
JSON.stringify(schedule)
);
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
const parsed = JSON.parse(
await loaded
.file("u/admin/daily_cleanup.schedule.json")!
.async("text")
);
expect(parsed.labels).toEqual(["cron", "maintenance"]);
});
test("pull without labels doesn't include labels field", async () => {
const zip = new JSZip();
zip.file(
"u/admin/no_labels.script.json",
JSON.stringify({ summary: "no labels", schema: {}, kind: "script" })
);
zip.file(
"u/admin/no_labels.flow.json",
JSON.stringify({ summary: "no labels", value: { modules: [] } })
);
zip.file(
"u/admin/no_labels.resource.json",
JSON.stringify({ value: {}, resource_type: "c_test" })
);
zip.file(
"u/admin/no_labels.variable.json",
JSON.stringify({ value: "x", is_secret: false, description: "" })
);
zip.file(
"u/admin/no_labels.schedule.json",
JSON.stringify({
schedule: "0 0 * * *",
timezone: "UTC",
script_path: "u/admin/x",
is_flow: false,
})
);
const content = await zip.generateAsync({ type: "uint8array" });
const loaded = await JSZip.loadAsync(content);
for (const name of [
"u/admin/no_labels.script.json",
"u/admin/no_labels.flow.json",
"u/admin/no_labels.resource.json",
"u/admin/no_labels.variable.json",
"u/admin/no_labels.schedule.json",
]) {
const parsed = JSON.parse(await loaded.file(name)!.async("text"));
expect(parsed.labels).toBeUndefined();
}
});
test("editing labels on pulled item and re-pushing preserves changes", () => {
// Simulate: pull -> edit labels -> push
const pulled = {
summary: "my script",
schema: {},
kind: "script" as const,
labels: ["old-label"],
};
// User edits labels
const edited = { ...pulled, labels: ["new-label", "another"] };
// Push constructs request body
const requestBody = {
content: "def main(): pass",
path: "u/admin/my_script",
summary: edited.summary,
kind: edited.kind,
labels: edited.labels,
};
expect(requestBody.labels).toEqual(["new-label", "another"]);
expect(requestBody.labels).not.toContain("old-label");
});
test("removing all labels on pulled item clears them on push", () => {
const pulled = {
summary: "labeled script",
schema: {},
kind: "script" as const,
labels: ["to-remove"],
};
// User removes all labels
const edited = { ...pulled };
delete (edited as any).labels;
const requestBody = {
content: "def main(): pass",
path: "u/admin/my_script",
summary: edited.summary,
kind: edited.kind,
labels: (edited as any).labels,
};
expect(requestBody.labels).toBeUndefined();
});
});
+1
View File
@@ -0,0 +1 @@
87fcd9a7e12e9b1ad8e5e27dddca466a8e2b2e5c
@@ -2,6 +2,7 @@
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import LabelsInput from './LabelsInput.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import {
OauthService,
@@ -109,6 +110,7 @@
let responseExtra: Record<string, string> = $state({})
let path: string = $state('')
let description = $state('')
let labels: string[] | undefined = $state(undefined)
/**
* Client credentials OAuth flow support
@@ -144,6 +146,7 @@
step = 1 //express && !manual ? 3 : 1
value = ''
description = ''
labels = undefined
resourceType = rt ?? ''
valueToken = undefined
@@ -568,7 +571,8 @@
resource_type: resourceType,
path,
value: resourceValue,
description
description,
labels
}
})
dispatch('refresh', path)
@@ -734,6 +738,7 @@
namePlaceholder={resourceType}
kind="resource"
/>
<LabelsInput bind:labels class="-mt-5" />
{#if apiTokenApps[resourceType]}
<h2 class="mt-4 mb-2">Instructions</h2>
@@ -836,6 +841,8 @@
</div>
{/if}
<LabelsInput bind:labels class="-mt-5" />
{#if supportsClientCredentials}
<div>
<h3 class="text-sm font-semibold text-emphasis mb-1">Authentication Method</h3>
@@ -932,6 +939,7 @@
bind:path
kind="resource"
/>
<LabelsInput bind:labels class="-mt-5" />
{#if apiTokenApps[resourceType] || !manual}
<ul class="mt-6">
<li class="text-xs text-primary font-normal">
@@ -531,12 +531,34 @@
type Preset = { name: string; value: string }
let presets: Preset[] = $derived(
_presets.filter((p) => {
// Only show presets that aren't already applied in asText
return !asText.val.includes(p.value)
// Only show presets that aren't already applied
if (asText.val.includes(p.value)) return false
// For allowMultiple: check if the value is already in the comma-separated list
const match = p.value.match(/^(\w+):\\\s+(.+)$/)
if (match) {
const [, key, val] = match
const existing = String(value[key] ?? '')
if (existing.split(',').some((v) => v.trim() === val)) return false
}
return true
})
)
function appendFilterAsText(presetValue: string) {
// For allowMultiple fields, append value with comma to existing filter instead of adding duplicate key
const match = presetValue.match(/^(\w+):\\\s+(.+)$/)
if (match) {
const [, key, newVal] = match
const filterDef = schema[key]
if (filterDef?.allowMultiple) {
const existing = value[key]
if (existing) {
value[key] = existing + ',' + newVal
asText.reparse()
return
}
}
}
if (!asText.val.endsWith('\u00A0') && !asText.val.endsWith(' ')) asText.val += ' '
asText.val += presetValue + '\u00A0'
}
@@ -327,7 +327,8 @@
draft_only: true,
ws_error_handler_muted: flow.ws_error_handler_muted,
visible_to_runner_only: flow.visible_to_runner_only,
on_behalf_of_email: flow.on_behalf_of_email
on_behalf_of_email: flow.on_behalf_of_email,
labels: (flow as any).labels
}
})
}
@@ -499,7 +500,8 @@
visible_to_runner_only: flow.visible_to_runner_only,
on_behalf_of_email: flow.on_behalf_of_email,
preserve_on_behalf_of: $preserveOnBehalfOf || undefined,
deployment_message: deploymentMsg || undefined
deployment_message: deploymentMsg || undefined,
labels: (flow as any).labels
}
})
await CaptureService.moveCapturesAndConfigs({
@@ -552,7 +554,8 @@
visible_to_runner_only: flow.visible_to_runner_only,
on_behalf_of_email: flow.on_behalf_of_email,
preserve_on_behalf_of: $preserveOnBehalfOf || undefined,
deployment_message: deploymentMsg || undefined
deployment_message: deploymentMsg || undefined,
labels: (flow as any).labels
}
})
}
@@ -0,0 +1,165 @@
<script lang="ts">
import Badge from './common/badge/Badge.svelte'
import { Plus, Tag, X } from 'lucide-svelte'
import { workspaceStore } from '$lib/stores'
interface Props {
labels: string[] | undefined
onchange?: () => void
class?: string
}
let { labels = $bindable(), onchange, class: clazz = '' }: Props = $props()
let adding = $state(false)
let inputValue = $state('')
let inputEl: HTMLInputElement | undefined = $state()
let existingLabels: string[] = $state([])
let selectedIdx = $state(-1)
let suggestions = $derived(
existingLabels
.filter(
(l) =>
(!inputValue || l.toLowerCase().includes(inputValue.toLowerCase())) &&
!(labels ?? []).includes(l)
)
.slice(0, 8)
)
let trimmedInput = $derived(inputValue.trim())
let showCreateNew = $derived(
trimmedInput.length > 0 &&
!suggestions.some((s) => s.toLowerCase() === trimmedInput.toLowerCase()) &&
!(labels ?? []).includes(trimmedInput)
)
async function loadExistingLabels() {
try {
const resp = await fetch(`/api/w/${$workspaceStore}/labels/list`)
if (resp.ok) existingLabels = await resp.json()
} catch {}
}
function startAdding() {
adding = true
inputValue = ''
selectedIdx = -1
loadExistingLabels()
setTimeout(() => inputEl?.focus(), 0)
}
function addLabel(value?: string) {
const v = (value ?? inputValue).trim().slice(0, 50)
if (!v) {
adding = false
return
}
if (!labels) {
labels = []
}
if (!labels.includes(v)) {
labels = [...labels, v]
onchange?.()
}
inputValue = ''
adding = false
}
function removeLabel(label: string) {
if (labels) {
labels = labels.filter((l) => l !== label)
onchange?.()
}
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault()
if (selectedIdx >= 0 && selectedIdx < suggestions.length) {
addLabel(suggestions[selectedIdx])
} else {
addLabel() // either "Create new" selected or free text
}
} else if (e.key === 'Escape') {
inputValue = ''
adding = false
} else if (e.key === 'ArrowDown') {
e.preventDefault()
const maxIdx = suggestions.length + (showCreateNew ? 1 : 0) - 1
selectedIdx = Math.min(selectedIdx + 1, maxIdx)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
selectedIdx = Math.max(selectedIdx - 1, -1)
}
}
function onBlur() {
// Delay to allow click on suggestion
setTimeout(() => {
if (adding) addLabel()
}, 150)
}
</script>
<div class="inline-flex items-center gap-1 ml-0.5 h-5 {clazz}">
{#each labels ?? [] as label (label)}
<Badge color="blue" small>
{label}
<button class="ml-0.5 hover:text-red-500" onclick={() => removeLabel(label)}>
<X size={10} />
</button>
</Badge>
{/each}
{#if adding}
<div class="relative">
<input
bind:this={inputEl}
bind:value={inputValue}
onkeydown={onKeydown}
onblur={onBlur}
class="text-2xs border border-blue-300 rounded px-1.5 py-0 h-5 max-w-32 outline-none focus:ring-1 focus:ring-blue-400"
placeholder="label"
/>
{#if suggestions.length > 0 || showCreateNew}
<div
class="absolute top-6 left-0 z-50 bg-surface border border-light rounded shadow-md max-h-32 overflow-y-auto min-w-32"
>
{#each suggestions as suggestion, i}
<button
class="w-full text-left text-2xs px-2 py-1 hover:bg-surface-hover {i === selectedIdx
? 'bg-surface-hover'
: ''}"
onmousedown={(e) => {
e.preventDefault()
addLabel(suggestion)
}}
>
{suggestion}
</button>
{/each}
{#if showCreateNew}
<button
class="w-full text-left text-2xs px-2 py-1 hover:bg-surface-hover text-blue-600 {selectedIdx ===
suggestions.length
? 'bg-surface-hover'
: ''}"
onmousedown={(e) => {
e.preventDefault()
addLabel()
}}
>
+ Create "{trimmedInput}"
</button>
{/if}
</div>
{/if}
</div>
{:else}
<button
class="text-tertiary hover:text-secondary text-2xs flex items-center gap-0.5"
onclick={startAdding}
>
<Tag size={12} /><Plus size={8} />
</button>
{/if}
</div>
@@ -7,6 +7,7 @@
import { createEventDispatcher, untrack } from 'svelte'
import { Alert, Skeleton } from './common'
import Path from './Path.svelte'
import LabelsInput from './LabelsInput.svelte'
import Required from './Required.svelte'
import { userStore, workspaceStore } from '$lib/stores'
@@ -58,6 +59,7 @@
let resourceToEdit: Resource | undefined = $state(undefined)
let description: string = $state('')
let labels: string[] | undefined = $state(undefined)
let DESCRIPTION_PLACEHOLDER = `Describe what this resource is for`
let resourceSchema: Schema | undefined = $state(undefined)
let args: Record<string, any> = $state({})
@@ -75,6 +77,7 @@
async function initEdit() {
resourceToEdit = await ResourceService.getResource({ workspace: effectiveWorkspace, path })
description = resourceToEdit!.description ?? ''
labels = resourceToEdit!.labels ?? undefined
resource_type = resourceToEdit!.resource_type
args = resourceToEdit?.value ?? ({} as any)
loadResourceType()
@@ -99,7 +102,7 @@
await ResourceService.updateResource({
workspace: effectiveWorkspace,
path: resourceToEdit.path,
requestBody: { path, value: args, description }
requestBody: { path, value: args, description, labels }
})
if (resourceToEdit.resource_type === 'json_schema') {
clearJsonSchemaResourceCache(resourceToEdit.path, effectiveWorkspace)
@@ -114,7 +117,7 @@
export async function createResource(): Promise<void> {
await ResourceService.createResource({
workspace: effectiveWorkspace,
requestBody: { path, value: args, description, resource_type: resource_type! }
requestBody: { path, value: args, description, resource_type: resource_type!, labels }
})
sendUserToast(`Updated resource at ${path}`)
dispatch('refresh', path)
@@ -224,6 +227,7 @@
/>
</div>
{/if}
<LabelsInput bind:labels class="-mt-4" />
{#if !emptyString(resourceTypeInfo?.description)}
<div class="flex flex-col gap-1">
@@ -82,6 +82,7 @@
import DefaultScripts from './DefaultScripts.svelte'
import { onMount, setContext, untrack } from 'svelte'
import Summary from './Summary.svelte'
import LabelsInput from './LabelsInput.svelte'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import TriggersEditor from './triggers/TriggersEditor.svelte'
@@ -607,7 +608,8 @@
on_behalf_of_email: script.on_behalf_of_email,
preserve_on_behalf_of: preserveOnBehalfOf || undefined,
assets: script.assets,
modules: script.modules
modules: script.modules,
labels: script.labels
}
})
@@ -765,7 +767,8 @@
has_preprocessor: script.has_preprocessor,
on_behalf_of_email: script.on_behalf_of_email,
assets: script.assets,
modules: script.modules
modules: script.modules,
labels: script.labels
}
})
}
@@ -1195,6 +1198,7 @@
}}
/>
</Label>
<LabelsInput bind:labels={script.labels} class="-mt-4" />
<Label label="Path">
{#snippet header()}
<Tooltip
@@ -8,10 +8,13 @@
import { sendUserToast } from '$lib/toast'
import { updateItemPathAndSummary, checkFlowOnBehalfOf } from './moveRenameManager'
import Label from './Label.svelte'
import LabelsInput from './LabelsInput.svelte'
import Badge from './common/badge/Badge.svelte'
interface Props {
summary?: string
path?: string
labels?: string[] | undefined
editable?: boolean
onSaved?: (newPath: string) => void
kind?: 'flow' | 'script'
@@ -20,6 +23,7 @@
let {
summary = $bindable(''),
path = $bindable(''),
labels = $bindable(),
editable = false,
onSaved,
kind = 'flow'
@@ -32,12 +36,14 @@
let own = $state(false)
let onBehalfOfEmail = $state<string | undefined>(undefined)
let summaryInput: ReturnType<typeof TextInput> | undefined = $state()
let hasChanges = $derived(editSummary !== (summary ?? '') || (own && dirtyPath))
let labelsDirty = $state(false)
let hasChanges = $derived(editSummary !== (summary ?? '') || (own && dirtyPath) || labelsDirty)
$effect(() => {
if (popoverOpen && onSaved) {
editSummary = summary ?? ''
editPath = path ?? ''
labelsDirty = false
own = isOwner(path ?? '', $userStore, $workspaceStore)
onBehalfOfEmail = undefined
if (kind === 'flow' && $workspaceStore && path) {
@@ -58,9 +64,11 @@
kind,
initialPath,
newPath,
newSummary: editSummary
newSummary: editSummary,
labels
})
sendUserToast(`${kind === 'flow' ? 'Flow' : 'Script'} updated`)
labelsDirty = false
close()
onSaved?.(newPath)
} catch (e: any) {
@@ -90,13 +98,22 @@
<span class="text-2xs leading-tight text-tertiary font-mono font-normal truncate max-w-full"
>{path}</span
>
<span
class="text-sm font-semibold truncate max-w-full {emptyString(summary)
? 'text-tertiary italic font-normal'
: 'text-emphasis'}"
>
{emptyString(summary) ? 'Add a summary...' : summary}
</span>
<div class="flex items-center gap-3 max-w-full">
<span
class="text-sm font-semibold truncate {emptyString(summary)
? 'text-tertiary italic font-normal'
: 'text-emphasis'}"
>
{emptyString(summary) ? 'Add a summary...' : summary}
</span>
{#if labels?.length}
<div class="flex items-center gap-0.5">
{#each labels as label}
<Badge color="blue" verySmall class="px-1" title="Label: {label}">{label}</Badge>
{/each}
</div>
{/if}
</div>
</div>
{/snippet}
{#snippet content({ close })}
@@ -117,6 +134,13 @@
bind:value={editSummary}
/>
</Label>
<LabelsInput
bind:labels
class="-mt-4"
onchange={() => {
labelsDirty = true
}}
/>
<Label label="Path">
{#if own}
<Path
@@ -1,6 +1,7 @@
<script lang="ts">
import { VariableService } from '$lib/gen'
import Path from './Path.svelte'
import LabelsInput from './LabelsInput.svelte'
import { createEventDispatcher } from 'svelte'
import { userStore, workspaceStore } from '$lib/stores'
import Tooltip from './Tooltip.svelte'
@@ -30,6 +31,7 @@
description: ''
})
let valid = $state(true)
let labels: string[] | undefined = $state(undefined)
let drawer: Drawer | undefined = $state()
let edit = $state(false)
@@ -46,6 +48,7 @@
edit = false
initialPath = ''
path = ''
labels = undefined
can_write = true
drawer?.openDrawer()
}
@@ -66,6 +69,7 @@
is_secret: getV.is_secret,
description: getV.description ?? ''
}
labels = getV.labels ?? undefined
initialPath = edit_path
path = edit_path
drawer?.openDrawer()
@@ -94,7 +98,8 @@
path,
value: variable.value,
is_secret: variable.is_secret,
description: variable.description
description: variable.description,
labels
}
})
sendUserToast(`Created variable ${path}`)
@@ -117,7 +122,8 @@
path: getV.path != path ? path : undefined,
value: variable.value == '' ? undefined : variable.value,
is_secret: getV.is_secret != variable.is_secret ? variable.is_secret : undefined,
description: getV.description != variable.description ? variable.description : undefined
description: getV.description != variable.description ? variable.description : undefined,
labels
}
})
sendUserToast(`Updated variable ${initialPath}`)
@@ -153,6 +159,7 @@
namePlaceholder="variable"
kind="variable"
/>
<LabelsInput bind:labels />
</div>
<label class="flex flex-col gap-1">
<span class="text-xs font-semibold text-emphasis">Secret</span>
@@ -96,6 +96,24 @@
{/if}
<SharedBadge canWrite={app.canWrite} extraPerms={app.extra_perms} />
<DraftBadge has_draft={app.has_draft} draft_only={app.draft_only} />
{#if app.labels?.length}
<div class="flex items-center gap-0.5">
{#each app.labels.slice(0, 3) as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{#if app.labels.length > 3}
<Badge
color="blue"
small
class="px-1"
title={app.labels
.slice(3)
.map((l) => 'Label: ' + l)
.join('\n')}>+{app.labels.length - 3}</Badge
>
{/if}
</div>
{/if}
<div class="w-8 center-center"></div>
{/snippet}
{#snippet actions()}
@@ -119,6 +119,24 @@
{/if}
<SharedBadge canWrite={flow.canWrite} extraPerms={flow.extra_perms} />
<DraftBadge has_draft={flow.has_draft} draft_only={flow.draft_only} />
{#if flow.labels?.length}
<div class="flex items-center gap-0.5">
{#each flow.labels.slice(0, 3) as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{#if flow.labels.length > 3}
<Badge
color="blue"
small
class="px-1"
title={flow.labels
.slice(3)
.map((l) => 'Label: ' + l)
.join('\n')}>+{flow.labels.length - 3}</Badge
>
{/if}
</div>
{/if}
<div class="w-8 center-center"></div>
{/snippet}
{#snippet actions()}
@@ -158,6 +158,24 @@
{/if}
<SharedBadge canWrite={script.canWrite} extraPerms={script.extra_perms} />
<DraftBadge has_draft={script.has_draft} draft_only={script.draft_only} />
{#if script.labels?.length}
<div class="flex items-center gap-0.5">
{#each script.labels.slice(0, 3) as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{#if script.labels.length > 3}
<Badge
color="blue"
small
class="px-1"
title={script.labels
.slice(3)
.map((l) => 'Label: ' + l)
.join('\n')}>+{script.labels.length - 3}</Badge
>
{/if}
</div>
{/if}
<div class="w-8 center-center">
<LanguageIcon lang={script.language} width={16} height={16} />
</div>
@@ -35,6 +35,7 @@
errorHandlerKind: 'flow' | 'script'
scriptOrFlowPath: string
errorHandlerMuted: boolean | undefined
labels?: string[] | undefined
onSaved?: (newPath: string) => void
children?: import('svelte').Snippet
trigger_badges?: import('svelte').Snippet
@@ -49,6 +50,7 @@
errorHandlerKind,
scriptOrFlowPath,
errorHandlerMuted = $bindable(),
labels = $bindable(),
onSaved,
children,
trigger_badges
@@ -64,7 +66,7 @@
>
<div class="grow px-2 inline-flex items-center gap-4 min-w-0">
<div class={twMerge('min-w-0', $userStore?.operator ? 'pl-10' : '')}>
<SummaryPathDisplay {summary} {path} {onSaved} kind={errorHandlerKind} />
<SummaryPathDisplay {summary} {path} bind:labels {onSaved} kind={errorHandlerKind} />
</div>
{#if tag}
<Badge>tag: {tag}</Badge>
@@ -20,6 +20,7 @@
import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte'
import MetadataGen from '$lib/components/copilot/MetadataGen.svelte'
import Badge from '$lib/components/Badge.svelte'
import LabelsInput from '$lib/components/LabelsInput.svelte'
import AIFormSettings from '$lib/components/copilot/AIFormSettings.svelte'
import { twMerge } from 'tailwind-merge'
import { inputBaseClass, inputBorderClass } from '$lib/components/text_input/TextInput.svelte'
@@ -124,6 +125,7 @@
}}
/>
</Label>
<LabelsInput bind:labels={(flowStore.val as any).labels} class="-mt-4" />
{#if !noEditor}
<Label label="Path">
@@ -172,7 +172,8 @@ export async function saveScheduleFromCfg(
cron_version: scheduleCfg.cron_version,
dynamic_skip: scheduleCfg.dynamic_skip,
permissioned_as: scheduleCfg.permissioned_as,
preserve_permissioned_as: scheduleCfg.preserve_permissioned_as
preserve_permissioned_as: scheduleCfg.preserve_permissioned_as,
labels: scheduleCfg.labels
}
try {
if (edit) {
@@ -1,6 +1,6 @@
<script lang="ts">
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, Skeleton } from '$lib/components/common'
import { Badge, Button, Skeleton } from '$lib/components/common'
import Toggle from '$lib/components/Toggle.svelte'
import {
AppService,
@@ -19,7 +19,8 @@
Code2,
LayoutDashboard,
ListFilterPlus,
SearchCode
SearchCode,
Tag
} from 'lucide-svelte'
import { HOME_SEARCH_SHOW_FLOW, HOME_SEARCH_PLACEHOLDER } from '$lib/consts'
@@ -155,6 +156,7 @@
}
let ownerFilter: string | undefined = $state(undefined)
let labelFilter: string | undefined = $state(undefined)
const cmp = new Intl.Collator('en').compare
@@ -296,9 +298,15 @@
a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1
)
)
let allLabels = $derived(
Array.from(
new Set(combinedItems?.flatMap((x) => ('labels' in x && x.labels) || []) ?? [])
).sort()
)
$effect(() => {
if ($workspaceStore) {
ownerFilter = undefined
labelFilter = undefined
}
})
let preFilteredItems = $derived(
@@ -307,12 +315,14 @@
(x) =>
x.path.startsWith(ownerFilter + '/') &&
(x.type == itemKind || itemKind == 'all') &&
filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType)
filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType) &&
(labelFilter == undefined || ('labels' in x && x.labels?.includes(labelFilter)))
)
: combinedItems?.filter(
(x) =>
(x.type == itemKind || itemKind == 'all') &&
filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType)
filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType) &&
(labelFilter == undefined || ('labels' in x && x.labels?.includes(labelFilter)))
)
)
let items = $derived(filter !== '' ? filteredItems : preFilteredItems)
@@ -450,6 +460,25 @@
filters={owners}
bottomMargin={false}
/>
{#if allLabels.length > 0}
<div class="gap-1.5 w-full flex flex-wrap mt-2">
{#each allLabels as label (label)}
<Badge
color="blue"
small
clickable
selected={label === labelFilter}
title="Label: {label}"
onclick={() => {
labelFilter = labelFilter === label ? undefined : label
}}
>
<Tag size={10} class="inline -mt-px" />{label}
{#if label === labelFilter}&cross;{/if}
</Badge>
{/each}
</div>
{/if}
{#if filteredItems?.length == 0}
<div class="mt-10"></div>
{/if}
@@ -27,8 +27,9 @@ export async function updateItemPathAndSummary(opts: {
initialPath: string
newPath: string
newSummary: string
labels?: string[]
}): Promise<void> {
const { workspace, kind, initialPath, newPath, newSummary } = opts
const { workspace, kind, initialPath, newPath, newSummary, labels } = opts
if (kind === 'flow') {
const flow = await FlowService.getFlowByPath({ workspace, path: initialPath })
@@ -44,7 +45,8 @@ export async function updateItemPathAndSummary(opts: {
tag: flow.tag,
dedicated_worker: flow.dedicated_worker,
ws_error_handler_muted: flow.ws_error_handler_muted,
visible_to_runner_only: flow.visible_to_runner_only
visible_to_runner_only: flow.visible_to_runner_only,
labels
}
})
} else if (kind === 'script') {
@@ -57,7 +59,8 @@ export async function updateItemPathAndSummary(opts: {
description: script.description ?? '',
lock: script.lock,
parent_hash: script.hash,
path: newPath
path: newPath,
labels
}
})
} else if (kind === 'app') {
@@ -66,7 +69,8 @@ export async function updateItemPathAndSummary(opts: {
path: initialPath,
requestBody: {
path: newPath !== initialPath ? newPath : undefined,
summary: newSummary
summary: newSummary,
labels
}
})
}
@@ -1,16 +1,18 @@
import { Boxes, FileCode, FileText, FolderIcon, Braces, Users } from 'lucide-svelte'
import { Boxes, FileCode, FileText, FolderIcon, Braces, Users, Tag } from 'lucide-svelte'
import type { FilterSchemaRec } from '../FilterSearchbar.svelte'
export function buildResourcesFilterSchema({
paths,
resourceTypes,
owners,
labels,
showUserFoldersFilter,
userFoldersLabel
}: {
paths: string[]
resourceTypes: string[]
owners: string[]
labels?: string[]
showUserFoldersFilter?: boolean
userFoldersLabel?: string
}) {
@@ -65,6 +67,15 @@ export function buildResourcesFilterSchema({
icon: Braces,
description: 'Filter by JSON subset match (e.g., {"bucket": "my-bucket"})'
},
label: {
type: 'oneof' as const,
options: (labels ?? []).map((s) => ({ label: s, value: s })),
allowNegative: false,
allowMultiple: true,
label: 'Label',
icon: Tag,
description: 'Filter by label (comma-separated for multiple)'
},
...(showUserFoldersFilter
? {
user_folders_only: {
@@ -7,7 +7,7 @@
import { workspaceStore } from '$lib/stores'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { ListFilter } from 'lucide-svelte'
import { ListFilter, Tag } from 'lucide-svelte'
import type { Job } from '$lib/gen'
interface Props {
@@ -46,7 +46,8 @@
{/if}
{#if job && 'job_kind' in job}
<div>
<Badge color="gray" {large}>Job kind: {getJobKindDisplayLabel(job.job_kind, job.script_path)}</Badge>
<Badge color="gray" {large}>Kind: {getJobKindDisplayLabel(job.job_kind, job.script_path)}</Badge
>
</div>
{/if}
{#if job && job.flow_status && job.job_kind === 'script'}
@@ -83,7 +84,9 @@
{#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0}
{#each job?.['labels'] as label}
<div>
<Badge {large}>Label: {label}</Badge>
<Badge color="blue" {large} title="Label: {label}"
><Tag size={10} class="inline -mt-px" />{label}</Badge
>
</div>
{/each}
{/if}
@@ -1,14 +1,16 @@
import { FileCode, FileText, Clock, Braces, Users } from 'lucide-svelte'
import { FileCode, FileText, Clock, Braces, Tag, Users } from 'lucide-svelte'
import type { FilterSchemaRec } from '../FilterSearchbar.svelte'
export function buildSchedulesFilterSchema({
paths,
scriptPaths,
labels,
showUserFoldersFilter,
userFoldersLabel
}: {
paths: string[]
scriptPaths: string[]
labels?: string[]
showUserFoldersFilter?: boolean
userFoldersLabel?: string
}) {
@@ -62,6 +64,15 @@ export function buildSchedulesFilterSchema({
icon: Braces,
description: 'Filter by JSON args subset match (e.g., {"param": "value"})'
},
label: {
type: 'oneof' as const,
options: (labels ?? []).map((s) => ({ label: s, value: s })),
allowNegative: false,
allowMultiple: true,
label: 'Label',
icon: Tag,
description: 'Filter by label (comma-separated for multiple)'
},
...(showUserFoldersFilter
? {
user_folders_only: {
@@ -5,6 +5,7 @@
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import CronInput from '$lib/components/CronInput.svelte'
import Path from '$lib/components/Path.svelte'
import LabelsInput from '$lib/components/LabelsInput.svelte'
import Required from '$lib/components/Required.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
@@ -105,6 +106,7 @@
let enabled: boolean = $state(false)
let pathError = $state('')
let summary = $state('')
let labels: string[] | undefined = $state(undefined)
let description = $state('')
let no_flow_overlap = $state(false)
let tag: string | undefined = $state(undefined)
@@ -296,6 +298,7 @@
paused_until = s?.paused_until ?? undefined
showPauseUntil = paused_until !== undefined
summary = s?.summary ?? ''
labels = s?.labels ?? undefined
description = s?.description ?? ''
script_path = s?.script_path ?? initialScriptPath
args = s?.args ?? {}
@@ -456,6 +459,7 @@
paused_until = cfg.paused_until
showPauseUntil = paused_until !== undefined
summary = cfg.summary ?? ''
labels = cfg.labels ?? undefined
description = cfg.description ?? ''
script_path = cfg.script_path ?? ''
await loadScript(script_path)
@@ -606,6 +610,7 @@
ws_error_handler_muted: wsErrorHandlerMuted,
retry: retry,
summary: summary != '' ? summary : undefined,
labels: labels,
description: description,
no_flow_overlap: no_flow_overlap,
tag: tag,
@@ -727,6 +732,7 @@
bind:value={summary}
/>
</label>
<LabelsInput bind:labels />
<div class="flex flex-col gap-1">
<label for="path" class="text-xs font-semibold text-emphasis">Path</label>
@@ -1,14 +1,16 @@
import { FileCode, FileText, FolderIcon, Key, Users } from 'lucide-svelte'
import { FileCode, FileText, FolderIcon, Key, Tag, Users } from 'lucide-svelte'
import type { FilterSchemaRec } from '../FilterSearchbar.svelte'
export function buildVariablesFilterSchema({
paths,
owners,
labels,
showUserFoldersFilter,
userFoldersLabel
}: {
paths: string[]
owners: string[]
labels?: string[]
showUserFoldersFilter?: boolean
userFoldersLabel?: string
}) {
@@ -53,6 +55,15 @@ export function buildVariablesFilterSchema({
icon: Key,
description: 'Search in non-secret variable values'
},
label: {
type: 'oneof' as const,
options: (labels ?? []).map((s) => ({ label: s, value: s })),
allowNegative: false,
allowMultiple: true,
label: 'Label',
icon: Tag,
description: 'Filter by label (comma-separated for multiple)'
},
...(showUserFoldersFilter
? {
user_folders_only: {
@@ -19,7 +19,7 @@
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, Skeleton } from '$lib/components/common'
import { Badge, Button, Skeleton } from '$lib/components/common'
import Dropdown from '$lib/components/DropdownV2.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
@@ -306,7 +306,7 @@
<div class="text-center text-sm text-primary mt-2"> No email triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { workspace_id, workspaced_local_part, path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, marked, local_part, mode, retry, error_handler_path, error_handler_args } (path)}
{#each items.slice(0, nbDisplayed) as { workspace_id, workspaced_local_part, path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, marked, local_part, mode, retry, error_handler_path, error_handler_args, labels } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const emailAddress = getEmailAddress(
local_part,
@@ -348,6 +348,11 @@
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if labels?.length}
{#each labels as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{/if}
</div>
<TriggerModeToggle
@@ -256,7 +256,12 @@
})
}
if (flow && !$userStore?.operator && !isCloudHosted() && !isRuleActive('DisableWorkspaceForking')) {
if (
flow &&
!$userStore?.operator &&
!isCloudHosted() &&
!isRuleActive('DisableWorkspaceForking')
) {
buttons.push({
label: 'Edit in fork',
buttonProps: {
@@ -353,13 +358,12 @@
disabled: !can_write
})
if (showEditButtons) {
menuItems.push({
label: 'Move/Rename',
onclick: () => moveDrawer?.openDrawer(flow?.path ?? '', flow?.summary, 'flow'),
Icon: FolderOpen
})
menuItems.push({
label: 'Move/Rename',
onclick: () => moveDrawer?.openDrawer(flow?.path ?? '', flow?.summary, 'flow'),
Icon: FolderOpen
})
}
menuItems.push({
@@ -510,6 +514,7 @@
scriptOrFlowPath={flow?.path ?? ''}
errorHandlerKind="flow"
tag={flow?.tag ?? ''}
labels={flow?.labels}
summary={flow?.summary}
path={flow?.path}
onSaved={can_write
@@ -19,7 +19,7 @@
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Button, Skeleton } from '$lib/components/common'
import { Alert, Badge, Button, Skeleton } from '$lib/components/common'
import Dropdown from '$lib/components/DropdownV2.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
@@ -357,7 +357,7 @@
<div class="text-center text-sm text-primary mt-2"> No GCP Pub/Sub triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { gcp_resource_path, topic_id, workspace_id, delivery_type, path, edited_by, error, edited_at, script_path, is_flow, extra_perms, canWrite, mode, server_id, subscription_id, retry, error_handler_path, error_handler_args } (path)}
{#each items.slice(0, nbDisplayed) as { gcp_resource_path, topic_id, workspace_id, delivery_type, path, edited_by, error, edited_at, script_path, is_flow, extra_perms, canWrite, mode, server_id, subscription_id, retry, error_handler_path, error_handler_args, labels } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const ping = new Date()}
{@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000}
@@ -385,6 +385,11 @@
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if labels?.length}
{#each labels as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{/if}
</div>
{#if delivery_type !== 'push'}
@@ -18,7 +18,7 @@
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Button, Skeleton } from '$lib/components/common'
import { Alert, Badge, Button, Skeleton } from '$lib/components/common'
import Dropdown from '$lib/components/DropdownV2.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
@@ -322,7 +322,7 @@
<div class="text-center text-sm text-primary mt-2"> No Kafka triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, kafka_resource_path, topics, extra_perms, canWrite, marked, server_id, error, last_server_ping, mode, retry, error_handler_path, error_handler_args } (path)}
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, kafka_resource_path, topics, extra_perms, canWrite, marked, server_id, error, last_server_ping, mode, retry, error_handler_path, error_handler_args, labels } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const ping = last_server_ping ? new Date(last_server_ping) : undefined}
{@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000}
@@ -361,6 +361,11 @@
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if labels?.length}
{#each labels as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{/if}
</div>
<div class="w-10">
@@ -19,7 +19,7 @@
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Button, Skeleton } from '$lib/components/common'
import { Alert, Badge, Button, Skeleton } from '$lib/components/common'
import Dropdown from '$lib/components/DropdownV2.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
@@ -310,7 +310,7 @@
<div class="text-center text-sm text-primary mt-2"> No MQTT triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, error, last_server_ping, server_id, mode, retry, error_handler_path, error_handler_args } (path)}
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, error, last_server_ping, server_id, mode, retry, error_handler_path, error_handler_args, labels } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const ping = last_server_ping ? new Date(last_server_ping) : undefined}
{@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000}
@@ -338,6 +338,11 @@
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if labels?.length}
{#each labels as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{/if}
</div>
<div class="w-10">
@@ -18,7 +18,7 @@
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Button, Skeleton } from '$lib/components/common'
import { Alert, Badge, Button, Skeleton } from '$lib/components/common'
import Dropdown from '$lib/components/DropdownV2.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
@@ -321,7 +321,7 @@
<div class="text-center text-sm text-primary mt-2"> No NATS triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, nats_resource_path, subjects, extra_perms, canWrite, marked, server_id, error, last_server_ping, mode, retry, error_handler_path, error_handler_args } (path)}
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, nats_resource_path, subjects, extra_perms, canWrite, marked, server_id, error, last_server_ping, mode, retry, error_handler_path, error_handler_args, labels } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const ping = last_server_ping ? new Date(last_server_ping) : undefined}
{@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000}
@@ -360,6 +360,11 @@
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if labels?.length}
{#each labels as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{/if}
</div>
<div class="w-10">
@@ -19,7 +19,7 @@
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Button, Skeleton } from '$lib/components/common'
import { Alert, Badge, Button, Skeleton } from '$lib/components/common'
import Dropdown from '$lib/components/DropdownV2.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
@@ -386,7 +386,7 @@
<div class="text-center text-sm text-primary mt-2"> No postgres triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { postgres_resource_path, publication_name, replication_slot_name, path, edited_by, error, edited_at, script_path, is_flow, extra_perms, canWrite, mode, server_id, retry, error_handler_path, error_handler_args } (path)}
{#each items.slice(0, nbDisplayed) as { postgres_resource_path, publication_name, replication_slot_name, path, edited_by, error, edited_at, script_path, is_flow, extra_perms, canWrite, mode, server_id, retry, error_handler_path, error_handler_args, labels } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const ping = new Date()}
{@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000}
@@ -417,6 +417,11 @@
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if labels?.length}
{#each labels as label}
<Badge color="blue" small class="px-1" title="Label: {label}">{label}</Badge>
{/each}
{/if}
</div>
<div class="w-10">
@@ -30,13 +30,7 @@
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import type { ResourceType, WorkspaceDeployUISettings } from '$lib/gen'
import {
FolderService,
OauthService,
ResourceService,
WorkspaceService,
type ListableResource
} from '$lib/gen'
import { OauthService, ResourceService, WorkspaceService, type ListableResource } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore, userWorkspaces } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import {
@@ -88,6 +82,7 @@
let allPaths: string[] = $state([])
let allResourceTypes: string[] = $state([])
let allOwners: string[] = $state([])
let allLabels: string[] = $state([])
let resourceTypeViewer: Drawer | undefined = $state(undefined)
let resourceTypeViewerObj = $state({
@@ -129,7 +124,6 @@
})
let showCreateButtons = $state(false)
let folders: string[] = $state([])
// FilterSearchbar setup
let userFoldersFilterType = $derived(
@@ -144,14 +138,27 @@
paths: allPaths,
resourceTypes: allResourceTypes,
owners: allOwners,
labels: allLabels,
showUserFoldersFilter: userFoldersFilterType !== undefined,
userFoldersLabel:
userFoldersFilterType === 'only f/*' ? 'Only f/*' : `Only u/${$userStore?.username} and f/*`
})
)
let filters = useUrlSyncedFilterInstance(untrack(() => resourcesFilterSchema))
let itemFolders = $derived(
Array.from(
new Set(
(resources ?? [])
.map((x) => x.path.split('/').slice(0, 2).join('/'))
.filter((x) => x.startsWith('f/'))
)
)
.sort()
.map((f) => f.replace(/^f\//, ''))
)
let folderPresets = $derived([
...folders.map((f) => ({ name: `f/${f}`, value: `path_start:\\ f/${f}/` })),
...itemFolders.map((f) => ({ name: `f/${f}`, value: `path_start:\\ f/${f}/` })),
...allLabels.map((l) => ({ name: l, value: `label:\\ ${l}` })),
...(resourcesFilterSchema.user_folders_only
? [
{
@@ -214,6 +221,9 @@
if (currentFilters._default_) {
apiParams.broadFilter = currentFilters._default_
}
if (currentFilters.label) {
apiParams.label = currentFilters.label
}
const result = (await ResourceService.listResource(apiParams)).map((x) => {
return {
@@ -229,6 +239,7 @@
allOwners = Array.from(
new Set(result.map((x) => x.path.split('/').slice(0, 2).join('/')))
).sort()
allLabels = Array.from(new Set(result.flatMap((x) => x.labels ?? []))).sort()
return result
}
@@ -552,16 +563,11 @@
})
}
})
async function loadFolders() {
folders = await FolderService.listFolderNames({ workspace: $workspaceStore! })
}
$effect(() => {
if ($workspaceStore && $userStore) {
untrack(() => {
loadResources()
loadResourceTypes()
loadFolders()
})
}
})
@@ -953,18 +959,43 @@
</Head>
<tbody class="divide-y bg-surface">
{#if filteredItems}
{#each filteredItems as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired, marked, is_refreshed }}
{#each filteredItems as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired, marked, is_refreshed, labels }}
<Row>
<Cell first>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</Cell>
<Cell>
<a
class="break-all"
href="#/resource/{path}"
onclick={() => resourceEditor?.initEdit?.(path)}
>{#if marked}{@html marked}{:else}{path}{/if}</a
>
<div class="flex items-center gap-2">
<a
class="break-all"
href="#/resource/{path}"
onclick={() => resourceEditor?.initEdit?.(path)}
>{#if marked}{@html marked}{:else}{path}{/if}</a
>
{#if labels?.length}
<div class="flex items-center gap-0.5">
{#each labels as label}
<Badge
color="blue"
small
class="px-1"
title="Label: {label}"
clickable
onclick={() => {
const arr = (filters.val.label ?? '').split(',').filter(Boolean)
const idx = arr.indexOf(label)
if (idx >= 0) arr.splice(idx, 1)
else arr.push(label)
const newFilters = { ...filters.val }
if (arr.length) newFilters.label = arr.join(',')
else delete newFilters.label
filters.val = newFilters
}}>{label}</Badge
>
{/each}
</div>
{/if}
</div>
</Cell>
<Cell>
<a
@@ -93,6 +93,9 @@
if (currentFilters._default_) {
apiParams.broadFilter = currentFilters._default_
}
if (currentFilters.label) {
apiParams.label = currentFilters.label
}
const result = (await ScheduleService.listSchedules(apiParams)).map((x) => {
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
@@ -101,6 +104,7 @@
// Extract unique values for autocomplete
allPaths = Array.from(new Set(result.map((x) => x.path))).sort()
allScriptPaths = Array.from(new Set(result.map((x) => x.script_path))).sort()
allLabels = Array.from(new Set(result.flatMap((x) => x.labels ?? []))).sort()
schedules = result
loading = false
@@ -161,6 +165,7 @@
// Collect unique values for filter autocomplete
let allPaths: string[] = $state([])
let allScriptPaths: string[] = $state([])
let allLabels: string[] = $state([])
// FilterSearchbar setup
let userFoldersFilterType = $derived(
@@ -174,12 +179,36 @@
buildSchedulesFilterSchema({
paths: allPaths,
scriptPaths: allScriptPaths,
labels: allLabels,
showUserFoldersFilter: userFoldersFilterType !== undefined,
userFoldersLabel:
userFoldersFilterType === 'only f/*' ? 'Only f/*' : `Only u/${$userStore?.username} and f/*`
})
)
let filters = useUrlSyncedFilterInstance(untrack(() => schedulesFilterSchema))
let allFolders = $derived(
Array.from(
new Set(
(schedules ?? [])
.map((x) => x.path.split('/').slice(0, 2).join('/'))
.filter((x) => x.startsWith('f/'))
)
)
.sort()
.map((f) => f.replace(/^f\//, ''))
)
let presets = $derived([
...allFolders.map((f) => ({ name: `f/${f}`, value: `path_start:\\ f/${f}/` })),
...allLabels.map((l) => ({ name: l, value: `label:\\ ${l}` })),
...(schedulesFilterSchema.user_folders_only
? [
{
name: schedulesFilterSchema.user_folders_only.label ?? '?',
value: 'user_folders_only:\\ true'
}
]
: [])
])
let nbDisplayed = $state(15)
let filterEnabledDisabled: 'all' | 'enabled' | 'disabled' = $state('all')
@@ -198,7 +227,6 @@
selectedFilterKind: 'schedule' | 'script_flow',
userFoldersOnly: boolean
) {
if ($workspaceStore == 'admins') return true
if (userFoldersOnly) {
if (selectedFilterKind === 'schedule') {
return (
@@ -275,6 +303,7 @@
schema={schedulesFilterSchema}
bind:value={filters.val}
class="grow max-w-[26rem]"
{presets}
/>
</div>
{#if loading}
@@ -285,7 +314,7 @@
<div class="text-center text-xs font-semibold text-emphasis mt-2"> No schedules </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, error, summary, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, extra_perms, canWrite, jobs, paused_until } (path)}
{#each items.slice(0, nbDisplayed) as { path, error, summary, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, extra_perms, canWrite, jobs, paused_until, labels } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const avg_s = jobs
? jobs.reduce((acc, x) => acc + x.duration_ms, 0) / jobs.length
@@ -312,6 +341,27 @@
schedule: {path}
</div>
</a>
{#if labels?.length}
{#each labels as label}
<Badge
color="blue"
small
class="px-1"
title="Label: {label}"
clickable
onclick={() => {
const arr = (filters.val.label ?? '').split(',').filter(Boolean)
const idx = arr.indexOf(label)
if (idx >= 0) arr.splice(idx, 1)
else arr.push(label)
const newFilters = { ...filters.val }
if (arr.length) newFilters.label = arr.join(',')
else delete newFilters.label
filters.val = newFilters
}}>{label}</Badge
>
{/each}
{/if}
{#if paused_until && new Date(paused_until) > new Date()}
<div class="pb-1">

Some files were not shown because too many files have changed in this diff Show More