From 164e499c64dc5eb76fcfb0f8cefbad2df244f610 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 4 Mar 2026 15:20:50 +0100 Subject: [PATCH] feat: add variable and resource types to flow env variables (#8214) * feat: add variable and resource types to flow env variables Flow env variables can now reference workspace variables ($var:path) and resources ($res:path) that are resolved at runtime. Adds Variable and Resource type options to the flow env editor with ItemPicker and ResourcePicker components, and resolves references in both the flow worker (via transform_json) and the API fallback endpoint. Co-Authored-By: Claude Opus 4.6 * fix(frontend): use inline DollarSign icon for variable picker Replace the separate "Pick" button with the standard inline DollarSign icon overlay that appears on hover, matching the existing ArgInput pattern. Also add the icon to the string type input for quick variable linking from any string field. Co-Authored-By: Claude Opus 4.6 * refactor: simplify flow env var resolution and json_path handling in API Co-Authored-By: Claude Opus 4.5 * fix(frontend): always show flow env variables in property picker Co-Authored-By: Claude Opus 4.5 * fix: update flow_env openapi type to allow any JSON value Co-Authored-By: Claude Opus 4.5 * refactor(frontend): remove redundant variable type from env var dropdown Co-Authored-By: Claude Opus 4.5 * fix(frontend): use Label component and fix alert text in flow env vars editor Co-Authored-By: Claude Opus 4.5 * fix(frontend): avoid redundant stringify/parse roundtrip in env type switch Co-Authored-By: Claude Opus 4.5 * fix: address PR review comments for flow env vars - Deduplicate db_authed in jobs.rs $var/$res resolution - Add warn logging on variable/resource resolution failures - Consolidate $effect blocks and remove auto-type-correction effect - Make linked variable text a clickable link to variable editor - Add hash-based variable editor opening on variables page Co-Authored-By: Claude Opus 4.6 * perf: avoid cloning entire FlowValue to resolve flow_env references Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- ...86d7c4b9bdb1e8fb1f7725060990ef8984943.json | 24 +++ ...689e2c0100c1569436a01b207876aaa470154.json | 25 --- backend/windmill-api/openapi-deref.yaml | 10 +- backend/windmill-api/src/jobs.rs | 91 ++++++++- backend/windmill-worker/src/worker_flow.rs | 59 ++++-- .../content/FlowEnvironmentVariables.svelte | 177 ++++++++++++++---- .../flows/propPicker/PropPickerWrapper.svelte | 6 +- .../propertyPicker/PropPicker.svelte | 69 ++----- .../(root)/(logged)/variables/+page.svelte | 11 +- openflow.openapi.yaml | 5 +- 10 files changed, 323 insertions(+), 154 deletions(-) create mode 100644 backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json delete mode 100644 backend/.sqlx/query-c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154.json diff --git a/backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json b/backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json new file mode 100644 index 0000000000..8c5f43ab07 --- /dev/null +++ b/backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n flow_version.value -> 'flow_env' -> $3\n ELSE\n root_job.raw_flow -> 'flow_env' -> $3\n END AS \"flow_env: sqlx::types::Json>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "flow_env: sqlx::types::Json>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943" +} diff --git a/backend/.sqlx/query-c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154.json b/backend/.sqlx/query-c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154.json deleted file mode 100644 index be352ce88e..0000000000 --- a/backend/.sqlx/query-c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n (flow_version.value -> 'flow_env' -> $3) #> $4\n ELSE\n (root_job.raw_flow -> 'flow_env' -> $3) #> $4\n END AS \"flow_env: sqlx::types::Json>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "flow_env: sqlx::types::Json>", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "TextArray" - ] - }, - "nullable": [ - null - ] - }, - "hash": "c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154" -} diff --git a/backend/windmill-api/openapi-deref.yaml b/backend/windmill-api/openapi-deref.yaml index 1a036f6dc9..7f04a7287e 100644 --- a/backend/windmill-api/openapi-deref.yaml +++ b/backend/windmill-api/openapi-deref.yaml @@ -8857,9 +8857,8 @@ paths: type: boolean flow_env: type: object - description: Environment variables available to all steps - additionalProperties: - type: string + description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)." + additionalProperties: {} priority: type: number description: Execution priority (higher numbers run first) @@ -14644,9 +14643,8 @@ paths: type: boolean flow_env: type: object - description: Environment variables available to all steps - additionalProperties: - type: string + description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)." + additionalProperties: {} priority: type: number description: Execution priority (higher numbers run first) diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index d58dcd3c69..ac5a9a306b 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -448,14 +448,15 @@ async fn get_flow_env_by_flow_job_id( Path((w_id, flow_job_id, var_name)): Path<(String, Uuid, String)>, Query(JsonPath { json_path, .. }): Query, ) -> windmill_common::error::JsonResult> { - let flow_env = sqlx::query_scalar!( + // Fetch raw value (without json_path) to check for $var:/$res: references + let raw_value = sqlx::query_scalar!( r#" SELECT CASE WHEN flow_version.id IS NOT NULL THEN - (flow_version.value -> 'flow_env' -> $3) #> $4 + flow_version.value -> 'flow_env' -> $3 ELSE - (root_job.raw_flow -> 'flow_env' -> $3) #> $4 + root_job.raw_flow -> 'flow_env' -> $3 END AS "flow_env: sqlx::types::Json>" FROM v2_job current_job @@ -472,16 +473,86 @@ async fn get_flow_env_by_flow_job_id( flow_job_id, w_id, var_name, - json_path - .as_ref() - .map(|x| x.split(".").collect::>()) - .unwrap_or_default() as Vec<&str>, ) .fetch_optional(&db) .await? - .map(|r| r.map(|x| x.0)) - .flatten() - .unwrap_or_else(|| to_raw_value(&serde_json::Value::Null)); + .and_then(|r| r.map(|x| x.0)); + + // Resolve $var:/$res: references if present + let resolved = if let Some(raw) = raw_value { + let raw_str = raw.get(); + let db_authed = windmill_common::db::DbWithOptAuthed::::from_authed( + &authed, + db.clone(), + None, + ); + if let Some(path) = raw_str + .strip_prefix("\"$var:") + .and_then(|s| s.strip_suffix("\"")) + { + match windmill_store::variables::get_value_internal(&db_authed, &w_id, path, false) + .await + { + Ok(val) => to_raw_value(&serde_json::Value::String(val)), + Err(e) => { + tracing::warn!("Failed to resolve flow_env variable $var:{path}: {e}"); + raw + } + } + } else if let Some(path) = raw_str + .strip_prefix("\"$res:") + .and_then(|s| s.strip_suffix("\"")) + { + match windmill_store::resources::get_resource_value_interpolated_internal( + &db_authed, + &w_id, + path, + Some(flow_job_id), + Some(&tokened.token), + false, + ) + .await + { + Ok(Some(val)) => to_raw_value(&val), + Ok(None) => { + tracing::warn!( + "Failed to resolve flow_env resource $res:{path}: resource not found" + ); + raw + } + Err(e) => { + tracing::warn!("Failed to resolve flow_env resource $res:{path}: {e}"); + raw + } + } + } else { + raw + } + } else { + to_raw_value(&serde_json::Value::Null) + }; + + // Apply json_path navigation on the (possibly resolved) value + let flow_env = if let Some(ref jp) = json_path { + let mut value: serde_json::Value = + serde_json::from_str(resolved.get()).unwrap_or(serde_json::Value::Null); + for part in jp.split('.') { + value = match value { + serde_json::Value::Object(ref mut map) => { + map.remove(part).unwrap_or(serde_json::Value::Null) + } + serde_json::Value::Array(ref arr) => part + .parse::() + .ok() + .and_then(|i| arr.get(i).cloned()) + .unwrap_or(serde_json::Value::Null), + _ => serde_json::Value::Null, + }; + } + to_raw_value(&value) + } else { + resolved + }; log_job_view( &db, diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index d0b7fc7a58..33dafbb482 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; -use crate::common::{cached_result_path, get_root_job_id, save_in_cache}; +use crate::common::{cached_result_path, get_root_job_id, save_in_cache, transform_json}; use crate::js_eval::{eval_timeout, IdContext}; use crate::worker_utils::get_tag_and_concurrency; use crate::{ @@ -53,7 +53,7 @@ use windmill_common::runnable_settings::{ use windmill_common::scripts::{ScriptHash, ScriptRunnableSettingsInline}; use windmill_common::users::username_to_permissioned_as; use windmill_common::utils::WarnAfterExt; -use windmill_common::worker::to_raw_value; +use windmill_common::worker::{to_raw_value, Connection}; use windmill_common::{ add_time, get_latest_flow_version_info_for_path, get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, DB, @@ -2245,6 +2245,35 @@ pub async fn handle_flow( killpill_rx: &tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { let flow = flow_data.value(); + + // Resolve $var: and $res: references in flow_env. + // We resolve into a separate variable to avoid cloning the entire FlowValue + // (which includes modules, failure_module, etc.) just to replace flow_env. + let resolved_env; + let flow_env = if let Some(ref env) = flow.flow_env { + match transform_json( + client, + &flow_job.workspace_id, + env, + &flow_job, + &Connection::Sql(db.clone()), + ) + .await + { + Ok(Some(resolved)) => { + resolved_env = resolved; + Some(&resolved_env) + } + Ok(None) => flow.flow_env.as_ref(), + Err(e) => { + tracing::warn!("Failed to resolve flow_env references: {e}"); + flow.flow_env.as_ref() + } + } + } else { + None + }; + let status = flow_job .parse_flow_status() .with_context(|| "Unable to parse flow status")?; @@ -2348,6 +2377,7 @@ pub async fn handle_flow( flow_job, status, flow, + flow_env, db, client, last_result.clone(), @@ -2448,6 +2478,7 @@ async fn push_next_flow_job( flow_job: Arc, mut status: FlowStatus, flow: &FlowValue, + flow_env: Option<&HashMap>>, db: &sqlx::Pool, client: &AuthedClient, last_job_result: Option>>, @@ -2580,7 +2611,7 @@ async fn push_next_flow_job( let skip = compute_bool_from_expr( &skip_expr, arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, Arc::new(to_raw_value(&json!("{}"))), None, None, @@ -2705,7 +2736,7 @@ async fn push_next_flow_job( expr.to_string(), context, Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, None, None, None @@ -2966,7 +2997,7 @@ async fn push_next_flow_job( &input_transform, arc_last_job_result.clone(), Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, Some(client), None, ) @@ -3004,7 +3035,7 @@ async fn push_next_flow_job( &status.retry, arc_last_job_result.clone(), arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, Some(client), ) .await? @@ -3092,7 +3123,7 @@ async fn push_next_flow_job( compute_bool_from_expr( &skip_if.expr, arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), None, Some(&idcontext), @@ -3182,7 +3213,7 @@ async fn push_next_flow_job( }; transform_input( arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), input_transforms, resumes.clone(), @@ -3209,7 +3240,7 @@ async fn push_next_flow_job( let next_flow_transform = compute_next_flow_transform( arc_flow_job_args.clone(), arc_last_job_result.clone(), - flow.flow_env.as_ref(), + flow_env, &flow_job, &flow, transform_context, @@ -3373,7 +3404,7 @@ async fn push_next_flow_job( let ctx = get_transform_context(&flow_job, "", &status); let ti = transform_input( Marc::new(args), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), input_transforms, resumes.clone(), @@ -3428,7 +3459,7 @@ async fn push_next_flow_job( let ctx = get_transform_context(&flow_job, &previous_id, &status); let ti = transform_input( Marc::new(hm), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), input_transforms, resumes.clone(), @@ -3546,7 +3577,7 @@ async fn push_next_flow_job( timeout_transform, arc_last_job_result.clone(), Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, Some(client), Some(&ctx), ) @@ -3625,7 +3656,7 @@ async fn push_next_flow_job( parallelism_transform, arc_last_job_result.clone(), Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, Some(client), Some(&ctx), ) @@ -4461,7 +4492,7 @@ async fn compute_next_flow_transform( let pred = compute_bool_from_expr( &b.expr, arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), None, Some(&idcontext), diff --git a/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte b/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte index ce51b8dc77..3b6b867abe 100644 --- a/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte +++ b/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte @@ -5,17 +5,21 @@ import { writable } from 'svelte/store' import type { FlowEditorContext } from '../types' import { Button } from '$lib/components/common' - import { Plus, Trash2 } from 'lucide-svelte' + import { DollarSign, Plus, Trash2 } from 'lucide-svelte' import FlowCard from '../common/FlowCard.svelte' import JsonEditor from '$lib/components/JsonEditor.svelte' import Label from '$lib/components/Label.svelte' import Select from '$lib/components/select/Select.svelte' + import ItemPicker from '$lib/components/ItemPicker.svelte' + import ResourcePicker from '$lib/components/ResourcePicker.svelte' + import { VariableService } from '$lib/gen' + import { workspaceStore } from '$lib/stores' interface Props { noEditor: boolean } - type EnvVarType = 'string' | 'json' + type EnvVarType = 'string' | 'json' | 'resource' interface EnvVarEntry { id: string @@ -37,6 +41,9 @@ function determineValueType(value: any): EnvVarType { if (typeof value === 'string') { + if (value.startsWith('$res:')) { + return 'resource' + } try { JSON.parse(value) return value.trim().startsWith('{') || @@ -53,30 +60,51 @@ let flowEnvTypes = $state>({}) - const typeOptions = [ - { label: 'String', value: 'string' as EnvVarType }, - { label: 'JSON', value: 'json' as EnvVarType } + const typeOptions: { label: string; value: EnvVarType }[] = [ + { label: 'String', value: 'string' }, + { label: 'JSON', value: 'json' }, + { label: 'Resource', value: 'resource' } ] + // Track resource paths separately for bind:value with ResourcePicker + let resourcePaths = $state>({}) + + // Initialize resourcePaths from existing flow_env values + for (const [key, value] of Object.entries(flowStore.val.value.flow_env || {})) { + if (typeof value === 'string' && value.startsWith('$res:')) { + resourcePaths[key] = value.substring('$res:'.length) + } + } + + // Initialize types for new keys and sync resourcePaths → flow_env $effect(() => { for (const [key, value] of flowEnvVarsMap.entries()) { if (!flowEnvTypes[key]) { flowEnvTypes[key] = determineValueType(value) } } - }) - - $effect(() => { - for (const [key, type] of Object.entries(flowEnvTypes)) { - if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) { - const currentType = determineValueType(flowStore.val.value.flow_env[key]) - if (currentType !== type) { - updateEnvType(key, type) + for (const [key, path] of Object.entries(resourcePaths)) { + if (flowStore.val.value.flow_env && flowEnvTypes[key] === 'resource') { + const newVal = '$res:' + (path || '') + if (flowStore.val.value.flow_env[key] !== newVal) { + flowStore.val.value.flow_env[key] = newVal + flowStore.val = flowStore.val } } } }) + // Convert values when user changes the type dropdown + let prevTypes: Record = {} + $effect(() => { + for (const [key, type] of Object.entries(flowEnvTypes)) { + if (prevTypes[key] && prevTypes[key] !== type) { + updateEnvType(key, type) + } + prevTypes[key] = type + } + }) + let flowEnvEntries = $derived( Array.from(flowEnvVarsMap.entries()).map(([key, value]): EnvVarEntry => { const stringValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2) @@ -113,6 +141,7 @@ if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) { delete flowStore.val.value.flow_env[key] delete flowEnvTypes[key] + delete resourcePaths[key] flowStore.val = flowStore.val } } @@ -150,32 +179,55 @@ flowStore.val.value.flow_env = newEnvVars delete flowEnvTypes[oldKey] flowEnvTypes[newKey] = type + + // Move resource path if applicable + if (type === 'resource' && oldKey in resourcePaths) { + resourcePaths[newKey] = resourcePaths[oldKey] + delete resourcePaths[oldKey] + } + flowStore.val = flowStore.val } } function updateEnvType(key: string, newType: EnvVarType) { if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) { - const currentValue = flowStore.val.value.flow_env[key] - const stringValue = - typeof currentValue === 'string' ? currentValue : JSON.stringify(currentValue, null, 2) - flowEnvTypes[key] = newType - if (newType === 'json') { - try { - const parsed = JSON.parse(stringValue) - flowStore.val.value.flow_env[key] = parsed - } catch { - flowStore.val.value.flow_env[key] = stringValue + if (newType === 'resource') { + flowStore.val.value.flow_env[key] = '$res:' + resourcePaths[key] = '' + } else if (newType === 'json') { + delete resourcePaths[key] + const currentValue = flowStore.val.value.flow_env[key] + if (typeof currentValue === 'string') { + try { + flowStore.val.value.flow_env[key] = JSON.parse(currentValue) + } catch { + // keep as string if not valid JSON + } } } else { - flowStore.val.value.flow_env[key] = stringValue + delete resourcePaths[key] + const currentValue = flowStore.val.value.flow_env[key] + if (typeof currentValue !== 'string') { + flowStore.val.value.flow_env[key] = JSON.stringify(currentValue, null, 2) + } } flowStore.val = flowStore.val } } + function setVarPath(key: string, path: string) { + if (flowStore.val.value.flow_env) { + flowStore.val.value.flow_env[key] = '$var:' + path + flowStore.val = flowStore.val + } + } + + let variablePicker: ItemPicker | undefined = $state(undefined) + let pickForKey: string | undefined = $state(undefined) + setContext('PropPickerWrapper', { inputMatches: writable(undefined), connectProp: () => {}, @@ -192,8 +244,8 @@ Flow envs can be referenced in any flow step input using the syntax{' '} flow_env.VARIABLE_NAME or flow_env["VARIABLE_NAME"]. These variables are available in the property picker and can be used in JavaScript expressions and - input bindings. You can choose between String or JSON types for each variable - JSON types - allow complex data structures. + input bindings. String values can link to workspace variables using the button. Resource type references workspace resources resolved at runtime. {#if flowEnvEntries.length === 0} @@ -246,10 +298,13 @@ {/if} -
- - - {#if entry.type === 'json'} +
+ {/each} @@ -285,3 +367,20 @@ + + { + if (pickForKey) { + setVarPath(pickForKey, path) + pickForKey = undefined + } + }} + itemName="Variable" + extraField="path" + loadItems={async () => + (await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({ + name: x.path, + ...x + }))} +/> diff --git a/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte b/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte index 6910fabbb5..ead5ad9ad3 100644 --- a/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte +++ b/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte @@ -27,7 +27,7 @@ import type { PickableProperties } from '../previousResults' import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte' import type { PropPickerContext } from '$lib/components/prop_picker' - import type { FlowEditorContext } from '../types' + interface Props { pickableProperties: PickableProperties | undefined @@ -67,9 +67,8 @@ const { flowPropPickerConfig } = getContext('PropPickerContext') flowPropPickerConfig.set(undefined) - const { flowStore } = getContext('FlowEditorContext') - let flow_env = $derived(pickableProperties?.flow_env || flowStore.val.value.flow_env) + setContext('PropPickerWrapper', { propPickerConfig, inputMatches, @@ -156,7 +155,6 @@ {extraResults} {displayContext} {error} - {flow_env} previousId={pickableProperties?.previousId} {pickableProperties} allowCopy={!notSelectable && !$propPickerConfig} diff --git a/frontend/src/lib/components/propertyPicker/PropPicker.svelte b/frontend/src/lib/components/propertyPicker/PropPicker.svelte index d04683e226..6910750480 100644 --- a/frontend/src/lib/components/propertyPicker/PropPicker.svelte +++ b/frontend/src/lib/components/propertyPicker/PropPicker.svelte @@ -19,7 +19,6 @@ error?: boolean allowCopy?: boolean previousId?: string | undefined - flow_env?: Record | undefined result?: any | undefined extraResults?: any } @@ -30,7 +29,6 @@ error = false, allowCopy = false, previousId = undefined, - flow_env = undefined, result = undefined, extraResults = undefined }: Props = $props() @@ -39,7 +37,6 @@ let resources: Record = $state({}) let displayVariable = $state(false) let displayResources = $state(false) - let displayFlowEnv = $state(false) let allResultsCollapsed = $state(true) let collapsableInitialState: @@ -47,7 +44,6 @@ allResultsCollapsed: boolean displayVariable: boolean displayResources: boolean - displayFlowEnv: boolean } | undefined @@ -139,7 +135,9 @@ resultByIdFiltered = {} } if (!$inputMatches?.some((match) => match.word === 'flow_env')) { - flowEnvFiltered = {} + if (search === EMPTY_STRING) { + flowEnvFiltered = pickableProperties.flow_env + } } if ($inputMatches?.length == 1) { filteringFlowInputsOrResult = $inputMatches[0].value @@ -185,8 +183,7 @@ collapsableInitialState = { allResultsCollapsed, displayVariable, - displayResources, - displayFlowEnv + displayResources } } @@ -200,10 +197,6 @@ displayResources = true return } - if ($inputMatches[0].word === 'flow_env') { - displayFlowEnv = true - return - } if ($inputMatches[0].word === 'results') { allResultsCollapsed = false return @@ -214,8 +207,7 @@ if (!collapsableInitialState) { return } - ;({ allResultsCollapsed, displayVariable, displayResources, displayFlowEnv } = - collapsableInitialState) + ;({ allResultsCollapsed, displayVariable, displayResources } = collapsableInitialState) collapsableInitialState = undefined } @@ -279,6 +271,18 @@ /> {/if} + {#if flowEnvFiltered && Object.keys(flowEnvFiltered ?? {}).length > 0} + Flow Env Variables +
+ +
+ {/if} {#if error} Error
@@ -445,45 +449,6 @@ {/if}
{/if} - {#if flow_env && Object.keys(flow_env).length > 0 && $inputMatches?.some((match) => match.word === 'flow_env')} -
- Flow Env Variables: - - {#if displayFlowEnv} - - - {:else} - - {/if} -
- {/if} {/if} diff --git a/frontend/src/routes/(root)/(logged)/variables/+page.svelte b/frontend/src/routes/(root)/(logged)/variables/+page.svelte index 4082e5f8ff..870bf8e7c4 100644 --- a/frontend/src/routes/(root)/(logged)/variables/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/variables/+page.svelte @@ -40,7 +40,8 @@ EyeOff, Circle } from 'lucide-svelte' - import { untrack } from 'svelte' + import { onMount, untrack } from 'svelte' + import { page } from '$app/stores' type ListableVariableW = ListableVariable & { canWrite: boolean } @@ -202,6 +203,14 @@ loadContextualVariables() }, 5000) } + + onMount(() => { + let hash = $page.url.hash + if (hash.length > 1) { + let path = hash.slice(1) + variableEditor?.editVariable(path) + } + }) diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 2b181a6c7b..5031c7e889 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -96,9 +96,8 @@ components: type: boolean flow_env: type: object - description: Environment variables available to all steps - additionalProperties: - type: string + description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)." + additionalProperties: {} priority: type: number description: Execution priority (higher numbers run first)