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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* refactor: simplify flow env var resolution and json_path handling in API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(frontend): always show flow env variables in property picker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update flow_env openapi type to allow any JSON value

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(frontend): remove redundant variable type from env var dropdown

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(frontend): use Label component and fix alert text in flow env vars editor

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(frontend): avoid redundant stringify/parse roundtrip in env type switch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* perf: avoid cloning entire FlowValue to resolve flow_env references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-03-04 15:20:50 +01:00
committed by GitHub
parent 8a859ff7b9
commit 164e499c64
10 changed files with 323 additions and 154 deletions
@@ -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<Box<RawValue>>\"\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<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943"
}
@@ -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<Box<RawValue>>\"\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<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"TextArray"
]
},
"nullable": [
null
]
},
"hash": "c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154"
}
+4 -6
View File
@@ -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)
+81 -10
View File
@@ -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<JsonPath>,
) -> windmill_common::error::JsonResult<Box<JsonRawValue>> {
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<Box<RawValue>>"
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::<Vec<_>>())
.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::<ApiAuthed>::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::<usize>()
.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,
+45 -14
View File
@@ -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<MiniPulledJob>,
mut status: FlowStatus,
flow: &FlowValue,
flow_env: Option<&HashMap<String, Box<RawValue>>>,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
last_job_result: Option<Arc<Box<RawValue>>>,
@@ -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),
@@ -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<Record<string, EnvVarType>>({})
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<Record<string, string | undefined>>({})
// 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<string, EnvVarType> = {}
$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<PropPickerWrapperContext>('PropPickerWrapper', {
inputMatches: writable(undefined),
connectProp: () => {},
@@ -192,8 +244,8 @@
Flow envs can be referenced in any flow step input using the syntax{' '}
<code>flow_env.VARIABLE_NAME</code> or <code>flow_env["VARIABLE_NAME"]</code>. 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 <DollarSign size={12}
class="inline" /> button. Resource type references workspace resources resolved at runtime.
</Alert>
{#if flowEnvEntries.length === 0}
@@ -246,10 +298,13 @@
{/if}
</div>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-sm font-medium">Value</label>
{#if entry.type === 'json'}
<Label label="Value">
{#if entry.type === 'resource'}
<ResourcePicker
bind:value={resourcePaths[entry.key]}
disabled={noEditor}
/>
{:else if entry.type === 'json'}
<div class="w-full">
<JsonEditor
bind:code={entry.displayValue}
@@ -261,16 +316,43 @@
/>
</div>
{:else}
<input
type="text"
value={entry.displayValue}
oninput={(e) => updateEnvValue(entry.key, e.currentTarget.value, 'string')}
disabled={noEditor}
class="input w-full"
placeholder="Variable value"
/>
<div class="relative group w-full">
<input
type="text"
value={entry.displayValue}
oninput={(e) =>
updateEnvValue(entry.key, e.currentTarget.value, 'string')}
disabled={noEditor}
class="input w-full"
placeholder="Variable value"
/>
{#if !noEditor}
<Button
iconOnly
startIcon={{ icon: DollarSign }}
unifiedSize="sm"
onClick={() => {
pickForKey = entry.key
variablePicker?.openDrawer?.()
}}
wrapperClasses="opacity-0 group-hover:opacity-100 transition-opacity absolute right-2 top-1/2 -translate-y-1/2 bg-surface-input"
variant="subtle"
title="Insert a Variable"
/>
{/if}
</div>
{#if typeof entry.value === 'string' && entry.value.startsWith('$var:') && entry.value.length > 5}
<div class="text-2xs text-tertiary">
Linked to variable <a
href="/variables#{entry.value.slice(5)}"
target="_blank"
class="text-accent underline font-normal"
>{entry.value.slice(5)}</a
>
</div>
{/if}
{/if}
</div>
</Label>
</div>
{/each}
</div>
@@ -285,3 +367,20 @@
</div>
</FlowCard>
</div>
<ItemPicker
bind:this={variablePicker}
pickCallback={(path, _) => {
if (pickForKey) {
setVarPath(pickForKey, path)
pickForKey = undefined
}
}}
itemName="Variable"
extraField="path"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
name: x.path,
...x
}))}
/>
@@ -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>('PropPickerContext')
flowPropPickerConfig.set(undefined)
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
let flow_env = $derived(pickableProperties?.flow_env || flowStore.val.value.flow_env)
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
propPickerConfig,
inputMatches,
@@ -156,7 +155,6 @@
{extraResults}
{displayContext}
{error}
{flow_env}
previousId={pickableProperties?.previousId}
{pickableProperties}
allowCopy={!notSelectable && !$propPickerConfig}
@@ -19,7 +19,6 @@
error?: boolean
allowCopy?: boolean
previousId?: string | undefined
flow_env?: Record<string, any> | 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<string, any> = $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 @@
/>
</div>
{/if}
{#if flowEnvFiltered && Object.keys(flowEnvFiltered ?? {}).length > 0}
<span class={categoryTitleClasses}>Flow Env Variables</span>
<div class={categoryContentClasses}>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
json={flowEnvFiltered}
prefix="flow_env"
on:select
/>
</div>
{/if}
{#if error}
<span class={categoryTitleClasses}>Error</span>
<div class={categoryContentClasses}>
@@ -445,45 +449,6 @@
{/if}
</div>
{/if}
{#if flow_env && Object.keys(flow_env).length > 0 && $inputMatches?.some((match) => match.word === 'flow_env')}
<div class="overflow-y-auto pb-2">
<span class="font-normal text-xs text-secondary">Flow Env Variables:</span>
{#if displayFlowEnv}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayFlowEnv = false
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={false}
json={flowEnvFiltered}
prefix="flow_env"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayFlowEnv = true
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-normal text-2xs rounded-[0.275rem] h-4 px-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
{/if}
<!-- </div> -->
</Scrollable>
@@ -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)
}
})
</script>
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
+2 -3
View File
@@ -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)