mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
Assets refactor (#6217)
* Moved logic to FlowAssetsProvider * Remove assetsMap in flow * do not parse everything on mount + only check for missing assets fields * add assets field in backend * remove fallbackAccessTypes * better structure and less queries / parsing * Fix assets not showing when pulling raw_flow from jobs * flow assets ctx for job run * Fix transitive assets fetching * Fix input args asset node * enablePathScriptAndFlowAssets flag * edit btn for variable * untrack refresh * move parseInputArgsAssets * Assets tab in runs * Update FlowStatusViewerInner to svelte 5 + fix asset sync bug * avoid toast error on bad resource * fetch res metadata for input arg asset * Job assets viewer in run page * r/w selector * remove indigo badge * store alt_access_type state in ScriptEditor * Don't parse assets in flow script editor * Add alt_access_type in backend * show Read as selected by default to avoid giving the feeling of having made a decision * keep alt_access_type when reparsing in flow raw scripts * Remove variable asset kind, and save assets for scripts * remove all backend asset parsing * R/W/RW selector button nits * fix insert into assets not saving alt access type * support named arguments in python asset parser * improve asset usage drawer R/W indicator * update legacy $res: syntax * reactivity issue * remove last variable asset stuff * sqlx prepare * tooltip explainer * deprecated variable asset nit
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"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, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets) 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)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Int8Array",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Custom": {
|
||||
"name": "script_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"trigger",
|
||||
"failure",
|
||||
"command",
|
||||
"approval",
|
||||
"preprocessor"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"VarcharArray",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Int2",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1a54356c1e1353950bf6ab1d25ab21270131e9e93ca10d195664e7e5a774fe9e"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Add 'variable' kind back
|
||||
ALTER TABLE asset ALTER column kind TYPE VARCHAR;
|
||||
DROP TYPE asset_kind;
|
||||
CREATE TYPE ASSET_KIND AS ENUM ('s3object', 'resource', 'variable');
|
||||
ALTER TABLE asset ALTER column kind TYPE ASSET_KIND using kind::ASSET_KIND;
|
||||
|
||||
ALTER TABLE script DROP COLUMN assets;
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE script ADD COLUMN assets jsonb;
|
||||
|
||||
-- Remove 'variable' kind
|
||||
DELETE FROM asset WHERE kind = 'variable';
|
||||
ALTER TABLE asset ALTER column kind TYPE VARCHAR;
|
||||
DROP TYPE asset_kind;
|
||||
CREATE TYPE ASSET_KIND AS ENUM ('s3object', 'resource');
|
||||
ALTER TABLE asset ALTER column kind TYPE ASSET_KIND using kind::ASSET_KIND;
|
||||
@@ -57,27 +57,27 @@ impl AssetsFinder {
|
||||
})
|
||||
.ok_or(())?;
|
||||
|
||||
use AssetKind::*;
|
||||
let (kind, access_type, arg) = match ident.as_str() {
|
||||
"load_s3_file" => (AssetKind::S3Object, Some(R), Arg::Pos(0)),
|
||||
"load_s3_file_reader" => (AssetKind::S3Object, Some(R), Arg::Pos(0)),
|
||||
"write_s3_file" => (AssetKind::S3Object, Some(W), Arg::Pos(0)),
|
||||
"get_resource" => (AssetKind::Resource, None, Arg::Pos(0)),
|
||||
"set_resource" => (AssetKind::Resource, Some(W), Arg::Named("path")),
|
||||
"get_boto3_connection_settings" => (AssetKind::Resource, None, Arg::Pos(0)),
|
||||
"get_polars_connection_settings" => (AssetKind::Resource, None, Arg::Pos(0)),
|
||||
"get_duckdb_connection_settings" => (AssetKind::Resource, None, Arg::Pos(0)),
|
||||
"get_variable" => (AssetKind::Variable, Some(R), Arg::Pos(0)),
|
||||
"set_variable" => (AssetKind::Variable, Some(W), Arg::Pos(0)),
|
||||
"load_s3_file" => (S3Object, Some(R), Arg::Pos(0, "s3object")),
|
||||
"load_s3_file_reader" => (S3Object, Some(R), Arg::Pos(0, "s3object")),
|
||||
"write_s3_file" => (S3Object, Some(W), Arg::Pos(0, "s3object")),
|
||||
"get_resource" => (Resource, None, Arg::Pos(0, "path")),
|
||||
"set_resource" => (Resource, Some(W), Arg::Pos(0, "path")),
|
||||
"get_boto3_connection_settings" => (Resource, None, Arg::Pos(0, "s3_resource_path")),
|
||||
"get_polars_connection_settings" => (Resource, None, Arg::Pos(0, "s3_resource_path")),
|
||||
"get_duckdb_connection_settings" => (Resource, None, Arg::Pos(0, "s3_resource_path")),
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
let arg_val = match arg {
|
||||
Arg::Pos(i) => node.args.get(i),
|
||||
Arg::Named(name) => node
|
||||
.keywords
|
||||
.iter()
|
||||
.find(|kw| kw.arg.as_deref() == Some(name))
|
||||
.map(|kw| &kw.value),
|
||||
Arg::Pos(i, name) => node.args.get(i).or_else(|| {
|
||||
// Get arg by name
|
||||
node.keywords
|
||||
.iter()
|
||||
.find(|kw| kw.arg.as_deref() == Some(name))
|
||||
.map(|kw| &kw.value)
|
||||
}),
|
||||
};
|
||||
|
||||
match arg_val {
|
||||
@@ -93,6 +93,6 @@ impl AssetsFinder {
|
||||
}
|
||||
|
||||
enum Arg {
|
||||
Pos(usize),
|
||||
Named(&'static str),
|
||||
// Positional arguments in python can also be used by their name
|
||||
Pos(usize, &'static str),
|
||||
}
|
||||
|
||||
@@ -84,8 +84,6 @@ impl AssetsFinder {
|
||||
"denoS3LightClientSettings" => (AssetKind::Resource, None, 0),
|
||||
"duckdbConnectionSettings" => (AssetKind::Resource, None, 0),
|
||||
"polarsConnectionSettings" => (AssetKind::Resource, None, 0),
|
||||
"getVariable" => (AssetKind::Variable, Some(R), 0),
|
||||
"setVariable" => (AssetKind::Variable, Some(W), 0),
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ use AssetUsageAccessType::*;
|
||||
pub enum AssetKind {
|
||||
S3Object,
|
||||
Resource,
|
||||
Variable,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -57,8 +56,6 @@ pub fn parse_asset_syntax(s: &str) -> Option<(AssetKind, &str)> {
|
||||
Some((AssetKind::Resource, &s[6..]))
|
||||
} else if s.starts_with("$res:") {
|
||||
Some((AssetKind::Resource, &s[5..]))
|
||||
} else if s.starts_with("var://") {
|
||||
Some((AssetKind::Variable, &s[6..]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1147,7 +1147,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
asset_fallback_access_types: None,
|
||||
assets: None,
|
||||
}
|
||||
.into(),
|
||||
stop_after_if: Default::default(),
|
||||
@@ -1191,7 +1191,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
asset_fallback_access_types: None,
|
||||
assets: None,
|
||||
}
|
||||
.into(),
|
||||
stop_after_if: Default::default(),
|
||||
@@ -1321,7 +1321,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
asset_fallback_access_types: None,
|
||||
assets: None,
|
||||
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
@@ -1376,7 +1376,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
asset_fallback_access_types: None,
|
||||
assets: None,
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
stop_after_all_iters_if: Default::default(),
|
||||
@@ -1416,7 +1416,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
asset_fallback_access_types: None,
|
||||
assets: None,
|
||||
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
@@ -1482,7 +1482,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
asset_fallback_access_types: None,
|
||||
assets: None,
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
stop_after_all_iters_if: Default::default(),
|
||||
|
||||
@@ -3115,7 +3115,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ContextualVariable"
|
||||
|
||||
|
||||
/w/{workspace}/workspaces/get_secondary_storage_names:
|
||||
get:
|
||||
summary: get secondary storage names
|
||||
@@ -13838,20 +13838,27 @@ components:
|
||||
type: boolean
|
||||
on_behalf_of_email:
|
||||
type: string
|
||||
fallback_access_types:
|
||||
assets:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [path, kind, access_type]
|
||||
required:
|
||||
- path
|
||||
- kind
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
enum: [s3object, resource]
|
||||
enum:
|
||||
- s3object
|
||||
- resource
|
||||
access_type:
|
||||
type: string
|
||||
enum: [r, w, rw]
|
||||
alt_access_type:
|
||||
type: string
|
||||
enum: [r, w, rw]
|
||||
|
||||
required:
|
||||
- path
|
||||
@@ -17606,7 +17613,6 @@ components:
|
||||
enum:
|
||||
- s3object
|
||||
- resource
|
||||
- variable
|
||||
Asset:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1391,7 +1391,7 @@ mod tests {
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
asset_fallback_access_types: None,
|
||||
assets: None,
|
||||
}),
|
||||
stop_after_if: Some(StopAfterIf {
|
||||
expr: "foo = 'bar'".to_string(),
|
||||
|
||||
@@ -43,7 +43,7 @@ use windmill_audit::ActionKind;
|
||||
use windmill_worker::process_relative_imports;
|
||||
|
||||
use windmill_common::{
|
||||
assets::{clear_asset_usage, insert_asset_usage, parse_assets, AssetUsageKind},
|
||||
assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType},
|
||||
error::to_anyhow,
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
@@ -116,6 +116,9 @@ pub struct ScriptWDraft {
|
||||
pub has_preprocessor: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub on_behalf_of_email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(json(nullable))]
|
||||
pub assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
@@ -771,8 +774,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, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation) \
|
||||
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)",
|
||||
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets) \
|
||||
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)",
|
||||
&w_id,
|
||||
&hash.0,
|
||||
ns.path,
|
||||
@@ -810,6 +813,7 @@ async fn create_script_internal<'c>(
|
||||
None
|
||||
},
|
||||
validate_schema,
|
||||
ns.assets.as_ref().and_then(|a| serde_json::to_value(a).ok())
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -924,16 +928,8 @@ async fn create_script_internal<'c>(
|
||||
}
|
||||
|
||||
clear_asset_usage(&mut *tx, &w_id, &script_path, AssetUsageKind::Script).await?;
|
||||
for asset in parse_assets(&ns.content, ns.language)?.iter().flatten() {
|
||||
insert_asset_usage(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
asset,
|
||||
ns.fallback_access_types.as_ref().map(Vec::as_slice),
|
||||
&ns.path,
|
||||
AssetUsageKind::Script,
|
||||
)
|
||||
.await?;
|
||||
for asset in ns.assets.as_ref().into_iter().flatten() {
|
||||
insert_asset_usage(&mut *tx, &w_id, &asset, &ns.path, AssetUsageKind::Script).await?;
|
||||
}
|
||||
|
||||
let permissioned_as = username_to_permissioned_as(&authed.username);
|
||||
@@ -1126,7 +1122,7 @@ async fn get_script_by_path_w_draft(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let script_o = sqlx::query_as::<_, ScriptWDraft>(
|
||||
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, has_preprocessor, on_behalf_of_email FROM script LEFT JOIN draft ON
|
||||
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, has_preprocessor, on_behalf_of_email, assets 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",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgExecutor;
|
||||
use windmill_parser::asset_parser::ParseAssetsResult;
|
||||
|
||||
use crate::{error, scripts::ScriptLang};
|
||||
use crate::error;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Hash, Eq, sqlx::Type)]
|
||||
#[sqlx(type_name = "ASSET_KIND", rename_all = "lowercase")]
|
||||
@@ -10,7 +9,8 @@ use crate::{error, scripts::ScriptLang};
|
||||
pub enum AssetKind {
|
||||
S3Object,
|
||||
Resource,
|
||||
Variable,
|
||||
// Avoid unnexpected crashes when deserializing old assets
|
||||
Variable, // Deprecated
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Hash, Eq, sqlx::Type)]
|
||||
@@ -41,85 +41,28 @@ pub struct AssetUsage {
|
||||
pub access_type: AssetUsageAccessType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
|
||||
pub struct AssetWithAccessType {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Hash, sqlx::Type)]
|
||||
pub struct AssetWithAltAccessType {
|
||||
pub path: String,
|
||||
pub kind: AssetKind,
|
||||
pub access_type: AssetUsageAccessType,
|
||||
}
|
||||
|
||||
pub fn parse_assets(
|
||||
input: &str,
|
||||
lang: ScriptLang,
|
||||
) -> anyhow::Result<Option<Vec<ParseAssetsResult<String>>>> {
|
||||
let r = match lang {
|
||||
ScriptLang::Python3 => windmill_parser_py::parse_assets(input),
|
||||
ScriptLang::DuckDb => windmill_parser_sql::parse_assets(input).map(|a| {
|
||||
a.iter()
|
||||
.map(|a| ParseAssetsResult {
|
||||
path: a.path.to_string(),
|
||||
access_type: a.access_type,
|
||||
kind: a.kind,
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
ScriptLang::Deno | ScriptLang::Bun | ScriptLang::Nativets => {
|
||||
windmill_parser_ts::parse_assets(input)
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
return r.map(Some);
|
||||
}
|
||||
|
||||
impl From<windmill_parser::asset_parser::AssetKind> for AssetKind {
|
||||
fn from(kind: windmill_parser::asset_parser::AssetKind) -> Self {
|
||||
match kind {
|
||||
windmill_parser::asset_parser::AssetKind::S3Object => AssetKind::S3Object,
|
||||
windmill_parser::asset_parser::AssetKind::Resource => AssetKind::Resource,
|
||||
windmill_parser::asset_parser::AssetKind::Variable => AssetKind::Variable,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<windmill_parser::asset_parser::AssetUsageAccessType> for AssetUsageAccessType {
|
||||
fn from(access_type: windmill_parser::asset_parser::AssetUsageAccessType) -> Self {
|
||||
match access_type {
|
||||
windmill_parser::asset_parser::AssetUsageAccessType::R => AssetUsageAccessType::R,
|
||||
windmill_parser::asset_parser::AssetUsageAccessType::W => AssetUsageAccessType::W,
|
||||
windmill_parser::asset_parser::AssetUsageAccessType::RW => AssetUsageAccessType::RW,
|
||||
}
|
||||
}
|
||||
pub access_type: Option<AssetUsageAccessType>,
|
||||
pub alt_access_type: Option<AssetUsageAccessType>,
|
||||
}
|
||||
|
||||
pub async fn insert_asset_usage<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
workspace_id: &str,
|
||||
parsed_asset: &ParseAssetsResult<String>,
|
||||
fallback_access_types: Option<&[AssetWithAccessType]>,
|
||||
asset: &AssetWithAltAccessType,
|
||||
usage_path: &str,
|
||||
usage_kind: AssetUsageKind,
|
||||
) -> error::Result<()> {
|
||||
let kind: AssetKind = parsed_asset.kind.into();
|
||||
let asset_alternative_access_type = || {
|
||||
fallback_access_types
|
||||
.as_ref()
|
||||
.and_then(|v| {
|
||||
v.iter()
|
||||
.find(|a| a.kind == kind && a.path == parsed_asset.path)
|
||||
})
|
||||
.map(|a| a.access_type)
|
||||
};
|
||||
let access_type: Option<AssetUsageAccessType> = parsed_asset
|
||||
.access_type
|
||||
.map(Into::into)
|
||||
.or_else(asset_alternative_access_type);
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO asset (workspace_id, path, kind, usage_access_type, usage_path, usage_kind)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO NOTHING"#,
|
||||
workspace_id,
|
||||
parsed_asset.path,
|
||||
kind as AssetKind,
|
||||
access_type as Option<AssetUsageAccessType>,
|
||||
asset.path,
|
||||
asset.kind as AssetKind,
|
||||
(asset.access_type.or(asset.alt_access_type)) as Option<AssetUsageAccessType>,
|
||||
usage_path,
|
||||
usage_kind as AssetUsageKind
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ use sqlx::types::Json;
|
||||
use sqlx::types::JsonRawValue;
|
||||
|
||||
use crate::{
|
||||
assets::AssetWithAccessType,
|
||||
assets::AssetWithAltAccessType,
|
||||
cache,
|
||||
error::Error,
|
||||
more_serde::{default_empty_string, default_id, default_null, default_true, is_default},
|
||||
@@ -506,7 +506,7 @@ pub enum FlowModuleValue {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is_trigger: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
asset_fallback_access_types: Option<Vec<AssetWithAccessType>>,
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
},
|
||||
Identity,
|
||||
// Internal only, never exposed to the frontend.
|
||||
@@ -526,6 +526,8 @@ pub enum FlowModuleValue {
|
||||
concurrency_time_window_s: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is_trigger: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -560,7 +562,7 @@ struct UntaggedFlowModuleValue {
|
||||
id: Option<FlowNodeId>,
|
||||
default_node: Option<FlowNodeId>,
|
||||
modules_node: Option<FlowNodeId>,
|
||||
asset_fallback_access_types: Option<Vec<AssetWithAccessType>>,
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
@@ -635,7 +637,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
concurrent_limit: untagged.concurrent_limit,
|
||||
concurrency_time_window_s: untagged.concurrency_time_window_s,
|
||||
is_trigger: untagged.is_trigger,
|
||||
asset_fallback_access_types: untagged.asset_fallback_access_types,
|
||||
assets: untagged.assets,
|
||||
}),
|
||||
"flowscript" => Ok(FlowModuleValue::FlowScript {
|
||||
input_transforms: untagged.input_transforms.unwrap_or_default(),
|
||||
@@ -650,6 +652,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
concurrent_limit: untagged.concurrent_limit,
|
||||
concurrency_time_window_s: untagged.concurrency_time_window_s,
|
||||
is_trigger: untagged.is_trigger,
|
||||
assets: untagged.assets,
|
||||
}),
|
||||
"identity" => Ok(FlowModuleValue::Identity),
|
||||
other => Err(serde::de::Error::unknown_variant(
|
||||
@@ -785,6 +788,7 @@ pub async fn resolve_module(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
assets,
|
||||
} = std::mem::replace(&mut val, Identity)
|
||||
else {
|
||||
unreachable!()
|
||||
@@ -808,7 +812,7 @@ pub async fn resolve_module(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
asset_fallback_access_types: None,
|
||||
assets,
|
||||
};
|
||||
}
|
||||
ForloopFlow { modules, modules_node, .. } | WhileloopFlow { modules, modules_node, .. } => {
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
assets::AssetWithAccessType,
|
||||
assets::AssetWithAltAccessType,
|
||||
error::{to_anyhow, Error},
|
||||
utils::http_get_from_hub,
|
||||
DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL,
|
||||
@@ -250,6 +250,9 @@ pub struct Script {
|
||||
pub has_preprocessor: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub on_behalf_of_email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(json(nullable))]
|
||||
pub assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
@@ -349,7 +352,8 @@ pub struct NewScript {
|
||||
pub codebase: Option<String>,
|
||||
pub has_preprocessor: Option<bool>,
|
||||
pub on_behalf_of_email: Option<String>,
|
||||
pub fallback_access_types: Option<Vec<AssetWithAccessType>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
}
|
||||
|
||||
fn lock_deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
|
||||
@@ -11,9 +11,7 @@ use serde_json::{json, Value};
|
||||
use sha2::Digest;
|
||||
use sqlx::types::Json;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::assets::{
|
||||
clear_asset_usage, insert_asset_usage, parse_assets, AssetUsageKind,
|
||||
};
|
||||
use windmill_common::assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind};
|
||||
use windmill_common::error::Error;
|
||||
use windmill_common::error::Result;
|
||||
use windmill_common::flows::{FlowModule, FlowModuleValue, FlowNodeId};
|
||||
@@ -946,7 +944,7 @@ async fn lock_modules<'c>(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
asset_fallback_access_types,
|
||||
assets,
|
||||
} = e.get_value()?
|
||||
else {
|
||||
match e.get_value()? {
|
||||
@@ -1139,12 +1137,11 @@ async fn lock_modules<'c>(
|
||||
continue;
|
||||
};
|
||||
|
||||
for asset in parse_assets(&content, language)?.iter().flatten() {
|
||||
for asset in assets.iter().flatten() {
|
||||
insert_asset_usage(
|
||||
&mut *tx,
|
||||
&job.workspace_id,
|
||||
asset,
|
||||
asset_fallback_access_types.as_ref().map(Vec::as_slice),
|
||||
job_path,
|
||||
AssetUsageKind::Flow,
|
||||
)
|
||||
@@ -1266,7 +1263,7 @@ async fn lock_modules<'c>(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
asset_fallback_access_types,
|
||||
assets,
|
||||
});
|
||||
new_flow_modules.push(e);
|
||||
|
||||
@@ -1418,6 +1415,7 @@ async fn reduce_flow<'c>(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
assets,
|
||||
..
|
||||
} = std::mem::replace(&mut val, Identity)
|
||||
else {
|
||||
@@ -1436,6 +1434,7 @@ async fn reduce_flow<'c>(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
assets,
|
||||
};
|
||||
}
|
||||
ForloopFlow { modules, modules_node, .. }
|
||||
|
||||
@@ -593,10 +593,10 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
let t = { postgresql: 'postgres', mysql: 'mysql', bigquery: 'bigquery' }[resType]
|
||||
if (!t) {
|
||||
sendUserToast(`Resource type ${resType} is not supported in DuckDB`, true)
|
||||
editor.insertAtCursor(`'$res:${path}'`)
|
||||
editor.insertAtCursor(`'res://${path}'`)
|
||||
return
|
||||
} else {
|
||||
editor.insertAtCursor(`ATTACH '$res:${path}' AS db (TYPE ${t});`)
|
||||
editor.insertAtCursor(`ATTACH 'res://${path}' AS db (TYPE ${t});`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
} from './stepHistoryLoader.svelte'
|
||||
import type { FlowBuilderProps } from './flow_builder'
|
||||
import { ModulesTestStates } from './modulesTest.svelte'
|
||||
import FlowAssetsHandler, { initFlowGraphAssetsCtx } from './flows/FlowAssetsHandler.svelte'
|
||||
|
||||
let {
|
||||
initialPath = $bindable(''),
|
||||
@@ -609,6 +610,11 @@
|
||||
outputPickerOpenFns
|
||||
})
|
||||
|
||||
setContext(
|
||||
'FlowGraphAssetContext',
|
||||
initFlowGraphAssetsCtx({ getModules: () => flowStore.val.value.modules })
|
||||
)
|
||||
|
||||
// Add triggers context store
|
||||
const triggersState = $state(
|
||||
new Triggers(
|
||||
@@ -1256,3 +1262,10 @@
|
||||
renderCount += 1
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlowAssetsHandler
|
||||
modules={flowStore.val.value.modules}
|
||||
enableParser
|
||||
enableDbExplore
|
||||
enablePathScriptAndFlowAssets
|
||||
/>
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
}}
|
||||
globalModuleStates={[]}
|
||||
globalDurationStatuses={[]}
|
||||
bind:localModuleStates
|
||||
bind:localDurationStatuses
|
||||
{localModuleStates}
|
||||
{localDurationStatuses}
|
||||
bind:selectedNode={selectedJobStep}
|
||||
on:start
|
||||
on:done
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import FlowStatusViewerInner from './FlowStatusViewerInner.svelte'
|
||||
|
||||
import {
|
||||
type FlowStatusModule,
|
||||
type Job,
|
||||
@@ -6,20 +8,20 @@
|
||||
type FlowStatus,
|
||||
type FlowModuleValue,
|
||||
type FlowModule,
|
||||
type ScriptArgs
|
||||
ResourceService
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { base } from '$lib/base'
|
||||
import FlowJobResult from './FlowJobResult.svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import { createEventDispatcher, getContext, setContext, tick, untrack } from 'svelte'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { Badge, Button, Skeleton, Tab } from './common'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import { type DurationStatus, type FlowStatusViewerContext, type GraphModuleState } from './graph'
|
||||
import ModuleStatus from './ModuleStatus.svelte'
|
||||
import { isScriptPreview, msToSec, truncateRev } from '$lib/utils'
|
||||
import { clone, isScriptPreview, msToSec, readFieldsRecursively, truncateRev } from '$lib/utils'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import { ChevronDown, Hourglass } from 'lucide-svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
@@ -30,8 +32,10 @@
|
||||
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { buildPrefix } from './graph/graphBuilder.svelte'
|
||||
import { parseAssetFromString, type AssetWithAccessType } from './assets/lib'
|
||||
import { parseInputArgsAssets } from './assets/lib'
|
||||
import FlowPreviewResult from './FlowPreviewResult.svelte'
|
||||
import type { FlowGraphAssetContext } from './flows/types'
|
||||
import { createState } from '$lib/svelte5Utils.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -46,77 +50,110 @@
|
||||
hideJobId
|
||||
} = getContext<FlowStatusViewerContext>('FlowStatusViewer')
|
||||
|
||||
export let jobId: string
|
||||
export let initialJob: Job | undefined = undefined
|
||||
export let workspaceId: string | undefined = undefined
|
||||
export let flowJobIds:
|
||||
| {
|
||||
moduleId: string
|
||||
flowJobs: string[]
|
||||
flowJobsSuccess: (boolean | undefined)[]
|
||||
length: number
|
||||
branchall?: boolean
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
//only useful when forloops are optimized and the job doesn't contain the mod id anymore
|
||||
export let innerModule: FlowModuleValue | undefined = undefined
|
||||
export let globalRefreshes: Record<string, (clear, root) => Promise<void>> = {}
|
||||
export let render = true
|
||||
|
||||
export let isOwner = false
|
||||
|
||||
export let selectedNode: string | undefined = undefined
|
||||
|
||||
export let globalModuleStates: Writable<Record<string, GraphModuleState>>[]
|
||||
export let globalDurationStatuses: Writable<Record<string, DurationStatus>>[]
|
||||
|
||||
export let childFlow: boolean = false
|
||||
export let isSubflow: boolean = false
|
||||
export let reducedPolling = false
|
||||
|
||||
export let wideResults = false
|
||||
export let hideFlowResult = false
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
export let prefix: string | undefined = undefined
|
||||
export let subflowParentsGlobalModuleStates: Writable<Record<string, GraphModuleState>>[] = []
|
||||
export let subflowParentsDurationStatuses: Writable<Record<string, DurationStatus>>[] = []
|
||||
export let isForloopSelected = false
|
||||
export let parentRecursiveRefresh: Record<string, (clear, root) => Promise<void>> = {}
|
||||
export let job: Job | undefined = undefined
|
||||
export let rightColumnSelect: 'timeline' | 'node_status' | 'node_definition' | 'user_states' =
|
||||
'timeline'
|
||||
|
||||
export let localModuleStates: Writable<Record<string, GraphModuleState>> = writable({})
|
||||
export let localDurationStatuses: Writable<Record<string, DurationStatus>> = writable({})
|
||||
let recursiveRefresh: Record<string, (clear, root) => Promise<void>> = {}
|
||||
|
||||
$: inputAssets = parseInputAssets(job?.args ?? {})
|
||||
|
||||
function parseInputAssets(args: ScriptArgs): AssetWithAccessType[] {
|
||||
const arr: AssetWithAccessType[] = []
|
||||
for (const v of Object.values(args)) {
|
||||
if (typeof v === 'string') {
|
||||
const asset = parseAssetFromString(v)
|
||||
if (asset) arr.push(asset)
|
||||
} else if (v && typeof v === 'object' && typeof v['s3'] === 'string') {
|
||||
const s3 = v['s3']
|
||||
const storage = typeof v['storage'] == 'string' ? v['storage'] : undefined
|
||||
arr.push({ kind: 's3object', path: `${storage ?? ''}/${s3}` })
|
||||
}
|
||||
}
|
||||
return arr
|
||||
interface Props {
|
||||
jobId: string
|
||||
initialJob?: Job | undefined
|
||||
workspaceId?: string | undefined
|
||||
flowJobIds?:
|
||||
| {
|
||||
moduleId: string
|
||||
flowJobs: string[]
|
||||
flowJobsSuccess: (boolean | undefined)[]
|
||||
length: number
|
||||
branchall?: boolean
|
||||
}
|
||||
| undefined
|
||||
//only useful when forloops are optimized and the job doesn't contain the mod id anymore
|
||||
innerModule?: FlowModuleValue | undefined
|
||||
globalRefreshes?: Record<string, (clear, root) => Promise<void>>
|
||||
render?: boolean
|
||||
isOwner?: boolean
|
||||
selectedNode?: string | undefined
|
||||
globalModuleStates: Writable<Record<string, GraphModuleState>>[]
|
||||
globalDurationStatuses: Writable<Record<string, DurationStatus>>[]
|
||||
childFlow?: boolean
|
||||
isSubflow?: boolean
|
||||
reducedPolling?: boolean
|
||||
wideResults?: boolean
|
||||
hideFlowResult?: boolean
|
||||
workspace?: string | undefined
|
||||
prefix?: string | undefined
|
||||
subflowParentsGlobalModuleStates?: Writable<Record<string, GraphModuleState>>[]
|
||||
subflowParentsDurationStatuses?: Writable<Record<string, DurationStatus>>[]
|
||||
isForloopSelected?: boolean
|
||||
parentRecursiveRefresh?: Record<string, (clear, root) => Promise<void>>
|
||||
job?: Job | undefined
|
||||
rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states'
|
||||
localModuleStates?: Writable<Record<string, GraphModuleState>>
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
}
|
||||
|
||||
let jobResults: any[] =
|
||||
flowJobIds?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
|
||||
let {
|
||||
jobId,
|
||||
initialJob = undefined,
|
||||
workspaceId = undefined,
|
||||
flowJobIds = undefined,
|
||||
innerModule = undefined,
|
||||
globalRefreshes = $bindable({}),
|
||||
render = true,
|
||||
isOwner = false,
|
||||
selectedNode = $bindable(undefined),
|
||||
globalModuleStates,
|
||||
globalDurationStatuses,
|
||||
childFlow = false,
|
||||
isSubflow = false,
|
||||
reducedPolling = false,
|
||||
wideResults = false,
|
||||
hideFlowResult = false,
|
||||
workspace = $workspaceStore,
|
||||
prefix = undefined,
|
||||
subflowParentsGlobalModuleStates = [],
|
||||
subflowParentsDurationStatuses = [],
|
||||
isForloopSelected = false,
|
||||
parentRecursiveRefresh = $bindable({}),
|
||||
job = $bindable(undefined),
|
||||
rightColumnSelect = $bindable('timeline'),
|
||||
localModuleStates = writable({}),
|
||||
localDurationStatuses = writable({})
|
||||
}: Props = $props()
|
||||
let recursiveRefresh: Record<string, (clear, root) => Promise<void>> = $state({})
|
||||
|
||||
let retry_selected = ''
|
||||
// Add support for the input args assets shown as an asset node
|
||||
const _flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
let extendedFlowGraphAssetsCtx = $state(createState(clone(_flowGraphAssetsCtx)))
|
||||
setContext('FlowGraphAssetContext', extendedFlowGraphAssetsCtx)
|
||||
$effect(() => {
|
||||
readFieldsRecursively(_flowGraphAssetsCtx)
|
||||
job?.args
|
||||
untrack(() => {
|
||||
if (extendedFlowGraphAssetsCtx && _flowGraphAssetsCtx) {
|
||||
const inputAssets = parseInputArgsAssets(job?.args ?? {})
|
||||
const resourceMetadataCache = _flowGraphAssetsCtx.val.resourceMetadataCache
|
||||
for (const asset of inputAssets) {
|
||||
if (asset.kind === 'resource' && !(asset.path in resourceMetadataCache)) {
|
||||
resourceMetadataCache[asset.path] = undefined
|
||||
ResourceService.getResource({
|
||||
workspace: workspace ?? $workspaceStore!,
|
||||
path: asset.path
|
||||
})
|
||||
.then((r) => (resourceMetadataCache[asset.path] = r))
|
||||
.catch((err) => {})
|
||||
}
|
||||
}
|
||||
extendedFlowGraphAssetsCtx.val = clone(_flowGraphAssetsCtx?.val)
|
||||
extendedFlowGraphAssetsCtx.val.additionalAssetsMap['Input'] = inputAssets
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let jobResults: any[] = $state(
|
||||
flowJobIds?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
|
||||
)
|
||||
|
||||
let retry_selected = $state('')
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
let expandedSubflows: Record<string, FlowModule[]> = {}
|
||||
|
||||
$: flowJobIds?.moduleId && onFlowModuleId()
|
||||
let expandedSubflows: Record<string, FlowModule[]> = $state({})
|
||||
|
||||
let selectedId: Writable<string | undefined> = writable(selectedNode)
|
||||
|
||||
@@ -328,7 +365,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let innerModules: FlowStatusModule[] = []
|
||||
let innerModules: FlowStatusModule[] = $state([])
|
||||
|
||||
function updateStatus(status: FlowStatus) {
|
||||
innerModules =
|
||||
@@ -436,8 +473,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: isForloopSelected && globalModuleStates && debounceLoadJobInProgress()
|
||||
|
||||
async function getNewJob(jobId: string, initialJob: Job | undefined) {
|
||||
if (
|
||||
jobId == initialJob?.id &&
|
||||
@@ -446,11 +481,12 @@
|
||||
) {
|
||||
return initialJob
|
||||
} else {
|
||||
return await JobService.getJob({
|
||||
let r = await JobService.getJob({
|
||||
workspace: workspaceId ?? $workspaceStore ?? '',
|
||||
id: jobId ?? '',
|
||||
noLogs: true
|
||||
})
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +510,7 @@
|
||||
}
|
||||
|
||||
let errorCount = 0
|
||||
let notAnonynmous = false
|
||||
let notAnonynmous = $state(false)
|
||||
let started = false
|
||||
async function loadJobInProgress() {
|
||||
if (!started) {
|
||||
@@ -546,20 +582,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: jobId && updateJobId()
|
||||
|
||||
$: isListJob = flowJobIds != undefined && Array.isArray(flowJobIds?.flowJobs)
|
||||
|
||||
function getTopModuleStates() {
|
||||
return get(globalModuleStates?.[globalModuleStates?.length - 1])
|
||||
}
|
||||
|
||||
let forloop_selected = getTopModuleStates()?.[flowJobIds?.moduleId ?? '']?.selectedForloop
|
||||
let forloop_selected = $state(getTopModuleStates()?.[flowJobIds?.moduleId ?? '']?.selectedForloop)
|
||||
|
||||
let sub: Unsubscriber | undefined = undefined
|
||||
let timeoutForloopSelectedSub: NodeJS.Timeout | undefined = undefined
|
||||
let timeoutForloopSelected: NodeJS.Timeout | undefined = undefined
|
||||
$: flowJobIds?.moduleId && onModuleIdChange()
|
||||
|
||||
function onModuleIdChange() {
|
||||
clearTimeout(timeoutForloopSelectedSub)
|
||||
@@ -583,8 +614,6 @@
|
||||
sub?.()
|
||||
})
|
||||
|
||||
$: selected = isListJob ? 'sequence' : 'graph'
|
||||
|
||||
function isSuccess(arg: any): boolean | undefined {
|
||||
if (arg == undefined) {
|
||||
return undefined
|
||||
@@ -815,7 +844,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let flowTimeline: FlowTimeline
|
||||
let flowTimeline: FlowTimeline | undefined = $state()
|
||||
|
||||
function loadPreviousIters(lenToAdd: number) {
|
||||
let r = $localDurationStatuses[flowJobIds?.moduleId ?? '']
|
||||
@@ -831,10 +860,10 @@
|
||||
// updateSlicedListJobIds()
|
||||
}
|
||||
|
||||
let stepDetail: FlowModule | string | undefined = undefined
|
||||
let stepDetail: FlowModule | string | undefined = $state(undefined)
|
||||
|
||||
let storedListJobs: Record<number, Job> = {}
|
||||
let wrapperHeight: number = 0
|
||||
let storedListJobs: Record<number, Job> = $state({})
|
||||
let wrapperHeight: number = $state(0)
|
||||
|
||||
function removeFailureNode(id: string, parent_module: any) {
|
||||
if (id?.startsWith('failure-') && parent_module) {
|
||||
@@ -883,7 +912,22 @@
|
||||
return rec(ids, undefined)
|
||||
}
|
||||
|
||||
let subflowsSize = 500
|
||||
let subflowsSize = $state(500)
|
||||
|
||||
$effect(() => {
|
||||
flowJobIds?.moduleId && untrack(() => onFlowModuleId())
|
||||
})
|
||||
$effect(() => {
|
||||
isForloopSelected && globalModuleStates && untrack(() => debounceLoadJobInProgress())
|
||||
})
|
||||
$effect(() => {
|
||||
jobId && untrack(() => updateJobId())
|
||||
})
|
||||
let isListJob = $derived(flowJobIds != undefined && Array.isArray(flowJobIds?.flowJobs))
|
||||
$effect(() => {
|
||||
flowJobIds?.moduleId && untrack(() => onModuleIdChange())
|
||||
})
|
||||
let selected = $derived(isListJob ? 'sequence' : 'graph')
|
||||
</script>
|
||||
|
||||
{#if notAnonynmous}
|
||||
@@ -906,7 +950,7 @@
|
||||
<p class="text-tertiary italic text-xs">
|
||||
For performance reasons, only the last 20 items are shown by default <button
|
||||
class="text-primary underline ml-4"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
loadPreviousIters(lenToAdd)
|
||||
}}
|
||||
>Load {lenToAdd} prior
|
||||
@@ -915,7 +959,7 @@
|
||||
{sliceFrom}
|
||||
<button
|
||||
class="text-primary underline ml-4"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
loadPreviousIters(allToAdd)
|
||||
}}
|
||||
>Load {allToAdd} prior
|
||||
@@ -1008,7 +1052,7 @@
|
||||
(innerModule?.type != 'forloopflow' && innerModule?.type != 'whileloopflow')}
|
||||
<!-- <LogId id={loopJobId} /> -->
|
||||
<div class="border p-6" class:hidden={forloop_selected != loopJobId}>
|
||||
<svelte:self
|
||||
<FlowStatusViewerInner
|
||||
{globalRefreshes}
|
||||
parentRecursiveRefresh={recursiveRefresh}
|
||||
{childFlow}
|
||||
@@ -1027,7 +1071,7 @@
|
||||
isForloopSelected={forloop_selected == loopJobId &&
|
||||
(innerModule?.type == 'forloopflow' || innerModule?.type == 'whileloopflow')}
|
||||
reducedPolling={reducedPolling ||
|
||||
(flowJobIds?.flowJobs.length && flowJobIds?.flowJobs.length > 20)}
|
||||
(!!flowJobIds?.flowJobs.length && flowJobIds?.flowJobs.length > 20)}
|
||||
{workspaceId}
|
||||
jobId={loopJobId}
|
||||
on:jobsLoaded={(e) => {
|
||||
@@ -1097,7 +1141,7 @@
|
||||
|
||||
<!-- <LogId id={loopJobId} /> -->
|
||||
<div class="border p-6" class:hidden={retry_selected != failedRetry}>
|
||||
<svelte:self
|
||||
<FlowStatusViewerInner
|
||||
{globalRefreshes}
|
||||
parentRecursiveRefresh={recursiveRefresh}
|
||||
{childFlow}
|
||||
@@ -1116,7 +1160,7 @@
|
||||
{/if}
|
||||
{#if ['InProgress', 'Success', 'Failure'].includes(mod.type)}
|
||||
{#if job.raw_flow?.modules[i]?.value.type == 'flow'}
|
||||
<svelte:self
|
||||
<FlowStatusViewerInner
|
||||
{globalRefreshes}
|
||||
parentRecursiveRefresh={recursiveRefresh}
|
||||
globalModuleStates={[]}
|
||||
@@ -1134,7 +1178,7 @@
|
||||
]}
|
||||
render={selected == 'sequence' && render}
|
||||
{workspaceId}
|
||||
jobId={mod.job}
|
||||
jobId={mod.job ?? ''}
|
||||
{reducedPolling}
|
||||
isSubflow
|
||||
childFlow
|
||||
@@ -1146,7 +1190,7 @@
|
||||
{:else if mod.flow_jobs?.length == 0 && mod.job == '00000000-0000-0000-0000-000000000000'}
|
||||
<div class="text-secondary">no subflow (empty loop?)</div>
|
||||
{:else}
|
||||
<svelte:self
|
||||
<FlowStatusViewerInner
|
||||
{globalRefreshes}
|
||||
parentRecursiveRefresh={recursiveRefresh}
|
||||
{childFlow}
|
||||
@@ -1157,14 +1201,14 @@
|
||||
{prefix}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
jobId={mod.job}
|
||||
jobId={mod.job ?? ''}
|
||||
{reducedPolling}
|
||||
innerModule={mod.flow_jobs ? job.raw_flow?.modules[i]?.value : undefined}
|
||||
flowJobIds={mod.flow_jobs
|
||||
? {
|
||||
moduleId: mod.id,
|
||||
moduleId: mod.id ?? '',
|
||||
flowJobs: mod.flow_jobs,
|
||||
flowJobsSuccess: mod.flow_jobs_success,
|
||||
flowJobsSuccess: mod.flow_jobs_success ?? [],
|
||||
length: mod.iterator?.itered?.length ?? mod.flow_jobs.length,
|
||||
branchall: job?.raw_flow?.modules?.[i]?.value?.type == 'branchall'
|
||||
}
|
||||
@@ -1212,7 +1256,6 @@
|
||||
</div>
|
||||
|
||||
<FlowGraphV2
|
||||
{inputAssets}
|
||||
{selectedId}
|
||||
triggerNode={true}
|
||||
download={!hideDownloadInGraph}
|
||||
|
||||
@@ -525,7 +525,7 @@
|
||||
has_preprocessor: script.has_preprocessor,
|
||||
deployment_message: deploymentMsg || undefined,
|
||||
on_behalf_of_email: script.on_behalf_of_email,
|
||||
fallback_access_types: script.fallback_access_types
|
||||
assets: script.assets
|
||||
}
|
||||
})
|
||||
|
||||
@@ -666,7 +666,8 @@
|
||||
visible_to_runner_only: script.visible_to_runner_only,
|
||||
no_main_func: script.no_main_func,
|
||||
has_preprocessor: script.has_preprocessor,
|
||||
on_behalf_of_email: script.on_behalf_of_email
|
||||
on_behalf_of_email: script.on_behalf_of_email,
|
||||
assets: script.assets
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1776,12 +1777,12 @@
|
||||
kind={script.kind}
|
||||
{template}
|
||||
tag={script.tag}
|
||||
bind:fallbackAccessTypes={script.fallback_access_types}
|
||||
lastSavedCode={savedScript?.draft?.content}
|
||||
lastDeployedCode={savedScript?.draft_only ? undefined : savedScript?.content}
|
||||
bind:args
|
||||
bind:hasPreprocessor
|
||||
bind:captureTable
|
||||
bind:assets={script.assets}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
import { BROWSER } from 'esm-env'
|
||||
|
||||
import type { Schema, SupportedLanguage } from '$lib/common'
|
||||
import {
|
||||
AssetService,
|
||||
type CompletedJob,
|
||||
type Job,
|
||||
JobService,
|
||||
type Preview,
|
||||
type ScriptLang
|
||||
} from '$lib/gen'
|
||||
import { type CompletedJob, type Job, JobService, type Preview, type ScriptLang } from '$lib/gen'
|
||||
import { copilotInfo, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { copyToClipboard, emptySchema, sendUserToast } from '$lib/utils'
|
||||
import Editor from './Editor.svelte'
|
||||
@@ -54,8 +47,7 @@
|
||||
import { aiChatManager, AIMode } from './copilot/chat/AIChatManager.svelte'
|
||||
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
|
||||
import AssetsDropdownButton from './assets/AssetsDropdownButton.svelte'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { assetEq, type AssetWithAccessType } from './assets/lib'
|
||||
import { assetEq, type AssetWithAltAccessType } from './assets/lib'
|
||||
|
||||
interface Props {
|
||||
// Exported
|
||||
@@ -86,8 +78,8 @@
|
||||
lastSavedCode?: string | undefined
|
||||
lastDeployedCode?: string | undefined
|
||||
disableAi?: boolean
|
||||
assets?: AssetWithAltAccessType[]
|
||||
editor_bar_right?: import('svelte').Snippet
|
||||
fallbackAccessTypes?: AssetWithAccessType[]
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -117,8 +109,8 @@
|
||||
lastSavedCode = undefined,
|
||||
lastDeployedCode = undefined,
|
||||
disableAi = false,
|
||||
editor_bar_right,
|
||||
fallbackAccessTypes = $bindable()
|
||||
assets = $bindable(),
|
||||
editor_bar_right
|
||||
}: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -147,28 +139,18 @@
|
||||
dispatch('change', { code, schema })
|
||||
})
|
||||
|
||||
let parsedAssets = usePromise(() => inferAssets(lang, code), { clearValueOnRefresh: false })
|
||||
$effect(() => {
|
||||
untrack(() => parsedAssets.refresh()), [lang, code]
|
||||
})
|
||||
|
||||
// Load initial fallbackAccessTypes
|
||||
if (edit && path) {
|
||||
AssetService.listAssetsByUsage({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { usages: [{ path, kind: 'script' }] }
|
||||
}).then((arr) => {
|
||||
const v = arr[0]
|
||||
setTimeout(() => {
|
||||
for (const a of parsedAssets.value ?? []) {
|
||||
const fallback = v.find((a2) => assetEq(a2, a))?.access_type
|
||||
if (!a.access_type && fallback) {
|
||||
fallbackAccessTypes = [...(fallbackAccessTypes ?? []), { ...a, access_type: fallback }]
|
||||
}
|
||||
;[lang, code]
|
||||
untrack(() => {
|
||||
inferAssets(lang, code).then((newAssets: AssetWithAltAccessType[]) => {
|
||||
for (const asset of newAssets) {
|
||||
const old = assets?.find((a) => assetEq(a, asset))
|
||||
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
|
||||
}
|
||||
}, 200)
|
||||
assets = newAssets
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let width = $state(1200)
|
||||
|
||||
@@ -541,8 +523,8 @@
|
||||
<Pane bind:size={codePanelSize} minSize={10} class="!overflow-visible">
|
||||
<div class="h-full !overflow-visible bg-gray-50 dark:bg-[#272D38] relative">
|
||||
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
|
||||
{#if parsedAssets.value?.length}
|
||||
<AssetsDropdownButton assets={parsedAssets.value} bind:fallbackAccessTypes />
|
||||
{#if assets?.length}
|
||||
<AssetsDropdownButton {assets} />
|
||||
{/if}
|
||||
{#if testPanelSize === 0}
|
||||
<HideButton
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { AlertTriangle, Edit2 } from 'lucide-svelte'
|
||||
import { Button } from '../common'
|
||||
import { Tooltip } from '../meltComponents'
|
||||
import ExploreAssetButton, { assetCanBeExplored } from '../ExploreAssetButton.svelte'
|
||||
import S3FilePicker from '../S3FilePicker.svelte'
|
||||
import DbManagerDrawer from '../DBManagerDrawer.svelte'
|
||||
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
|
||||
import type { Asset } from '$lib/gen'
|
||||
|
||||
type Props = {
|
||||
s3FilePicker?: S3FilePicker | undefined
|
||||
dbManagerDrawer?: DbManagerDrawer | undefined
|
||||
resourceEditorDrawer?: ResourceEditorDrawer | undefined
|
||||
resourceDataCache: Record<string, string | undefined>
|
||||
asset: Asset
|
||||
onClick?: () => void
|
||||
}
|
||||
let {
|
||||
s3FilePicker,
|
||||
dbManagerDrawer,
|
||||
resourceEditorDrawer,
|
||||
resourceDataCache,
|
||||
asset,
|
||||
onClick
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if asset.kind === 'resource' && resourceDataCache[asset.path] !== undefined}
|
||||
<Button
|
||||
startIcon={{ icon: Edit2 }}
|
||||
size="xs"
|
||||
variant="border"
|
||||
spacingSize="xs2"
|
||||
iconOnly
|
||||
on:click={() => (resourceEditorDrawer?.initEdit(asset.path), onClick?.())}
|
||||
/>
|
||||
{/if}
|
||||
{#if asset.kind === 'resource' && resourceDataCache[asset.path] === undefined}
|
||||
<Tooltip class="mr-2.5">
|
||||
<AlertTriangle size={16} class="text-orange-600 dark:text-orange-500" />
|
||||
<svelte:fragment slot="text">Could not find resource</svelte:fragment>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if assetCanBeExplored(asset, { resource_type: resourceDataCache[asset.path] })}
|
||||
<ExploreAssetButton
|
||||
{asset}
|
||||
{s3FilePicker}
|
||||
{dbManagerDrawer}
|
||||
onClick={() => onClick?.()}
|
||||
noText
|
||||
_resourceMetadata={{ resource_type: resourceDataCache[asset.path] }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,20 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { clone, pluralize } from '$lib/utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { AlertTriangle, Edit2, Pyramid } from 'lucide-svelte'
|
||||
import { Pyramid } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Popover } from '../meltComponents'
|
||||
import S3FilePicker from '../S3FilePicker.svelte'
|
||||
import ExploreAssetButton, { assetCanBeExplored } from '../ExploreAssetButton.svelte'
|
||||
import { assetEq, formatAssetKind, type Asset, type AssetWithAccessType } from './lib'
|
||||
import {
|
||||
assetsEq,
|
||||
formatAssetAccessType,
|
||||
formatAssetKind,
|
||||
getAccessType,
|
||||
type Asset,
|
||||
type AssetWithAltAccessType
|
||||
} from './lib'
|
||||
import DbManagerDrawer from '../DBManagerDrawer.svelte'
|
||||
import { tick, untrack } from 'svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import Tooltip from '../meltComponents/Tooltip.svelte'
|
||||
import Tooltip2 from '../Tooltip.svelte'
|
||||
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import AssetButtons from './AssetButtons.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
|
||||
@@ -25,17 +31,15 @@
|
||||
noBtnText = false,
|
||||
popoverPlacement = 'bottom-end',
|
||||
disableLiTooltip = false,
|
||||
fallbackAccessTypes = $bindable(),
|
||||
onHoverLi,
|
||||
liSubtitle
|
||||
}: {
|
||||
assets: AssetWithAccessType[]
|
||||
assets: AssetWithAltAccessType[]
|
||||
enableChangeAnimation?: boolean
|
||||
size?: 'xs' | '3xs'
|
||||
noBtnText?: boolean
|
||||
popoverPlacement?: Placement
|
||||
disableLiTooltip?: boolean
|
||||
fallbackAccessTypes?: AssetWithAccessType[]
|
||||
onHoverLi?: (asset: Asset, eventType: 'enter' | 'leave') => void
|
||||
liSubtitle?: (asset: Asset) => string
|
||||
} = $props()
|
||||
@@ -60,7 +64,7 @@
|
||||
$effect(() => {
|
||||
assets
|
||||
untrack(() => {
|
||||
if (deepEqual(assets, prevAssets)) return
|
||||
if (assetsEq(assets, prevAssets)) return
|
||||
prevAssets = clone(assets)
|
||||
|
||||
// Replay animation
|
||||
@@ -113,16 +117,52 @@
|
||||
<svelte:fragment slot="content">
|
||||
<ul class="divide-y rounded-md">
|
||||
{#each assets as asset}
|
||||
{@const fallbackAccessType = fallbackAccessTypes?.find((a) =>
|
||||
assetEq(a, asset)
|
||||
)?.access_type}
|
||||
{@const hasWarning = !asset.access_type && !fallbackAccessType}
|
||||
<li
|
||||
class="text-sm px-4 h-12 flex gap-4 items-center justify-between hover:bg-surface-hover"
|
||||
class="text-sm px-3 h-12 flex gap-3 items-center hover:bg-surface-hover/25"
|
||||
onmouseenter={() => onHoverLi?.(asset, 'enter')}
|
||||
onmouseleave={() => onHoverLi?.(asset, 'leave')}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<Popover
|
||||
contentClasses="py-2 px-4 flex flex-col gap-2"
|
||||
disablePopup={!!asset.access_type}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-xs font-normal border text-tertiary w-10 p-1 text-center rounded-md',
|
||||
!asset.access_type && !asset.alt_access_type
|
||||
? 'text-orange-500 !border-orange-500'
|
||||
: '',
|
||||
!asset.access_type ? 'hover:bg-surface active:opacity-80' : ''
|
||||
)}
|
||||
>
|
||||
{formatAssetAccessType(getAccessType(asset))}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if !asset.access_type}
|
||||
<span class="text-sm text-tertiary leading-4">
|
||||
Could not infer automatically <br />
|
||||
<span class="text-xs">Please select manually </span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<ToggleButtonGroup bind:selected={asset.alt_access_type} class="max-w-fit">
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="r" label="Read" {item} />
|
||||
<ToggleButton value="w" label="Write" {item} />
|
||||
<ToggleButton value="rw" label="Read/Write" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<Tooltip2>
|
||||
This is used to determine if the asset should be displayed as an input or an
|
||||
output node in the flow editor
|
||||
</Tooltip2>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
<div class="flex flex-col flex-1">
|
||||
<Tooltip class="select-none max-w-48 truncate" disablePopup={disableLiTooltip}>
|
||||
{asset.path}
|
||||
<svelte:fragment slot="text">
|
||||
@@ -133,68 +173,21 @@
|
||||
{liSubtitle?.(asset) ??
|
||||
formatAssetKind({
|
||||
...asset,
|
||||
metadata: { resource_type: resourceDataCache[asset.path] }
|
||||
...(asset.kind === 'resource'
|
||||
? { metadata: { resource_type: resourceDataCache[asset.path] } }
|
||||
: {})
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if asset.kind === 'resource' && resourceDataCache[asset.path] !== undefined}
|
||||
<Button
|
||||
startIcon={{ icon: Edit2 }}
|
||||
size="xs"
|
||||
variant="border"
|
||||
spacingSize="xs2"
|
||||
iconOnly
|
||||
on:click={() => (resourceEditorDrawer?.initEdit(asset.path), (isOpen = false))}
|
||||
/>
|
||||
{/if}
|
||||
{#if asset.kind === 'resource' && resourceDataCache[asset.path] === undefined}
|
||||
<Tooltip class="mr-2.5">
|
||||
<AlertTriangle size={16} class="text-orange-600 dark:text-orange-500" />
|
||||
<svelte:fragment slot="text">Could not find resource</svelte:fragment>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if assetCanBeExplored(asset, { resource_type: resourceDataCache[asset.path] })}
|
||||
<ExploreAssetButton
|
||||
{asset}
|
||||
{s3FilePicker}
|
||||
{dbManagerDrawer}
|
||||
onClick={() => (isOpen = false)}
|
||||
noText
|
||||
_resourceMetadata={{ resource_type: resourceDataCache[asset.path] }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ToggleButtonGroup
|
||||
disabled={!!asset.access_type}
|
||||
tabListClass={hasWarning ? 'bg-red-200 dark:bg-red-300' : ''}
|
||||
bind:selected={
|
||||
() => asset.access_type ?? fallbackAccessType,
|
||||
async (access_type) => {
|
||||
fallbackAccessTypes ??= []
|
||||
await tick()
|
||||
let val = fallbackAccessTypes?.filter((a) => !assetEq(a, asset))
|
||||
val.push({ ...asset, access_type })
|
||||
fallbackAccessTypes = val
|
||||
}
|
||||
}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
{#each ['r', 'w', 'rw'] as v}
|
||||
<ToggleButton
|
||||
class={hasWarning
|
||||
? 'bg-transparent hover:bg-red-100 dark:text-primary-inverse'
|
||||
: ''}
|
||||
value={v}
|
||||
label={v}
|
||||
{item}
|
||||
tooltip={'Could not infer access type from code, please select manually'}
|
||||
/>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<AssetButtons
|
||||
onClick={() => (isOpen = false)}
|
||||
{asset}
|
||||
{resourceDataCache}
|
||||
{dbManagerDrawer}
|
||||
{resourceEditorDrawer}
|
||||
{s3FilePicker}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { AssetUsageAccessType, AssetUsageKind } from '$lib/gen'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Drawer, DrawerContent } from '../common'
|
||||
import RowIcon from '../common/table/RowIcon.svelte'
|
||||
import {
|
||||
assetDisplaysAsInputInFlowGraph,
|
||||
assetDisplaysAsOutputInFlowGraph
|
||||
} from '../graph/renderers/nodes/AssetNode.svelte'
|
||||
import { getAssetUsagePageUri } from './lib'
|
||||
import { formatAssetAccessType, getAssetUsagePageUri } from './lib'
|
||||
|
||||
let usagesDrawerData:
|
||||
| {
|
||||
@@ -42,13 +39,13 @@
|
||||
<span class="font-semibold">{u.path}</span>
|
||||
<span class="text-xs text-tertiary">{u.kind}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{#if assetDisplaysAsInputInFlowGraph(u)}
|
||||
<div class="text-xs border text-tertiary max-w-fit p-1 rounded-md">Read</div>
|
||||
{/if}
|
||||
{#if assetDisplaysAsOutputInFlowGraph(u)}
|
||||
<div class="text-xs border text-tertiary max-w-fit p-1 rounded-md">Write</div>
|
||||
{/if}
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-xs font-normal border text-tertiary w-10 p-1 text-center rounded-md',
|
||||
!u.access_type ? 'hover:bg-surface active:opacity-80' : ''
|
||||
)}
|
||||
>
|
||||
{formatAssetAccessType(u.access_type)}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService, type Job } from '$lib/gen'
|
||||
import { inferAssets } from '$lib/infer'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { pruneNullishArrayWithSet, uniqueBy } from '$lib/utils'
|
||||
import DbManagerDrawer from '../DBManagerDrawer.svelte'
|
||||
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
|
||||
import S3FilePicker from '../S3FilePicker.svelte'
|
||||
import AssetButtons from './AssetButtons.svelte'
|
||||
import {
|
||||
formatAssetKind,
|
||||
getFlowModuleAssets,
|
||||
parseInputArgsAssets,
|
||||
type AssetWithAccessType
|
||||
} from './lib'
|
||||
|
||||
type Props = {
|
||||
job: Job
|
||||
}
|
||||
let { job }: Props = $props()
|
||||
|
||||
async function extractAssets(job: Job): Promise<AssetWithAccessType[]> {
|
||||
if (job.job_kind === 'flow') {
|
||||
const additionalAssetsMap = {
|
||||
// TODO : Transitive assets
|
||||
}
|
||||
return uniqueBy(
|
||||
pruneNullishArrayWithSet([
|
||||
...(job.raw_flow?.modules.flatMap((m) => getFlowModuleAssets(m, additionalAssetsMap)) ??
|
||||
[]),
|
||||
...parseInputArgsAssets(job.args ?? {})
|
||||
]),
|
||||
(x) => x.kind + x.path
|
||||
)
|
||||
}
|
||||
if (job.job_kind === 'script') {
|
||||
return [
|
||||
...(await inferAssets(job.language!, job.raw_code ?? '')),
|
||||
...parseInputArgsAssets(job.args ?? {})
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
let assets = usePromise(() => extractAssets(job), { loadInit: false })
|
||||
$effect(() => {
|
||||
job.id
|
||||
$workspaceStore
|
||||
assets.refresh()
|
||||
})
|
||||
|
||||
let resourceDataCache: Record<string, string | undefined> = $state({})
|
||||
$effect(() => {
|
||||
for (const asset of assets.value ?? []) {
|
||||
if (asset.kind !== 'resource' || asset.path in resourceDataCache) continue
|
||||
ResourceService.getResource({ path: asset.path, workspace: $workspaceStore! })
|
||||
.then((resource) => (resourceDataCache[asset.path] = resource.resource_type))
|
||||
.catch((err) => (resourceDataCache[asset.path] = undefined))
|
||||
}
|
||||
})
|
||||
|
||||
let s3FilePicker: S3FilePicker | undefined = $state()
|
||||
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
|
||||
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
|
||||
</script>
|
||||
|
||||
<ul class="flex flex-col divide-y mt-1">
|
||||
{#each assets.value ?? [] as asset}
|
||||
<li class="flex justify-between py-3 leading-4 text-sm pl-4">
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
{asset.path}
|
||||
<span class="text-2xs text-tertiary">
|
||||
{formatAssetKind({
|
||||
...asset,
|
||||
...(asset.kind === 'resource'
|
||||
? { metadata: { resource_type: resourceDataCache[asset.path] } }
|
||||
: {})
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<AssetButtons
|
||||
{asset}
|
||||
{resourceDataCache}
|
||||
{dbManagerDrawer}
|
||||
{resourceEditorDrawer}
|
||||
{s3FilePicker}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
|
||||
<DbManagerDrawer bind:this={dbManagerDrawer} />
|
||||
<ResourceEditorDrawer bind:this={resourceEditorDrawer} />
|
||||
@@ -2,13 +2,17 @@ import type {
|
||||
AssetKind as _AssetKind,
|
||||
Asset as _Asset,
|
||||
ListAssetsResponse,
|
||||
AssetUsageAccessType
|
||||
AssetUsageAccessType,
|
||||
FlowModule,
|
||||
ScriptArgs
|
||||
} from '$lib/gen'
|
||||
import { capitalize } from '$lib/utils'
|
||||
|
||||
export type Asset = _Asset
|
||||
export type AssetKind = _AssetKind
|
||||
export type AssetWithAccessType = Asset & { access_type?: AssetUsageAccessType }
|
||||
export type AssetWithAltAccessType = AssetWithAccessType & {
|
||||
alt_access_type?: AssetUsageAccessType
|
||||
}
|
||||
|
||||
export function formatAsset(asset: Asset): string {
|
||||
switch (asset.kind) {
|
||||
@@ -16,8 +20,6 @@ export function formatAsset(asset: Asset): string {
|
||||
return `res://${asset.path}`
|
||||
case 's3object':
|
||||
return `s3://${asset.path}`
|
||||
case 'variable':
|
||||
return `var://${asset.path}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +36,11 @@ export function assetEq(a: Asset | undefined, b: Asset | undefined): boolean {
|
||||
return a.kind === b.kind && a.path === b.path
|
||||
}
|
||||
|
||||
export function assetsEq(a: Asset[], b: Asset[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((asset, index) => assetEq(asset, b[index]))
|
||||
}
|
||||
|
||||
export function parseAssetFromString(s: string): Asset | undefined {
|
||||
if (s.startsWith('res://')) {
|
||||
return { kind: 'resource', path: s.slice(6) }
|
||||
@@ -41,8 +48,6 @@ export function parseAssetFromString(s: string): Asset | undefined {
|
||||
return { kind: 'resource', path: s.slice(5) }
|
||||
} else if (s.startsWith('s3://')) {
|
||||
return { kind: 's3object', path: s.slice(5) }
|
||||
} else if (s.startsWith('var://')) {
|
||||
return { kind: 'variable', path: s.slice(6) }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -63,7 +68,49 @@ export function formatAssetKind(asset: {
|
||||
}
|
||||
case 's3object':
|
||||
return 'S3 Object'
|
||||
case 'variable':
|
||||
return 'Variable'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatAssetAccessType(accessType: AssetUsageAccessType | undefined) {
|
||||
switch (accessType) {
|
||||
case 'r':
|
||||
return 'Read'
|
||||
case 'w':
|
||||
return 'Write'
|
||||
case 'rw':
|
||||
return 'R/W'
|
||||
}
|
||||
return '?'
|
||||
}
|
||||
|
||||
export function getAccessType(asset: AssetWithAltAccessType): AssetUsageAccessType | undefined {
|
||||
if (asset.alt_access_type) return asset.alt_access_type
|
||||
if (asset.access_type) return asset.access_type
|
||||
}
|
||||
|
||||
export function getFlowModuleAssets(
|
||||
flowModuleValue: FlowModule,
|
||||
additionalAssetsMap?: Record<string, AssetWithAccessType[]>
|
||||
): AssetWithAccessType[] | undefined {
|
||||
if (flowModuleValue.value.type === 'rawscript') return flowModuleValue.value.assets
|
||||
if (flowModuleValue.value.type === 'script' || flowModuleValue.value.type === 'flow') {
|
||||
const additionalAssets = additionalAssetsMap?.[flowModuleValue.id]
|
||||
if (additionalAssets) return additionalAssets
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function parseInputArgsAssets(args: ScriptArgs): AssetWithAccessType[] {
|
||||
const arr: AssetWithAccessType[] = []
|
||||
for (const v of Object.values(args)) {
|
||||
if (typeof v === 'string') {
|
||||
const asset = parseAssetFromString(v)
|
||||
if (asset) arr.push(asset)
|
||||
} else if (v && typeof v === 'object' && typeof v['s3'] === 'string') {
|
||||
const s3 = v['s3']
|
||||
const storage = typeof v['storage'] == 'string' ? v['storage'] : undefined
|
||||
arr.push({ kind: 's3object', path: `${storage ?? ''}/${s3}` })
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<script lang="ts" module>
|
||||
export function initFlowGraphAssetsCtx({
|
||||
getModules
|
||||
}: {
|
||||
getModules: () => FlowModule[]
|
||||
}): FlowGraphAssetContext {
|
||||
let s = $state({
|
||||
val: {
|
||||
selectedAsset: undefined,
|
||||
dbManagerDrawer: undefined,
|
||||
s3FilePicker: undefined,
|
||||
resourceEditorDrawer: undefined,
|
||||
resourceMetadataCache: {},
|
||||
additionalAssetsMap: {},
|
||||
computeAssetsCount: (asset) => {
|
||||
return getAllModules(getModules())
|
||||
.flatMap((m) => getFlowModuleAssets(m, s.val.additionalAssetsMap) ?? [])
|
||||
.filter((a) => assetEq(asset, a)).length
|
||||
}
|
||||
}
|
||||
} satisfies FlowGraphAssetContext)
|
||||
return s
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { inferAssets } from '$lib/infer'
|
||||
import {
|
||||
assetEq,
|
||||
getFlowModuleAssets,
|
||||
type AssetWithAccessType,
|
||||
type AssetWithAltAccessType
|
||||
} from '../assets/lib'
|
||||
import OnChange from '../common/OnChange.svelte'
|
||||
import { getAllModules } from './flowExplorer'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import type { FlowGraphAssetContext } from './types'
|
||||
import {
|
||||
AssetService,
|
||||
ResourceService,
|
||||
type AssetUsageKind,
|
||||
type FlowModule,
|
||||
type RawScript
|
||||
} from '$lib/gen'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import S3FilePicker from '../S3FilePicker.svelte'
|
||||
import DbManagerDrawer from '../DBManagerDrawer.svelte'
|
||||
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
|
||||
|
||||
let {
|
||||
modules,
|
||||
enableParser = false,
|
||||
enableDbExplore = false,
|
||||
enablePathScriptAndFlowAssets = false
|
||||
}: {
|
||||
modules: FlowModule[]
|
||||
enableParser?: boolean
|
||||
enableDbExplore?: boolean
|
||||
enablePathScriptAndFlowAssets?: boolean
|
||||
} = $props()
|
||||
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
|
||||
let allModules = $derived(getAllModules(modules))
|
||||
|
||||
// Fetch resource metadata for the ExploreAssetButton
|
||||
const resMetadataCache = $derived(flowGraphAssetsCtx?.val.resourceMetadataCache)
|
||||
$effect(() => {
|
||||
if (!resMetadataCache || !enableDbExplore) return
|
||||
const assets: AssetWithAccessType[] =
|
||||
allModules.flatMap(
|
||||
(m) => getFlowModuleAssets(m, flowGraphAssetsCtx?.val.additionalAssetsMap) ?? []
|
||||
) ?? []
|
||||
for (const asset of assets) {
|
||||
if (asset.kind !== 'resource' || asset.path in resMetadataCache) continue
|
||||
resMetadataCache[asset.path] = undefined // avoid fetching multiple times because of async
|
||||
ResourceService.getResource({ path: asset.path, workspace: $workspaceStore! })
|
||||
.then((r) => (resMetadataCache[asset.path] = { resource_type: r.resource_type }))
|
||||
.catch((err) => {
|
||||
console.error("Couldn't fetch resource", asset.path, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch transitive assets (path scripts and flows)
|
||||
$effect(() => {
|
||||
if (!$workspaceStore || !flowGraphAssetsCtx || !enablePathScriptAndFlowAssets) return
|
||||
let usages: { path: string; kind: AssetUsageKind }[] = []
|
||||
let modIds: string[] = []
|
||||
for (const mod of allModules) {
|
||||
if (mod.id in flowGraphAssetsCtx.val.additionalAssetsMap) continue
|
||||
flowGraphAssetsCtx.val.additionalAssetsMap[mod.id] = [] // avoid fetching multiple times because of async
|
||||
if (mod.value.type === 'flow' || mod.value.type === 'script') {
|
||||
usages.push({ path: mod.value.path, kind: mod.value.type })
|
||||
modIds.push(mod.id)
|
||||
}
|
||||
}
|
||||
if (usages.length) {
|
||||
AssetService.listAssetsByUsage({
|
||||
workspace: $workspaceStore,
|
||||
requestBody: { usages }
|
||||
}).then((result) => {
|
||||
result.forEach((assets, idx) => {
|
||||
flowGraphAssetsCtx.val.additionalAssetsMap[modIds[idx]] = assets
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
// Prune all additionalAssetsMap entries from deleted modules
|
||||
$effect(() => {
|
||||
if (!flowGraphAssetsCtx) return
|
||||
const modulesSet = new Set(allModules.map((m) => m.id))
|
||||
for (const key of Object.keys(flowGraphAssetsCtx.val.additionalAssetsMap)) {
|
||||
if (!modulesSet.has(key)) {
|
||||
delete flowGraphAssetsCtx.val.additionalAssetsMap[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function parseAndUpdateRawScriptModule(v: RawScript) {
|
||||
try {
|
||||
let parsedAssets: AssetWithAltAccessType[] = await inferAssets(v.language, v.content)
|
||||
for (const asset of parsedAssets) {
|
||||
const old = v.assets?.find((a) => assetEq(a, asset))
|
||||
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
|
||||
}
|
||||
if (!deepEqual(v.assets, parsedAssets)) v.assets = parsedAssets
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Check for raw script modules whose assets were not parsed. Useful for flows created
|
||||
// before the assets feature was introduced.
|
||||
$effect(() => {
|
||||
if (!enableParser) return
|
||||
untrack(() => {
|
||||
setTimeout(() => {
|
||||
for (const mod of allModules) {
|
||||
if (mod.value.type === 'rawscript' && mod.value.assets === undefined) {
|
||||
console.log('RawScript module', mod.id, 'without assets field, parsing')
|
||||
parseAndUpdateRawScriptModule(mod.value)
|
||||
}
|
||||
}
|
||||
}, 500) // ensure modules are loaded
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if enableParser}
|
||||
{#each allModules as mod (mod.id)}
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
{@const v = mod.value}
|
||||
<OnChange key={v.content} onChange={() => parseAndUpdateRawScriptModule(v)} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if flowGraphAssetsCtx}
|
||||
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} readOnlyMode />
|
||||
<DbManagerDrawer bind:this={flowGraphAssetsCtx.val.dbManagerDrawer} />
|
||||
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} />
|
||||
{/if}
|
||||
@@ -51,10 +51,9 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { checkIfParentLoop } from '../utils'
|
||||
import ModulePreviewResultViewer from '$lib/components/ModulePreviewResultViewer.svelte'
|
||||
import { refreshStateStore, usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
|
||||
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
|
||||
import AssetsDropdownButton from '$lib/components/assets/AssetsDropdownButton.svelte'
|
||||
import { inferAssets } from '$lib/infer'
|
||||
|
||||
const {
|
||||
selectedId,
|
||||
@@ -123,6 +122,8 @@
|
||||
let testIsLoading = $state(false)
|
||||
let scriptProgress = $state(undefined)
|
||||
|
||||
let assets = $derived((flowModule.value.type === 'rawscript' && flowModule.value.assets) || [])
|
||||
|
||||
function onModulesChange(savedModule: FlowModule | undefined, flowModule: FlowModule) {
|
||||
// console.log('onModulesChange', savedModule, flowModule)
|
||||
return savedModule?.value?.type === 'rawscript' &&
|
||||
@@ -299,19 +300,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
let assets = usePromise(
|
||||
async () =>
|
||||
flowModule.value.type === 'rawscript'
|
||||
? await inferAssets(flowModule.value.language, flowModule.value.content)
|
||||
: undefined,
|
||||
{ clearValueOnRefresh: false, loadInit: false }
|
||||
)
|
||||
$effect(() => {
|
||||
if (flowModule.value.type !== 'rawscript') return
|
||||
;[flowModule.value.content, flowModule.value.language]
|
||||
untrack(() => assets.refresh())
|
||||
})
|
||||
|
||||
let rawScriptLang = $derived(
|
||||
flowModule.value.type == 'rawscript' ? flowModule.value.language : undefined
|
||||
)
|
||||
@@ -422,11 +410,8 @@
|
||||
{#if !noEditor}
|
||||
{#key flowModule.id}
|
||||
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
|
||||
{#if assets.value?.length}
|
||||
<AssetsDropdownButton
|
||||
assets={assets.value}
|
||||
bind:fallbackAccessTypes={flowModule.value.asset_fallback_access_types}
|
||||
/>
|
||||
{#if assets?.length}
|
||||
<AssetsDropdownButton {assets} />
|
||||
{/if}
|
||||
</div>
|
||||
<Editor
|
||||
|
||||
@@ -89,10 +89,11 @@ export type FlowEditorContext = {
|
||||
|
||||
export type FlowGraphAssetContext = StateStore<{
|
||||
selectedAsset: Asset | undefined
|
||||
assetsMap: Record<string, AssetWithAccessType[]> // Maps module ids to their assets
|
||||
s3FilePicker: S3FilePicker | undefined
|
||||
dbManagerDrawer: DbManagerDrawer | undefined
|
||||
resourceEditorDrawer: ResourceEditorDrawer | undefined
|
||||
// Maps resource paths to their metadata. undefined is for error
|
||||
resourceMetadataCache: Record<string, { resource_type?: string } | undefined>
|
||||
additionalAssetsMap: Record<string, AssetWithAccessType[]>
|
||||
computeAssetsCount: (asset: Asset) => number
|
||||
}>
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
AssetService,
|
||||
FlowService,
|
||||
ResourceService,
|
||||
type AssetUsageKind,
|
||||
type FlowModule,
|
||||
type Job
|
||||
} from '../../gen'
|
||||
import { FlowService, type FlowModule, type Job } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { getContext, onDestroy, setContext, tick, untrack } from 'svelte'
|
||||
|
||||
@@ -58,15 +51,8 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import ViewportResizer from './ViewportResizer.svelte'
|
||||
import AssetNode, { computeAssetNodes } from './renderers/nodes/AssetNode.svelte'
|
||||
import type { FlowGraphAssetContext } from '../flows/types'
|
||||
import { getAllModules } from '../flows/flowExplorer'
|
||||
import { inferAssets } from '$lib/infer'
|
||||
import OnChange from '../common/OnChange.svelte'
|
||||
import S3FilePicker from '../S3FilePicker.svelte'
|
||||
import DbManagerDrawer from '../DBManagerDrawer.svelte'
|
||||
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
|
||||
import { assetEq, type AssetWithAccessType } from '../assets/lib'
|
||||
import AssetsOverflowedNode from './renderers/nodes/AssetsOverflowedNode.svelte'
|
||||
import type { FlowGraphAssetContext } from '../flows/types'
|
||||
|
||||
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
|
||||
|
||||
@@ -103,7 +89,6 @@
|
||||
editMode?: boolean
|
||||
allowSimplifiedPoll?: boolean
|
||||
expandedSubflows?: Record<string, FlowModule[]>
|
||||
inputAssets?: AssetWithAccessType[]
|
||||
isOwner?: boolean
|
||||
isRunning?: boolean
|
||||
individualStepTests?: boolean
|
||||
@@ -174,7 +159,6 @@
|
||||
editMode = false,
|
||||
allowSimplifiedPoll = true,
|
||||
expandedSubflows = $bindable({}),
|
||||
inputAssets,
|
||||
onTestUpTo = undefined,
|
||||
onEditInput = undefined,
|
||||
isOwner = false,
|
||||
@@ -204,67 +188,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
const flowGraphAssetsCtx: FlowGraphAssetContext = $state({
|
||||
val: {
|
||||
assetsMap: inputAssets ? ({ Input: inputAssets } as any) : {},
|
||||
selectedAsset: undefined,
|
||||
dbManagerDrawer: undefined,
|
||||
s3FilePicker: undefined,
|
||||
resourceEditorDrawer: undefined,
|
||||
resourceMetadataCache: {}
|
||||
}
|
||||
})
|
||||
setContext<FlowGraphAssetContext>('FlowGraphAssetContext', flowGraphAssetsCtx)
|
||||
const assetsMap = $derived(flowGraphAssetsCtx.val.assetsMap)
|
||||
$effect(() => {
|
||||
if (inputAssets) flowGraphAssetsCtx.val.assetsMap.Input = inputAssets
|
||||
})
|
||||
|
||||
// Fetch resource metadata for the ExploreAssetButton
|
||||
const resMetadataCache = $derived(flowGraphAssetsCtx.val.resourceMetadataCache)
|
||||
$effect(() => {
|
||||
for (const asset of Object.values(assetsMap ?? []).flatMap((x) => x)) {
|
||||
if (asset.kind !== 'resource' || asset.path in resMetadataCache) continue
|
||||
resMetadataCache[asset.path] = undefined // avoid fetching multiple times because of async
|
||||
ResourceService.getResource({ path: asset.path, workspace: $workspaceStore! }).then(
|
||||
(r) => (resMetadataCache[asset.path] = { resource_type: r.resource_type })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch transitive assets (path scripts and flows)
|
||||
$effect(() => {
|
||||
if (!$workspaceStore) return
|
||||
let usages: { path: string; kind: AssetUsageKind }[] = []
|
||||
let modIds: string[] = []
|
||||
for (const mod of getAllModules(modules)) {
|
||||
if (mod.id in assetsMap) continue
|
||||
assetsMap[mod.id] = [] // avoid fetching multiple times because of async
|
||||
if (mod.value.type === 'flow' || mod.value.type === 'script') {
|
||||
usages.push({ path: mod.value.path, kind: mod.value.type })
|
||||
modIds.push(mod.id)
|
||||
}
|
||||
}
|
||||
if (usages.length) {
|
||||
AssetService.listAssetsByUsage({
|
||||
workspace: $workspaceStore,
|
||||
requestBody: { usages }
|
||||
}).then((result) => {
|
||||
result.forEach((assets, idx) => {
|
||||
assetsMap[modIds[idx]] = assets
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Prune assetsMap to only contain assets that are actually used
|
||||
$effect(() => {
|
||||
const allModules = new Set(getAllModules(modules).map((mod) => mod.id))
|
||||
for (const modId in assetsMap) {
|
||||
if (modId !== 'Input' && !allModules.has(modId)) delete assetsMap[modId]
|
||||
}
|
||||
})
|
||||
|
||||
function computeSimplifiableFlow(modules: FlowModule[], simplifiedFlow: boolean) {
|
||||
const isSimplif = isSimplifiable(modules)
|
||||
simplifiableFlow = isSimplif ? { simplifiedFlow } : undefined
|
||||
@@ -283,8 +206,8 @@
|
||||
)
|
||||
}
|
||||
|
||||
let lastNodes: [NodeLayout[], Node[]] | undefined = undefined
|
||||
function layoutNodes(nodes: NodeLayout[]): Node[] {
|
||||
let lastNodes: [NodeLayout[], (Node & NodeLayout)[]] | undefined = undefined
|
||||
function layoutNodes(nodes: NodeLayout[]): (Node & NodeLayout)[] {
|
||||
let lastResult = lastNodes?.[1]
|
||||
if (lastResult && nodes === lastNodes?.[0]) {
|
||||
return lastResult
|
||||
@@ -457,11 +380,8 @@
|
||||
}
|
||||
let newGraph = graph
|
||||
newGraph.nodes.sort((a, b) => b.id.localeCompare(a.id))
|
||||
;[nodes, edges] = computeAssetNodes(layoutNodes(newGraph.nodes), newGraph.edges, assetsMap, {
|
||||
moving,
|
||||
eventHandlers: eventHandler,
|
||||
disableAi
|
||||
})
|
||||
console.log('compute')
|
||||
;[nodes, edges] = computeAssetNodes(layoutNodes(newGraph.nodes), newGraph.edges)
|
||||
await tick()
|
||||
height = Math.max(...nodes.map((n) => n.position.y + NODE.height + 100), minHeight)
|
||||
}
|
||||
@@ -499,11 +419,14 @@
|
||||
// })
|
||||
let yamlEditorDrawer: Drawer | undefined = $state(undefined)
|
||||
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
|
||||
$effect(() => {
|
||||
allowSimplifiedPoll && modules && untrack(() => onModulesChange(modules ?? []))
|
||||
})
|
||||
$effect(() => {
|
||||
modules && untrack(() => onModulesChange2(modules))
|
||||
readFieldsRecursively(modules)
|
||||
untrack(() => onModulesChange2(modules))
|
||||
})
|
||||
let graph = $derived.by(() => {
|
||||
moduleCounter
|
||||
@@ -525,7 +448,8 @@
|
||||
flowJob,
|
||||
showJobStatus,
|
||||
suspendStatus,
|
||||
flowHasChanged
|
||||
flowHasChanged,
|
||||
additionalAssetsMap: flowGraphAssetsCtx?.val.additionalAssetsMap
|
||||
},
|
||||
failureModule,
|
||||
preprocessorModule,
|
||||
@@ -541,7 +465,6 @@
|
||||
})
|
||||
$effect(() => {
|
||||
;[graph, allowSimplifiedPoll]
|
||||
readFieldsRecursively(assetsMap)
|
||||
untrack(() => updateStores())
|
||||
})
|
||||
|
||||
@@ -670,33 +593,6 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#each getAllModules(modules) as mod (mod.id)}
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
{@const v = mod.value}
|
||||
<OnChange
|
||||
key={[v.content, v.asset_fallback_access_types]}
|
||||
runFirstEffect
|
||||
onChange={() =>
|
||||
inferAssets(v.language, v.content)
|
||||
.then((assets) => {
|
||||
for (const override of v.asset_fallback_access_types ?? []) {
|
||||
assets = assets.map((asset) => {
|
||||
if (assetEq(asset, override) && !asset.access_type)
|
||||
return { ...asset, access_type: override.access_type }
|
||||
return asset
|
||||
})
|
||||
}
|
||||
if (assetsMap && !deepEqual(assetsMap[mod.id], assets)) assetsMap[mod.id] = assets
|
||||
})
|
||||
.catch((e) => {})}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} readOnlyMode />
|
||||
<DbManagerDrawer bind:this={flowGraphAssetsCtx.val.dbManagerDrawer} />
|
||||
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} />
|
||||
|
||||
<style lang="postcss">
|
||||
:global(.svelte-flow__handle) {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { FlowModule, Job, RawScript, Script } from '$lib/gen'
|
||||
import { type Edge } from '@xyflow/svelte'
|
||||
import { getDependeeAndDependentComponents } from '../flows/flowExplorer'
|
||||
import { getAllModules, getDependeeAndDependentComponents } from '../flows/flowExplorer'
|
||||
import { dfsByModule } from '../flows/previousResults'
|
||||
import { defaultIfEmptyString } from '$lib/utils'
|
||||
import type { GraphModuleState } from './model'
|
||||
import type { AssetWithAccessType } from '../assets/lib'
|
||||
import { getFlowModuleAssets, type AssetWithAltAccessType } from '../assets/lib'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { assetDisplaysAsOutputInFlowGraph } from './renderers/nodes/AssetNode.svelte'
|
||||
|
||||
export type InsertKind =
|
||||
| 'script'
|
||||
@@ -115,6 +116,7 @@ export type InputN = {
|
||||
flowJob: Job | undefined
|
||||
showJobStatus: boolean
|
||||
flowHasChanged: boolean
|
||||
assets?: AssetWithAltAccessType[] | undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +134,7 @@ export type ModuleN = {
|
||||
editMode: boolean
|
||||
flowJob: Job | undefined
|
||||
isOwner: boolean
|
||||
assets: AssetWithAltAccessType[] | undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,14 +280,14 @@ export type TriggerN = {
|
||||
export type AssetN = {
|
||||
type: 'asset'
|
||||
data: {
|
||||
asset: AssetWithAccessType
|
||||
asset: AssetWithAltAccessType
|
||||
}
|
||||
}
|
||||
|
||||
export type AssetsOverflowedN = {
|
||||
type: 'assetsOverflowed'
|
||||
data: {
|
||||
overflowedAssets: AssetWithAccessType[]
|
||||
overflowedAssets: AssetWithAltAccessType[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +325,7 @@ export function graphBuilder(
|
||||
showJobStatus: boolean
|
||||
suspendStatus: Writable<Record<string, { job: Job; nb: number }>>
|
||||
flowHasChanged: boolean
|
||||
additionalAssetsMap?: Record<string, AssetWithAltAccessType[]>
|
||||
},
|
||||
failureModule: FlowModule | undefined,
|
||||
preprocessorModule: FlowModule | undefined,
|
||||
@@ -374,7 +378,8 @@ export function graphBuilder(
|
||||
insertable: extra.insertable,
|
||||
editMode: extra.editMode,
|
||||
isOwner: extra.isOwner,
|
||||
flowJob: extra.flowJob
|
||||
flowJob: extra.flowJob,
|
||||
assets: getFlowModuleAssets(module, extra.additionalAssetsMap)
|
||||
},
|
||||
type: 'module'
|
||||
})
|
||||
@@ -382,6 +387,15 @@ export function graphBuilder(
|
||||
return module.id
|
||||
}
|
||||
|
||||
// TODO : Do better than this
|
||||
const nodeIdsWithOutputAssets = new Set(
|
||||
getAllModules(modules)
|
||||
.filter((m) =>
|
||||
getFlowModuleAssets(m, extra.additionalAssetsMap)?.some(assetDisplaysAsOutputInFlowGraph)
|
||||
)
|
||||
.map((m) => m.id)
|
||||
)
|
||||
|
||||
const parents: { [key: string]: string[] } = {}
|
||||
|
||||
//
|
||||
@@ -458,11 +472,13 @@ export function graphBuilder(
|
||||
// If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array
|
||||
index: index >= 0 ? index : (mods?.length ?? 0),
|
||||
...extra,
|
||||
insertable: extra.insertable && !options?.disableInsert && prefix == undefined
|
||||
insertable: extra.insertable && !options?.disableInsert && prefix == undefined,
|
||||
shouldOffsetInsertBtnDueToAssetNode: nodeIdsWithOutputAssets.has(sourceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const inputAssets = extra.additionalAssetsMap?.['Input']
|
||||
const inputNode: NodeLayout = {
|
||||
id: 'Input',
|
||||
type: 'input2',
|
||||
@@ -479,7 +495,8 @@ export function graphBuilder(
|
||||
individualStepTests: extra.individualStepTests,
|
||||
flowJob: extra.flowJob,
|
||||
showJobStatus: extra.showJobStatus,
|
||||
flowHasChanged: extra.flowHasChanged
|
||||
flowHasChanged: extra.flowHasChanged,
|
||||
...(inputAssets ? { assets: inputAssets } : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
import type { GraphEventHandlers } from '../../graphBuilder.svelte'
|
||||
import { getStraightLinePath } from '../utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { type FlowGraphAssetContext } from '$lib/components/flows/types'
|
||||
import {
|
||||
assetDisplaysAsOutputInFlowGraph,
|
||||
NODE_WITH_WRITE_ASSET_Y_OFFSET
|
||||
} from '../nodes/AssetNode.svelte'
|
||||
import { NODE_WITH_WRITE_ASSET_Y_OFFSET } from '../nodes/AssetNode.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import FlowStatusWaitingForEvents from '$lib/components/FlowStatusWaitingForEvents.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
@@ -21,8 +17,6 @@
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
}>('FlowGraphContext')
|
||||
|
||||
const flowGraphAssetCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
|
||||
let {
|
||||
// id,
|
||||
sourceX,
|
||||
@@ -50,13 +44,10 @@
|
||||
isOwner: boolean
|
||||
flowJob: Job | undefined
|
||||
suspendStatus?: Writable<Record<string, { job: Job; nb: number }>>
|
||||
shouldOffsetInsertBtnDueToAssetNode?: boolean
|
||||
}
|
||||
} = $props()
|
||||
|
||||
const shouldOffsetInsertButtonDueToAssetNode = flowGraphAssetCtx?.val.assetsMap?.[
|
||||
data.sourceId
|
||||
]?.some(assetDisplaysAsOutputInFlowGraph)
|
||||
|
||||
let [edgePath] = $derived(
|
||||
getBezierPath({
|
||||
sourceX,
|
||||
@@ -89,7 +80,7 @@
|
||||
|
||||
<EdgeLabel
|
||||
x={sourceX}
|
||||
y={sourceY + 28 + (shouldOffsetInsertButtonDueToAssetNode ? NODE_WITH_WRITE_ASSET_Y_OFFSET : 0)}
|
||||
y={sourceY + 28 + (data.shouldOffsetInsertBtnDueToAssetNode ? NODE_WITH_WRITE_ASSET_Y_OFFSET : 0)}
|
||||
class="base-edge"
|
||||
style=""
|
||||
>
|
||||
|
||||
@@ -3,33 +3,31 @@
|
||||
export const NODE_WITH_WRITE_ASSET_Y_OFFSET = 45
|
||||
export const READ_ASSET_Y_OFFSET = -45
|
||||
export const WRITE_ASSET_Y_OFFSET = 64
|
||||
export const assetDisplaysAsInputInFlowGraph = (a: { access_type?: AssetUsageAccessType }) =>
|
||||
!a.access_type || a.access_type === 'r' || a.access_type === 'rw'
|
||||
export const assetDisplaysAsOutputInFlowGraph = (a: { access_type?: AssetUsageAccessType }) =>
|
||||
a.access_type === 'w' || a.access_type === 'rw'
|
||||
export const assetDisplaysAsInputInFlowGraph = (a: AssetWithAltAccessType) =>
|
||||
!getAccessType(a) || getAccessType(a) === 'r' || getAccessType(a) === 'rw'
|
||||
export const assetDisplaysAsOutputInFlowGraph = (a: AssetWithAltAccessType) =>
|
||||
getAccessType(a) === 'w' || getAccessType(a) === 'rw'
|
||||
|
||||
let computeAssetNodesCache:
|
||||
| [Node[], Record<string, AssetWithAccessType[]>, ReturnType<typeof computeAssetNodes>]
|
||||
| [(Node & NodeLayout)[], ReturnType<typeof computeAssetNodes>]
|
||||
| undefined
|
||||
|
||||
export function computeAssetNodes(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
assetsMap: Record<string, AssetWithAccessType[]>,
|
||||
extraData: any
|
||||
): [Node[], Edge[]] {
|
||||
if (nodes === computeAssetNodesCache?.[0] && deepEqual(assetsMap, computeAssetNodesCache?.[1]))
|
||||
return computeAssetNodesCache[2]
|
||||
nodes: (Node & NodeLayout)[],
|
||||
edges: Edge[]
|
||||
): [(Node & NodeLayout)[], Edge[]] {
|
||||
if (nodes === computeAssetNodesCache?.[0]) return computeAssetNodesCache[1]
|
||||
|
||||
const MAX_ASSET_ROW_WIDTH = 300
|
||||
const ASSETS_OVERFLOWED_NODE_WIDTH = 25
|
||||
const allAssetNodes: Node[] = []
|
||||
const allAssetNodes: (Node & NodeLayout)[] = []
|
||||
const allAssetEdges: Edge[] = []
|
||||
|
||||
const yPosMap: Record<number, { r?: true; w?: true }> = {}
|
||||
|
||||
for (const node of nodes) {
|
||||
const assets = assetsMap?.[node.id] ?? []
|
||||
if (node.type !== 'module' && node.type !== 'input2') continue
|
||||
const assets = node.data.assets ?? []
|
||||
|
||||
// Each asset can be displayed at the top and bottom
|
||||
// i.e once (R or W) or twice (RW)
|
||||
@@ -197,16 +195,21 @@
|
||||
[...sortedNewNodes, ...allAssetNodes],
|
||||
[...edges, ...allAssetEdges]
|
||||
]
|
||||
computeAssetNodesCache = [nodes, clone(assetsMap), ret]
|
||||
computeAssetNodesCache = [nodes, ret]
|
||||
return ret
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { AssetN, AssetsOverflowedN } from '../../graphBuilder.svelte'
|
||||
import type { AssetN, AssetsOverflowedN, NodeLayout } from '../../graphBuilder.svelte'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
import { assetEq, formatAssetKind, type AssetWithAccessType } from '$lib/components/assets/lib'
|
||||
import {
|
||||
assetEq,
|
||||
formatAssetKind,
|
||||
getAccessType,
|
||||
type AssetWithAltAccessType
|
||||
} from '$lib/components/assets/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { FlowGraphAssetContext } from '$lib/components/flows/types'
|
||||
import { getContext } from 'svelte'
|
||||
@@ -215,28 +218,21 @@
|
||||
import { clone, pluralize } from '$lib/utils'
|
||||
import AssetGenericIcon from '$lib/components/icons/AssetGenericIcon.svelte'
|
||||
import type { Edge, Node } from '@xyflow/svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
import { NODE } from '../../util'
|
||||
import type { AssetUsageAccessType } from '$lib/gen'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
interface Props {
|
||||
data: AssetN['data']
|
||||
}
|
||||
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext>('FlowGraphAssetContext')
|
||||
|
||||
const usageCount = $derived(
|
||||
Object.values(flowGraphAssetsCtx.val.assetsMap ?? {})
|
||||
.flat()
|
||||
.filter((asset) => assetEq(asset, data.asset)).length
|
||||
)
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
|
||||
let { data }: Props = $props()
|
||||
const isSelected = $derived(assetEq(flowGraphAssetsCtx.val.selectedAsset, data.asset))
|
||||
|
||||
const isSelected = $derived(assetEq(flowGraphAssetsCtx?.val.selectedAsset, data.asset))
|
||||
const cachedResourceMetadata = $derived(
|
||||
flowGraphAssetsCtx.val.resourceMetadataCache[data.asset.path]
|
||||
flowGraphAssetsCtx?.val.resourceMetadataCache[data.asset.path]
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -249,8 +245,10 @@
|
||||
'bg-surface h-6 flex items-center gap-1.5 rounded-sm text-tertiary border overflow-clip',
|
||||
isSelected ? 'bg-surface-secondary !border-surface-inverse' : 'border-transparent'
|
||||
)}
|
||||
onmouseenter={() => (flowGraphAssetsCtx.val.selectedAsset = data.asset)}
|
||||
onmouseleave={() => (flowGraphAssetsCtx.val.selectedAsset = undefined)}
|
||||
onmouseenter={() =>
|
||||
flowGraphAssetsCtx && (flowGraphAssetsCtx.val.selectedAsset = data.asset)}
|
||||
onmouseleave={() =>
|
||||
flowGraphAssetsCtx && (flowGraphAssetsCtx.val.selectedAsset = undefined)}
|
||||
>
|
||||
<AssetGenericIcon
|
||||
assetKind={data.asset.kind}
|
||||
@@ -272,14 +270,17 @@
|
||||
asset={data.asset}
|
||||
noText
|
||||
buttonVariant="contained"
|
||||
s3FilePicker={flowGraphAssetsCtx.val.s3FilePicker}
|
||||
dbManagerDrawer={flowGraphAssetsCtx.val.dbManagerDrawer}
|
||||
s3FilePicker={flowGraphAssetsCtx?.val.s3FilePicker}
|
||||
dbManagerDrawer={flowGraphAssetsCtx?.val.dbManagerDrawer}
|
||||
_resourceMetadata={cachedResourceMetadata}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
Used in {pluralize(usageCount, 'step')}<br />
|
||||
Used in {pluralize(
|
||||
flowGraphAssetsCtx?.val.computeAssetsCount?.(data.asset) ?? -1,
|
||||
'step'
|
||||
)}<br />
|
||||
<a
|
||||
href={undefined}
|
||||
class={twMerge(
|
||||
@@ -290,7 +291,7 @@
|
||||
)}
|
||||
onclick={() => {
|
||||
if (data.asset.kind === 'resource')
|
||||
flowGraphAssetsCtx.val.resourceEditorDrawer?.initEdit(data.asset.path)
|
||||
flowGraphAssetsCtx?.val.resourceEditorDrawer?.initEdit(data.asset.path)
|
||||
}}
|
||||
>
|
||||
{data.asset.path}
|
||||
@@ -298,6 +299,8 @@
|
||||
<span class="dark:text-tertiary text-tertiary-inverse text-xs"
|
||||
>{formatAssetKind({ ...data.asset, metadata: cachedResourceMetadata })}</span
|
||||
>
|
||||
<br />
|
||||
fdsfs
|
||||
</svelte:fragment>
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
data: AssetsOverflowedN['data']
|
||||
}
|
||||
let { data }: Props = $props()
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext>('FlowGraphAssetContext')
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
|
||||
let isOpen = $state(false)
|
||||
|
||||
let includesSelected = $derived(
|
||||
data.overflowedAssets.some((asset) => assetEq(flowGraphAssetsCtx.val.selectedAsset, asset))
|
||||
data.overflowedAssets.some((asset) => assetEq(flowGraphAssetsCtx?.val.selectedAsset, asset))
|
||||
)
|
||||
|
||||
let wasOpenedBecauseOfExternalSelected = false
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type { AssetKind } from '../assets/lib'
|
||||
import AssetResIcon from './AssetResIcon.svelte'
|
||||
import AssetS3Icon from './AssetS3Icon.svelte'
|
||||
import AssetVarIcon from './AssetVarIcon.svelte'
|
||||
|
||||
interface Props {
|
||||
size?: string
|
||||
@@ -19,8 +18,6 @@
|
||||
<AssetS3Icon {fill} width={size} height={size} class={className} />
|
||||
{:else if assetKind == 'resource'}
|
||||
<AssetResIcon {fill} width={size} height={size} class={className} />
|
||||
{:else if assetKind == 'variable'}
|
||||
<AssetVarIcon {fill} width={size} height={size} class={className} />
|
||||
{:else}
|
||||
<Pyramid {size} color={fill} class={'!fill-none ' + className} />
|
||||
{/if}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string
|
||||
width?: string
|
||||
fill?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px', fill = 'black', class: className = '' }: Props = $props()
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
class={className}
|
||||
viewBox="0 0 22 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1_2)">
|
||||
<path
|
||||
d="M11.0586 0.306641C11.3106 0.315989 11.5582 0.378277 11.7822 0.495117C12.0247 0.621655 12.2331 0.805033 12.3897 1.0293L12.3916 1.03223L17.9033 9H16.2022L11.6963 2.4873V20.0986L12.75 19.4961V21.1094L12.333 21.3477L12.332 21.3486C11.925 21.5804 11.4645 21.7021 10.9961 21.7021C10.5275 21.7021 10.0664 21.5806 9.65919 21.3486V21.3477L1.14844 16.4883L1.14258 16.4844C0.940872 16.3666 0.765826 16.2081 0.627936 16.0195C0.489969 15.8308 0.391887 15.6158 0.340827 15.3877C0.289772 15.1595 0.286682 14.9229 0.331061 14.6934C0.375447 14.4641 0.466395 14.2462 0.598639 14.0537L0.599616 14.0518L9.59962 1.03223L9.60157 1.0293C9.75823 0.804889 9.9673 0.62167 10.21 0.495117C10.4336 0.378564 10.6802 0.316161 10.9316 0.306641C10.9528 0.304686 10.9744 0.299805 10.9961 0.299805C11.0172 0.299841 11.038 0.304797 11.0586 0.306641ZM1.75196 14.8457C1.72867 14.8796 1.71294 14.9186 1.70508 14.959C1.69725 14.9995 1.69803 15.0418 1.70704 15.082C1.71605 15.1222 1.73351 15.1601 1.75782 15.1934C1.78213 15.2266 1.81309 15.2546 1.84864 15.2754H1.84766L10.2959 20.0986V2.4873L1.75196 14.8457Z"
|
||||
{fill}
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M18 10.8496C18.1472 10.8496 18.2858 10.9138 18.3857 11.0225C18.4851 11.1306 18.539 11.2749 18.5391 11.4229V11.9785H20.5928C20.7397 11.9786 20.8777 12.042 20.9775 12.1504C21.077 12.2586 21.1318 12.4037 21.1318 12.5518C21.1317 12.6997 21.0769 12.844 20.9775 12.9521C20.8777 13.0606 20.7397 13.124 20.5928 13.124H18.5391V15.9268H19.2959C19.9244 15.9268 20.5246 16.1988 20.9648 16.6777C21.4047 17.1563 21.6504 17.8029 21.6504 18.4746C21.6503 19.1463 21.4046 19.793 20.9648 20.2715C20.5246 20.7503 19.9243 21.0215 19.2959 21.0215H18.5391V21.5771C18.539 21.7251 18.4851 21.8694 18.3857 21.9775C18.2858 22.0862 18.1472 22.1504 18 22.1504C17.8528 22.1504 17.7142 22.0862 17.6143 21.9775C17.5149 21.8694 17.461 21.7251 17.4609 21.5771V21.0215H14.8887C14.7417 21.0214 14.6037 20.958 14.5039 20.8496C14.4044 20.7414 14.3496 20.5963 14.3496 20.4482C14.3497 20.3003 14.4045 20.156 14.5039 20.0479C14.6037 19.9394 14.7417 19.876 14.8887 19.876H17.4609V17.0732H16.7041C16.0756 17.0732 15.4754 16.8012 15.0352 16.3223C14.5953 15.8437 14.3496 15.1971 14.3496 14.5254C14.3497 13.8537 14.5954 13.207 15.0352 12.7285C15.4754 12.2497 16.0757 11.9785 16.7041 11.9785H17.4609V11.4229C17.461 11.2749 17.5149 11.1306 17.6143 11.0225C17.7142 10.9138 17.8528 10.8496 18 10.8496ZM18.5391 19.876H19.2959C19.63 19.876 19.9539 19.7313 20.1943 19.4697C20.4351 19.2078 20.5722 18.85 20.5723 18.4746C20.5723 18.0991 20.4352 17.7405 20.1943 17.4785C19.9539 17.217 19.63 17.0732 19.2959 17.0732H18.5391V19.876ZM16.7041 13.124C16.37 13.124 16.0461 13.2687 15.8057 13.5303C15.5649 13.7922 15.4278 14.15 15.4277 14.5254C15.4277 14.9009 15.5648 15.2595 15.8057 15.5215C16.0461 15.783 16.37 15.9268 16.7041 15.9268H17.4609V13.124H16.7041Z"
|
||||
{fill}
|
||||
stroke={fill}
|
||||
stroke-width="0.3"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_2">
|
||||
<rect width="22" height="22" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
@@ -15,8 +15,10 @@
|
||||
import WorkflowTimeline from '../WorkflowTimeline.svelte'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { isFlowPreview, isScriptPreview, truncateRev } from '$lib/utils'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, setContext, untrack } from 'svelte'
|
||||
import { ListFilter } from 'lucide-svelte'
|
||||
import FlowAssetsHandler, { initFlowGraphAssetsCtx } from '../flows/FlowAssetsHandler.svelte'
|
||||
import JobAssetsViewer from '../assets/JobAssetsViewer.svelte'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
@@ -46,6 +48,11 @@
|
||||
|
||||
let viewTab = $state('result')
|
||||
|
||||
setContext(
|
||||
'FlowGraphAssetContext',
|
||||
initFlowGraphAssetsCtx({ getModules: () => job?.raw_flow?.modules ?? [] })
|
||||
)
|
||||
|
||||
function asWorkflowStatus(x: any): Record<string, WorkflowStatus> {
|
||||
return x as Record<string, WorkflowStatus>
|
||||
}
|
||||
@@ -192,6 +199,7 @@
|
||||
<Tabs bind:selected={viewTab}>
|
||||
<Tab size="xs" value="result">Result</Tab>
|
||||
<Tab size="xs" value="logs">Logs</Tab>
|
||||
<Tab size="xs" value="assets">Assets</Tab>
|
||||
{#if isScriptPreview(job?.job_kind)}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{/if}
|
||||
@@ -205,6 +213,8 @@
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if viewTab == 'assets'}
|
||||
<JobAssetsViewer {job} />
|
||||
{:else}
|
||||
<div class="flex flex-col border rounded-md p-2 mt-2 h-full overflow-auto">
|
||||
{#if viewTab == 'logs'}
|
||||
@@ -263,3 +273,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<FlowAssetsHandler
|
||||
modules={job?.raw_flow?.modules ?? []}
|
||||
enableDbExplore
|
||||
enablePathScriptAndFlowAssets
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NewScript } from '$lib/gen'
|
||||
import type { AssetWithAccessType } from './assets/lib'
|
||||
import type { AssetWithAltAccessType } from './assets/lib'
|
||||
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import type { DiffDrawerI } from './diff_drawer'
|
||||
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
|
||||
@@ -9,7 +9,7 @@ import type { NewScriptWithDraftAndDraftTriggers, Trigger } from './triggers/uti
|
||||
export interface ScriptBuilderProps {
|
||||
script: NewScript & {
|
||||
draft_triggers?: Trigger[]
|
||||
fallback_access_types?: AssetWithAccessType[]
|
||||
assets?: AssetWithAltAccessType[]
|
||||
}
|
||||
disableAi?: boolean
|
||||
fullyLoaded?: boolean
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// https://github.com/sveltejs/svelte/issues/14600
|
||||
|
||||
import { untrack } from 'svelte'
|
||||
import type { StateStore } from './utils'
|
||||
|
||||
export function withProps<Component, Props>(component: Component, props: Props) {
|
||||
@@ -10,6 +11,11 @@ export function withProps<Component, Props>(component: Component, props: Props)
|
||||
return ret
|
||||
}
|
||||
|
||||
export function createState<T>(initialValue: T): T {
|
||||
let s = $state(initialValue)
|
||||
return s
|
||||
}
|
||||
|
||||
export function stateSnapshot<T>(state: T) {
|
||||
return $state.snapshot(state)
|
||||
}
|
||||
@@ -38,23 +44,25 @@ export function usePromise<T>(
|
||||
status: 'loading',
|
||||
__promise: undefined,
|
||||
refresh: () => {
|
||||
let promise = createPromise()
|
||||
ret.__promise = promise
|
||||
ret.status = 'loading'
|
||||
if (clearValueOnRefresh) ret.value = undefined
|
||||
ret.error = undefined
|
||||
untrack(() => {
|
||||
let promise = createPromise()
|
||||
ret.__promise = promise
|
||||
ret.status = 'loading'
|
||||
if (clearValueOnRefresh) ret.value = undefined
|
||||
ret.error = undefined
|
||||
|
||||
promise
|
||||
.then((value) => {
|
||||
if (ret.__promise !== promise) return
|
||||
ret.value = value
|
||||
ret.status = 'ok'
|
||||
})
|
||||
.catch((error) => {
|
||||
if (ret.__promise !== promise) return
|
||||
ret.error = error
|
||||
ret.status = 'error'
|
||||
})
|
||||
promise
|
||||
.then((value) => {
|
||||
if (ret.__promise !== promise) return
|
||||
ret.value = value
|
||||
ret.status = 'ok'
|
||||
})
|
||||
.catch((error) => {
|
||||
if (ret.__promise !== promise) return
|
||||
ret.error = error
|
||||
ret.status = 'error'
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
if (loadInit) ret.refresh()
|
||||
|
||||
@@ -1465,3 +1465,20 @@ export function isS3Uri(uri: string): uri is S3Uri {
|
||||
const match = uri.match(/^s3:\/\/([^/]*)\/(.*)$/)
|
||||
return !!match && match.length === 3
|
||||
}
|
||||
|
||||
export function uniqueBy<T>(array: T[], key: (t: T) => any): T[] {
|
||||
const seen = new Set()
|
||||
return array.filter((item) => {
|
||||
const value = key(item)
|
||||
if (seen.has(value)) {
|
||||
return false
|
||||
} else {
|
||||
seen.add(value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function pruneNullishArrayWithSet<T>(array: (T | null | undefined)[]): T[] {
|
||||
return array.filter((item): item is T => item !== null && item !== undefined)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
import { setContext } from 'svelte'
|
||||
import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte'
|
||||
import { Triggers } from '$lib/components/triggers/triggers.svelte'
|
||||
import FlowAssetsHandler, {
|
||||
initFlowGraphAssetsCtx
|
||||
} from '$lib/components/flows/FlowAssetsHandler.svelte'
|
||||
|
||||
let flow: Flow | undefined = $state()
|
||||
let can_write = false
|
||||
@@ -91,6 +94,11 @@
|
||||
triggersState
|
||||
})
|
||||
|
||||
setContext(
|
||||
'FlowGraphAssetContext',
|
||||
initFlowGraphAssetsCtx({ getModules: () => flow?.value.modules ?? [] })
|
||||
)
|
||||
|
||||
let previousPath: string | undefined = $state(undefined)
|
||||
|
||||
async function archiveFlow(): Promise<void> {
|
||||
@@ -648,3 +656,9 @@
|
||||
{/snippet}
|
||||
</DetailPageLayout>
|
||||
{/if}
|
||||
|
||||
<FlowAssetsHandler
|
||||
modules={flow?.value.modules ?? []}
|
||||
enableDbExplore
|
||||
enablePathScriptAndFlowAssets
|
||||
/>
|
||||
|
||||
@@ -90,8 +90,12 @@
|
||||
import CustomPopover from '$lib/components/CustomPopover.svelte'
|
||||
import { isWindmillTooBigObject } from '$lib/components/job_args'
|
||||
import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { setContext, untrack } from 'svelte'
|
||||
import WorkerHostname from '$lib/components/WorkerHostname.svelte'
|
||||
import FlowAssetsHandler, {
|
||||
initFlowGraphAssetsCtx
|
||||
} from '$lib/components/flows/FlowAssetsHandler.svelte'
|
||||
import JobAssetsViewer from '$lib/components/assets/JobAssetsViewer.svelte'
|
||||
|
||||
let job: Job | undefined = $state()
|
||||
let jobUpdateLastFetch: Date | undefined = $state()
|
||||
@@ -99,7 +103,7 @@
|
||||
let scriptProgress: number | undefined = $state(undefined)
|
||||
let currentJobIsLongRunning: boolean = $state(false)
|
||||
|
||||
let viewTab: 'result' | 'logs' | 'code' | 'stats' = $state('result')
|
||||
let viewTab: 'result' | 'logs' | 'code' | 'stats' | 'assets' = $state('result')
|
||||
let selectedJobStep: string | undefined = $state(undefined)
|
||||
let branchOrIterationN: number = $state(0)
|
||||
|
||||
@@ -118,6 +122,12 @@
|
||||
|
||||
let lastJobId: string | undefined = $state(undefined)
|
||||
let concurrencyKey: string | undefined = $state(undefined)
|
||||
|
||||
setContext(
|
||||
'FlowGraphAssetContext',
|
||||
initFlowGraphAssetsCtx({ getModules: () => job?.raw_flow?.modules ?? [] })
|
||||
)
|
||||
|
||||
async function getConcurrencyKey(job: Job | undefined) {
|
||||
if (!job) return
|
||||
lastJobId = job.id
|
||||
@@ -925,6 +935,7 @@
|
||||
<Tab value="result">Result</Tab>
|
||||
<Tab value="logs">Logs</Tab>
|
||||
<Tab value="stats">Metrics</Tab>
|
||||
<Tab value="assets">Assets</Tab>
|
||||
{#if isScriptPreview(job?.job_kind)}
|
||||
<Tab value="code">Code</Tab>
|
||||
{/if}
|
||||
@@ -944,6 +955,10 @@
|
||||
tag={job?.tag}
|
||||
/>
|
||||
</div>
|
||||
{:else if viewTab == 'assets'}
|
||||
<div class="w-full">
|
||||
<JobAssetsViewer {job} />
|
||||
</div>
|
||||
{:else if viewTab == 'code'}
|
||||
{#if job && 'raw_code' in job && job.raw_code}
|
||||
<div class="text-xs">
|
||||
@@ -1001,3 +1016,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<FlowAssetsHandler
|
||||
modules={job?.raw_flow?.modules ?? []}
|
||||
enableDbExplore
|
||||
enablePathScriptAndFlowAssets
|
||||
/>
|
||||
|
||||
@@ -270,7 +270,7 @@ components:
|
||||
type: string
|
||||
is_trigger:
|
||||
type: boolean
|
||||
asset_fallback_access_types:
|
||||
assets:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
@@ -287,10 +287,10 @@ components:
|
||||
- resource
|
||||
access_type:
|
||||
type: string
|
||||
enum:
|
||||
- r
|
||||
- w
|
||||
- rw
|
||||
enum: [r, w, rw]
|
||||
alt_access_type:
|
||||
type: string
|
||||
enum: [r, w, rw]
|
||||
required:
|
||||
- type
|
||||
- content
|
||||
|
||||
Reference in New Issue
Block a user