mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
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:
+24
@@ -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"
|
||||
}
|
||||
-25
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user