Compare commits
17 Commits
dieri/trig
...
workspace-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18940d5303 | ||
|
|
a23d4f015a | ||
|
|
69c550bca6 | ||
|
|
dc9d25289b | ||
|
|
c4dfe6798c | ||
|
|
0b71ace5ad | ||
|
|
15c9364d08 | ||
|
|
6dc53f385d | ||
|
|
67e30c7741 | ||
|
|
a2457dc9f2 | ||
|
|
7dbf05865d | ||
|
|
d92c0e96ba | ||
|
|
4544b6be54 | ||
|
|
1613e6957d | ||
|
|
a976d963a8 | ||
|
|
2179c78761 | ||
|
|
2eeb568966 |
55
backend/.sqlx/query-5387fbfd4be55674dcbe9f9b3ce9649d0f5db125776fbdffea993a53a09708de.json
generated
Normal file
55
backend/.sqlx/query-5387fbfd4be55674dcbe9f9b3ce9649d0f5db125776fbdffea993a53a09708de.json
generated
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id FROM workspace_dependencies\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"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",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5387fbfd4be55674dcbe9f9b3ce9649d0f5db125776fbdffea993a53a09708de"
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, content, language AS \"language: ScriptLang\", name, description, archived, workspace_id, created_at\n FROM workspace_dependencies\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "content",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "language: ScriptLang",
|
||||
"type_info": {
|
||||
"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",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "archived",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"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",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f0e7d28be69c4b922b34b76abb780766003d79884d33f94a79e61f0259807dbc"
|
||||
}
|
||||
@@ -64,7 +64,7 @@ mod workspace_dependencies {
|
||||
name: Some("test".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create("", "", "", db)
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -151,7 +151,7 @@ mod workspace_dependencies {
|
||||
name: None, // No name = default workspace dependencies
|
||||
description: None,
|
||||
}
|
||||
.create("", "", "", db)
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -13,9 +13,8 @@ use windmill_common::{
|
||||
workspace_dependencies::WorkspaceDependencies,
|
||||
DB,
|
||||
};
|
||||
use windmill_worker::{
|
||||
scoped_dependency_map, trigger_dependents_to_recompute_dependencies,
|
||||
workspace_dependencies::NewWorkspaceDependencies,
|
||||
use windmill_worker::workspace_dependencies::{
|
||||
trigger_dependents_to_recompute_dependencies_in_the_background, NewWorkspaceDependencies,
|
||||
};
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
@@ -43,10 +42,12 @@ async fn create(
|
||||
format!(
|
||||
"{}",
|
||||
nwd.create(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&username_to_permissioned_as(&authed.username),
|
||||
&db
|
||||
(
|
||||
authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
authed.username,
|
||||
),
|
||||
db
|
||||
)
|
||||
.await?
|
||||
),
|
||||
@@ -93,23 +94,20 @@ async fn archive(
|
||||
let db = &db;
|
||||
WorkspaceDependencies::archive(params.name.clone(), language, &w_id, db).await?;
|
||||
|
||||
trigger_dependents_to_recompute_dependencies(
|
||||
&w_id,
|
||||
scoped_dependency_map::ScopedDependencyMap::get_dependents(
|
||||
WorkspaceDependencies::to_path(¶ms.name, language)?.as_str(),
|
||||
&w_id,
|
||||
db,
|
||||
)
|
||||
.await?,
|
||||
None,
|
||||
None,
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&username_to_permissioned_as(&authed.username),
|
||||
db,
|
||||
vec![],
|
||||
trigger_dependents_to_recompute_dependencies_in_the_background(
|
||||
params.name.is_none(),
|
||||
w_id,
|
||||
language,
|
||||
(
|
||||
authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
authed.username,
|
||||
),
|
||||
WorkspaceDependencies::to_path(¶ms.name, language)?,
|
||||
db.clone(),
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
@@ -125,21 +123,18 @@ async fn delete(
|
||||
let db = &db;
|
||||
WorkspaceDependencies::delete(params.name.clone(), language, &w_id, db).await?;
|
||||
|
||||
trigger_dependents_to_recompute_dependencies(
|
||||
&w_id,
|
||||
scoped_dependency_map::ScopedDependencyMap::get_dependents(
|
||||
WorkspaceDependencies::to_path(¶ms.name, language)?.as_str(),
|
||||
&w_id,
|
||||
db,
|
||||
)
|
||||
.await?,
|
||||
None,
|
||||
None,
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&username_to_permissioned_as(&authed.username),
|
||||
db,
|
||||
vec![],
|
||||
trigger_dependents_to_recompute_dependencies_in_the_background(
|
||||
params.name.is_none(),
|
||||
w_id,
|
||||
language,
|
||||
(
|
||||
authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
authed.username,
|
||||
),
|
||||
WorkspaceDependencies::to_path(¶ms.name, language)?,
|
||||
db.clone(),
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -892,6 +892,86 @@ pub mod job {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod workspace_dependencies {
|
||||
use std::{
|
||||
future::Future,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{error, scripts::ScriptLang, workspace_dependencies::WorkspaceDependencies, DB};
|
||||
|
||||
make_static! {
|
||||
/// Workspace Dependencies by id and workspace cache.
|
||||
static ref WORKSPACE_DEPENDENCIES: { (i64, String) => WorkspaceDependencies } in "workspace_dependencies" <= 1000;
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
/// Cache for checking if default/unnamed workspace dependencies exist for a workspace and language.
|
||||
/// Cache key: (workspace_id, language)
|
||||
/// Cache value: (exists: bool, cached_at timestamp)
|
||||
static ref DEFAULT_WD_EXISTS_CACHE: quick_cache::sync::Cache<(String, ScriptLang), (bool, Instant)> = quick_cache::sync::Cache::new(500);
|
||||
}
|
||||
/// Cache timeout for existence checks (10 seconds)
|
||||
pub const EXISTS_CACHE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub fn fetch_workspace_dependencies<'c>(
|
||||
id: i64,
|
||||
workspace_id: String,
|
||||
db: &'c DB,
|
||||
) -> impl Future<Output = error::Result<WorkspaceDependencies>> + 'c {
|
||||
tracing::debug!(workspace_id = %workspace_id, id, "fetching workspace dependencies");
|
||||
WORKSPACE_DEPENDENCIES.get_or_insert_async(
|
||||
(id, workspace_id.clone()),
|
||||
WorkspaceDependencies::get(id, workspace_id, db),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_cached_is_unnamed_workspace_dependencies_exists<'c>(
|
||||
language: ScriptLang,
|
||||
workspace_id: String,
|
||||
) -> Option<bool> {
|
||||
let exists_key = (workspace_id.to_string(), language);
|
||||
if let Some((exists, cached_at)) = DEFAULT_WD_EXISTS_CACHE.get(&exists_key) {
|
||||
if cached_at.elapsed() < EXISTS_CACHE_TIMEOUT {
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
exists,
|
||||
"cache hit for unnamed workspace dependencies existence"
|
||||
);
|
||||
return Some(exists);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
"cache expired for unnamed workspace dependencies existence"
|
||||
);
|
||||
DEFAULT_WD_EXISTS_CACHE.remove(&exists_key);
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
"cache miss for unnamed workspace dependencies existence"
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn set_cached_is_unnamed_workspace_dependencies_exists<'c>(
|
||||
language: ScriptLang,
|
||||
workspace_id: String,
|
||||
exists: bool,
|
||||
) {
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
exists,
|
||||
"setting cache for unnamed workspace dependencies existence"
|
||||
);
|
||||
let exists_key = (workspace_id.to_string(), language);
|
||||
DEFAULT_WD_EXISTS_CACHE.insert(exists_key, (exists, Instant::now()));
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = {
|
||||
impl Import for RawFlow {
|
||||
fn import(src: &impl Storage) -> error::Result<Self> {
|
||||
@@ -1075,7 +1155,8 @@ const _: () = {
|
||||
(ScriptHash, |x| format!("{:016x}", x.0)),
|
||||
((u8, ScriptHash), |x| format!("{:02x}-{:016x}", x.0, x.1.0)),
|
||||
(FlowNodeId, |x| format!("{:016x}", x.0)),
|
||||
(AppScriptId, |x| format!("{:016x}", x.0))
|
||||
(AppScriptId, |x| format!("{:016x}", x.0)),
|
||||
((i64, String), |x| format!("{}-{}", x.1, x.0))
|
||||
}
|
||||
|
||||
#[cfg(feature = "scoped_cache")]
|
||||
|
||||
@@ -2,9 +2,17 @@ use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgExecutor;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::{error, scripts::ScriptLang, utils::calculate_hash, worker::Connection};
|
||||
use crate::{
|
||||
cache::workspace_dependencies::{
|
||||
fetch_workspace_dependencies, get_cached_is_unnamed_workspace_dependencies_exists,
|
||||
set_cached_is_unnamed_workspace_dependencies_exists,
|
||||
},
|
||||
error,
|
||||
scripts::ScriptLang,
|
||||
utils::calculate_hash,
|
||||
worker::Connection,
|
||||
};
|
||||
use phf::phf_set;
|
||||
|
||||
pub static BLACKLIST: phf::Set<&'static str> = phf_set! {
|
||||
@@ -15,16 +23,8 @@ pub static BLACKLIST: phf::Set<&'static str> = phf_set! {
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: bool = std::env::var("WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES").is_ok();
|
||||
|
||||
/// Simple in-memory cache for workspace dependencies get_latest with 10-second timeout.
|
||||
/// Cache key: (workspace_id, language, name)
|
||||
/// Cache value: (Option<WorkspaceDependencies>, cached_at timestamp)
|
||||
static ref WORKSPACE_DEPENDENCIES_CACHE: quick_cache::sync::Cache<(String, ScriptLang, Option<String>), (Option<WorkspaceDependencies>, Instant)> = quick_cache::sync::Cache::new(1000);
|
||||
}
|
||||
|
||||
/// Cache timeout for workspace dependencies
|
||||
const CACHE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Minimum Windmill version required for workspace dependencies feature
|
||||
pub const MIN_VERSION_WORKSPACE_DEPENDENCIES: &str = "1.587.0";
|
||||
|
||||
@@ -174,6 +174,42 @@ impl WorkspaceDependencies {
|
||||
.map_err(error::Error::from)
|
||||
}
|
||||
|
||||
async fn get_latest_id<'c>(
|
||||
name: Option<String>,
|
||||
language: ScriptLang,
|
||||
workspace_id: &str,
|
||||
e: impl PgExecutor<'c>,
|
||||
) -> error::Result<Option<i64>> {
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
?name,
|
||||
"fetching latest workspace dependencies id"
|
||||
);
|
||||
let result = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT id FROM workspace_dependencies
|
||||
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3
|
||||
LIMIT 1
|
||||
"#,
|
||||
name,
|
||||
workspace_id,
|
||||
language as ScriptLang
|
||||
)
|
||||
.fetch_optional(e)
|
||||
.await
|
||||
.map_err(error::Error::from)?;
|
||||
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
?name,
|
||||
?result,
|
||||
"fetched latest workspace dependencies id"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Gets the latest version of workspace dependencies by name and language.
|
||||
pub async fn get_latest(
|
||||
name: Option<String>,
|
||||
@@ -185,66 +221,84 @@ impl WorkspaceDependencies {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let cache_key = (workspace_id.to_string(), language, name.clone());
|
||||
|
||||
// Check if cached value is still valid
|
||||
if let Some((cached_value, cached_at)) = WORKSPACE_DEPENDENCIES_CACHE.get(&cache_key) {
|
||||
if cached_at.elapsed() < CACHE_TIMEOUT {
|
||||
return Ok(cached_value);
|
||||
}
|
||||
// Expired, remove it
|
||||
WORKSPACE_DEPENDENCIES_CACHE.remove(&cache_key);
|
||||
if name.is_none()
|
||||
&& get_cached_is_unnamed_workspace_dependencies_exists(
|
||||
language,
|
||||
workspace_id.to_owned(),
|
||||
)
|
||||
.map(|exists| exists == false)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
"skipping unnamed workspace dependencies fetch - cached as non-existent"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Fetch and cache
|
||||
let fetch = Box::pin(async {
|
||||
match &conn {
|
||||
Connection::Sql(db) => sqlx::query_as!(
|
||||
Self,
|
||||
r#"
|
||||
SELECT id, content, language AS "language: ScriptLang", name, description, archived, workspace_id, created_at
|
||||
FROM workspace_dependencies
|
||||
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3
|
||||
LIMIT 1
|
||||
"#,
|
||||
name,
|
||||
workspace_id,
|
||||
language as ScriptLang
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.map_err(error::Error::from),
|
||||
// Fetch from database or HTTP
|
||||
let wd = match &conn {
|
||||
Connection::Sql(db) => {
|
||||
let Some(id) =
|
||||
Self::get_latest_id(name.clone(), language, workspace_id, db).await?
|
||||
else {
|
||||
if name.is_none() {
|
||||
set_cached_is_unnamed_workspace_dependencies_exists(
|
||||
language,
|
||||
workspace_id.to_owned(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
Connection::Http(http_client) => http_client
|
||||
.get::<Option<WorkspaceDependencies>>(&format!(
|
||||
"/api/w/{workspace_id}/agent_workers/workspace_dependencies/get_latest/{}{}",
|
||||
language.as_str(),
|
||||
if let Some(ref name_val) = name {
|
||||
format!("?name={name_val}")
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
))
|
||||
.await
|
||||
.map_err(error::Error::from),
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
?name,
|
||||
"no latest workspace dependencies found"
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
?language,
|
||||
?name,
|
||||
id,
|
||||
"fetching workspace dependencies by id from cache or db"
|
||||
);
|
||||
Some(fetch_workspace_dependencies(id, workspace_id.to_owned(), db).await?)
|
||||
}
|
||||
});
|
||||
|
||||
let (workspace_dependencies_o, ..) = WORKSPACE_DEPENDENCIES_CACHE
|
||||
.get_or_insert_async(&cache_key, async {
|
||||
Ok::<_, error::Error>((fetch.await?, Instant::now()))
|
||||
})
|
||||
.await?;
|
||||
Connection::Http(http_client) => http_client
|
||||
.get::<Option<WorkspaceDependencies>>(&format!(
|
||||
"/api/w/{workspace_id}/agent_workers/workspace_dependencies/get_latest/{}{}",
|
||||
language.as_str(),
|
||||
if let Some(ref name_val) = name {
|
||||
format!("?name={name_val}")
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
))
|
||||
.await
|
||||
.map_err(error::Error::from)?,
|
||||
};
|
||||
|
||||
Ok(workspace_dependencies_o)
|
||||
if name.is_none() {
|
||||
set_cached_is_unnamed_workspace_dependencies_exists(
|
||||
language,
|
||||
workspace_id.to_owned(),
|
||||
wd.is_some(),
|
||||
);
|
||||
}
|
||||
Ok(wd)
|
||||
}
|
||||
|
||||
/// Gets workspace dependencies by their unique ID.
|
||||
pub async fn get<'c>(
|
||||
id: i64,
|
||||
workspace_id: &str,
|
||||
workspace_id: String,
|
||||
e: impl PgExecutor<'c>,
|
||||
) -> error::Result<Option<Self>> {
|
||||
) -> error::Result<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
r#"
|
||||
@@ -254,9 +308,9 @@ impl WorkspaceDependencies {
|
||||
LIMIT 1
|
||||
"#,
|
||||
id,
|
||||
workspace_id
|
||||
&workspace_id
|
||||
)
|
||||
.fetch_optional(e)
|
||||
.fetch_one(e)
|
||||
.await
|
||||
.map_err(error::Error::from)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use windmill_common::{error, scripts::ScriptLang, workspace_dependencies::WorkspaceDependencies};
|
||||
use windmill_common::{
|
||||
cache::workspace_dependencies::EXISTS_CACHE_TIMEOUT, error, scripts::ScriptLang,
|
||||
workspace_dependencies::WorkspaceDependencies,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
scoped_dependency_map::ScopedDependencyMap, trigger_dependents_to_recompute_dependencies,
|
||||
@@ -25,10 +28,8 @@ impl NewWorkspaceDependencies {
|
||||
/// and rebuilds the dependency map if this is the first unnamed dependency for the workspace.
|
||||
pub async fn create<'c>(
|
||||
self,
|
||||
email: &str,
|
||||
created_by: &str,
|
||||
permissioned_as: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
metadata: (String, String, String),
|
||||
db: sqlx::Pool<sqlx::Postgres>,
|
||||
) -> error::Result<i64> {
|
||||
// Check if all workers support workspace dependencies feature
|
||||
windmill_common::workspace_dependencies::min_version_supports_v0_workspace_dependencies()
|
||||
@@ -43,7 +44,7 @@ impl NewWorkspaceDependencies {
|
||||
let setting_name = format!("workspace_dependencies_map_rebuilt:{}", self.workspace_id);
|
||||
let already_rebuilt =
|
||||
windmill_common::global_settings::load_value_from_global_settings(
|
||||
db,
|
||||
&db,
|
||||
&setting_name,
|
||||
)
|
||||
.await?
|
||||
@@ -54,11 +55,11 @@ impl NewWorkspaceDependencies {
|
||||
workspace_id = %self.workspace_id,
|
||||
"Rebuilding workspace dependencies map for first unnamed workspace dependencies"
|
||||
);
|
||||
ScopedDependencyMap::rebuild_map_unchecked(&self.workspace_id, db).await?;
|
||||
ScopedDependencyMap::rebuild_map_unchecked(&self.workspace_id, &db).await?;
|
||||
|
||||
// Mark as rebuilt by creating the setting
|
||||
windmill_common::global_settings::set_value_in_global_settings(
|
||||
db,
|
||||
&db,
|
||||
&setting_name,
|
||||
serde_json::json!({}),
|
||||
)
|
||||
@@ -111,50 +112,83 @@ impl NewWorkspaceDependencies {
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
// Make sure trigger dependents will have latest view.
|
||||
// NOTE: Uncomment for tests
|
||||
// #[cfg(test)]
|
||||
// assert_eq!(
|
||||
// sqlx::query_scalar!(
|
||||
// "
|
||||
// SELECT id FROM workspace_dependencies
|
||||
// WHERE archived = false
|
||||
// AND name IS NOT DISTINCT FROM $1
|
||||
// AND workspace_id = $2
|
||||
// AND language = $3
|
||||
// ",
|
||||
// self.name,
|
||||
// self.workspace_id,
|
||||
// self.language as ScriptLang,
|
||||
// )
|
||||
// .fetch_one(db) // Use db
|
||||
// .await?,
|
||||
// new_id
|
||||
// );
|
||||
|
||||
// It's ok to fail, it will return an error and user will get notified that they should redeploy workspace dependencies
|
||||
trigger_dependents_to_recompute_dependencies(
|
||||
&self.workspace_id,
|
||||
crate::scoped_dependency_map::ScopedDependencyMap::get_dependents(
|
||||
path.as_str(),
|
||||
&self.workspace_id,
|
||||
db,
|
||||
)
|
||||
.await?,
|
||||
None,
|
||||
None,
|
||||
email,
|
||||
created_by,
|
||||
permissioned_as,
|
||||
trigger_dependents_to_recompute_dependencies_in_the_background(
|
||||
prev_description.is_none() && self.name.is_none(),
|
||||
self.workspace_id,
|
||||
self.language,
|
||||
metadata,
|
||||
path,
|
||||
db,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
Ok(new_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn trigger_dependents_to_recompute_dependencies_in_the_background(
|
||||
wait_for_cache_timeout: bool,
|
||||
workspace_id: String,
|
||||
language: ScriptLang,
|
||||
(email, permissioned_as, created_by): (String, String, String),
|
||||
path: String,
|
||||
db: sqlx::Pool<sqlx::Postgres>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
if wait_for_cache_timeout {
|
||||
tracing::debug!(
|
||||
workspace_id = %workspace_id,
|
||||
language = ?language,
|
||||
"waiting for cache timeout after creating first unnamed workspace dependencies"
|
||||
);
|
||||
// Wait for cache timeout.
|
||||
// For context, workers have cache on whether the unnamed workspace dependencies exists or not.
|
||||
// when we trigger dependents to recompoute dependencies we want to make sure all workers are having cache timed out.
|
||||
// otherwise it would result into bug, when workers skip fetch of workspace dependencies because they think they don't exist.
|
||||
tokio::time::sleep(EXISTS_CACHE_TIMEOUT).await;
|
||||
}
|
||||
|
||||
// It's ok to fail, it will return an error and user will get notified that they should redeploy workspace dependencies
|
||||
if let Err(e) = trigger_dependents_to_recompute_dependencies(
|
||||
&workspace_id,
|
||||
match crate::scoped_dependency_map::ScopedDependencyMap::get_dependents(
|
||||
path.as_str(),
|
||||
&workspace_id,
|
||||
&db,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(importers) => importers,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
workspace_id = %workspace_id,
|
||||
path = %path,
|
||||
error = %e,
|
||||
"CRITICAL: failed to get dependents for workspace dependencies - dependent runnables are not being redeployed. Please contact the Windmill team"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
None,
|
||||
None,
|
||||
email.as_str(),
|
||||
created_by.as_str(),
|
||||
permissioned_as.as_str(),
|
||||
&db,
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
workspace_id = %workspace_id,
|
||||
path = %path,
|
||||
error = %e,
|
||||
"CRITICAL: failed to trigger dependents to recompute dependencies - dependent runnables are not being redeployed. Please contact the Windmill team"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Type aliases for backward compatibility
|
||||
pub type RawRequirements = WorkspaceDependencies;
|
||||
pub type NewRawRequirements = NewWorkspaceDependencies;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ListableApp, Policy } from "../../../gen/types.gen.ts";
|
||||
import { GlobalOptions, isSuperset } from "../../types.ts";
|
||||
import { readInlinePathSync } from "../../utils/utils.ts";
|
||||
import devCommand from "./dev.ts";
|
||||
import { isVersionsGeq15851 } from "../sync/global.ts";
|
||||
|
||||
export interface AppFile {
|
||||
value: any;
|
||||
@@ -30,22 +31,26 @@ function respecializeFields(fields: Record<string, any>) {
|
||||
Object.entries(fields).forEach(([k, v]) => {
|
||||
if (typeof v == "object") {
|
||||
if (v.value !== undefined) {
|
||||
fields[k] = { value: v.value, type: "static" }
|
||||
fields[k] = { value: v.value, type: "static" };
|
||||
} else if (v.expr !== undefined) {
|
||||
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources, type: "javascript" }
|
||||
fields[k] = {
|
||||
expr: v.expr,
|
||||
allowUserResources: v.allowUserResources,
|
||||
type: "javascript",
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function repopulateFields(runnables: Record<string, any>) {
|
||||
Object.values(runnables).forEach((v) => {
|
||||
if (typeof v == "object") {
|
||||
if (v.fields !== undefined) {
|
||||
respecializeFields(v.fields)
|
||||
respecializeFields(v.fields);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
export function replaceInlineScripts(rec: any, localPath: string) {
|
||||
if (!rec) {
|
||||
@@ -53,11 +58,14 @@ export function replaceInlineScripts(rec: any, localPath: string) {
|
||||
}
|
||||
if (typeof rec == "object") {
|
||||
return Object.entries(rec).flatMap(([k, v]) => {
|
||||
if (k == 'runType') {
|
||||
rec["type"] = 'path'
|
||||
|
||||
if (k == "runType") {
|
||||
if (isVersionsGeq15851()) {
|
||||
rec["type"] = "path";
|
||||
}
|
||||
} else if (k == "inlineScript" && typeof v == "object") {
|
||||
rec["type"] = 'inline'
|
||||
if (isVersionsGeq15851()) {
|
||||
rec["type"] = "inline";
|
||||
}
|
||||
const o: Record<string, any> = v as any;
|
||||
|
||||
if (o["content"] && o["content"].startsWith("!inline")) {
|
||||
@@ -114,7 +122,12 @@ export async function pushApp(
|
||||
const localApp = (await yamlParseFile(path)) as AppFile;
|
||||
|
||||
replaceInlineScripts(localApp.value, localPath);
|
||||
await generatingPolicy(localApp, remotePath, localApp?.["public"] ?? false);
|
||||
await generatingPolicy(
|
||||
localApp,
|
||||
remotePath,
|
||||
localApp?.["public"] ??
|
||||
localApp?.["policy"]?.["execution_mode"] == "anonymous"
|
||||
);
|
||||
if (app) {
|
||||
if (isSuperset(localApp, app)) {
|
||||
log.info(colors.green(`App ${remotePath} is up to date`));
|
||||
|
||||
31
cli/src/commands/sync/global.ts
Normal file
31
cli/src/commands/sync/global.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { colors, log } from "../../../deps.ts";
|
||||
|
||||
let GLOBAL_VERSIONS: {
|
||||
remoteMajor: number | undefined;
|
||||
remoteMinor: number | undefined;
|
||||
} = {
|
||||
remoteMajor: undefined,
|
||||
remoteMinor: undefined,
|
||||
};
|
||||
|
||||
export function updateGlobalVersions(version: string) {
|
||||
try {
|
||||
const [prefix, remoteMinorStr] = version.split(".");
|
||||
|
||||
GLOBAL_VERSIONS = {
|
||||
remoteMajor: parseInt(prefix.split("v")[1]),
|
||||
remoteMinor: parseInt(remoteMinorStr),
|
||||
};
|
||||
} catch (e) {
|
||||
log.info(colors.gray(`Error reading remote version: ${e}`));
|
||||
}
|
||||
}
|
||||
|
||||
export function isVersionsGeq15851(): boolean {
|
||||
return (
|
||||
GLOBAL_VERSIONS.remoteMajor !== undefined &&
|
||||
GLOBAL_VERSIONS.remoteMajor >= 1 &&
|
||||
GLOBAL_VERSIONS.remoteMinor !== undefined &&
|
||||
GLOBAL_VERSIONS.remoteMinor >= 5851
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,12 @@ import {
|
||||
} from "../script/script.ts";
|
||||
|
||||
import { handleFile } from "../script/script.ts";
|
||||
import { deepEqual, isFileResource, isRawAppFile, isWorkspaceDependencies } from "../../utils/utils.ts";
|
||||
import {
|
||||
deepEqual,
|
||||
isFileResource,
|
||||
isRawAppFile,
|
||||
isWorkspaceDependencies,
|
||||
} from "../../utils/utils.ts";
|
||||
import {
|
||||
getEffectiveSettings,
|
||||
mergeConfigWithConfigFile,
|
||||
@@ -70,6 +75,7 @@ import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../w
|
||||
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
|
||||
import { isExecutionModeAnonymous } from "../app/apps.ts";
|
||||
import { generateAppLocksInternal } from "../app/app_metadata.ts";
|
||||
import { updateGlobalVersions } from "./global.ts";
|
||||
|
||||
// Merge CLI options with effective settings, preserving CLI flags as overrides
|
||||
function mergeCliWithEffectiveOptions<
|
||||
@@ -268,29 +274,28 @@ function extractFields(fields: Record<string, any>) {
|
||||
Object.entries(fields).forEach(([k, v]) => {
|
||||
if (typeof v == "object") {
|
||||
if (v.type == "static") {
|
||||
fields[k] = { value: v.value }
|
||||
fields[k] = { value: v.value };
|
||||
} else if (v.type == "javascript") {
|
||||
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources }
|
||||
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources };
|
||||
} else if (v.type == "user") {
|
||||
fields[k] = undefined
|
||||
fields[k] = undefined;
|
||||
}
|
||||
}
|
||||
// if (k == 'runType') {
|
||||
// fields["type"] = undefined
|
||||
// fields["schema"] = undefined
|
||||
// }
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function extractFieldsForRawApps(runnables: Record<string, any>) {
|
||||
Object.values(runnables).forEach((v) => {
|
||||
if (typeof v == "object") {
|
||||
if (v.fields !== undefined) {
|
||||
extractFields(v.fields)
|
||||
extractFields(v.fields);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
export function extractInlineScriptsForApps(
|
||||
key: string | undefined,
|
||||
@@ -303,12 +308,12 @@ export function extractInlineScriptsForApps(
|
||||
}
|
||||
if (typeof rec == "object") {
|
||||
return Object.entries(rec).flatMap(([k, v]) => {
|
||||
if (k == 'runType') {
|
||||
rec["type"] = undefined
|
||||
rec["schema"] = undefined
|
||||
return []
|
||||
if (k == "runType") {
|
||||
rec["type"] = undefined;
|
||||
rec["schema"] = undefined;
|
||||
return [];
|
||||
} else if (k == "inlineScript" && typeof v == "object") {
|
||||
rec["type"] = undefined
|
||||
rec["type"] = undefined;
|
||||
const o: Record<string, any> = v as any;
|
||||
const name = toId(key ?? "", rec);
|
||||
const [basePathO, ext] = pathAssigner.assignPath(name, o["language"]);
|
||||
@@ -351,20 +356,26 @@ function ZipFSElement(
|
||||
p: string,
|
||||
f: JSZip.JSZipObject
|
||||
): Promise<DynFSElement[]> {
|
||||
const kind: "flow" | "app" | "script" | "resource" | "other" | "raw_app" | "dependencies" =
|
||||
p.endsWith(".flow.json")
|
||||
? "flow"
|
||||
: p.endsWith(".app.json")
|
||||
? "app"
|
||||
: p.endsWith(".raw_app.json")
|
||||
? "raw_app"
|
||||
: p.endsWith(".script.json")
|
||||
? "script"
|
||||
: p.endsWith(".resource.json")
|
||||
? "resource"
|
||||
: p.startsWith("dependencies/")
|
||||
const kind:
|
||||
| "flow"
|
||||
| "app"
|
||||
| "script"
|
||||
| "resource"
|
||||
| "other"
|
||||
| "raw_app"
|
||||
| "dependencies" = p.endsWith(".flow.json")
|
||||
? "flow"
|
||||
: p.endsWith(".app.json")
|
||||
? "app"
|
||||
: p.endsWith(".raw_app.json")
|
||||
? "raw_app"
|
||||
: p.endsWith(".script.json")
|
||||
? "script"
|
||||
: p.endsWith(".resource.json")
|
||||
? "resource"
|
||||
: p.startsWith("dependencies/")
|
||||
? "dependencies"
|
||||
: "other";
|
||||
: "other";
|
||||
|
||||
const isJson = p.endsWith(".json");
|
||||
|
||||
@@ -502,7 +513,7 @@ function ZipFSElement(
|
||||
undefined,
|
||||
value,
|
||||
newPathAssigner(defaultTs),
|
||||
(key, val_) => key
|
||||
(key, val_) => key
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
@@ -759,7 +770,11 @@ export async function* readDirRecursiveWithIgnore(
|
||||
for await (const e2 of e.c()) {
|
||||
if (e2.isDirectory) {
|
||||
const dirName = e2.path.split(SEP).pop();
|
||||
if (dirName == "node_modules" || dirName == ".claude" || dirName?.startsWith(".")) {
|
||||
if (
|
||||
dirName == "node_modules" ||
|
||||
dirName == ".claude" ||
|
||||
dirName?.startsWith(".")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -792,7 +807,7 @@ export async function elementsToMap(
|
||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
||||
json: boolean,
|
||||
skips: Skips,
|
||||
specificItems?: SpecificItemsConfig,
|
||||
specificItems?: SpecificItemsConfig
|
||||
): Promise<{ [key: string]: string }> {
|
||||
const map: { [key: string]: string } = {};
|
||||
const processedBasePaths = new Set<string>();
|
||||
@@ -802,7 +817,11 @@ export async function elementsToMap(
|
||||
continue;
|
||||
}
|
||||
const path = entry.path;
|
||||
if (!isFileResource(path) && !isRawAppFile(path) && !isWorkspaceDependencies(path)) {
|
||||
if (
|
||||
!isFileResource(path) &&
|
||||
!isRawAppFile(path) &&
|
||||
!isWorkspaceDependencies(path)
|
||||
) {
|
||||
if (json && path.endsWith(".yaml")) continue;
|
||||
if (!json && path.endsWith(".json")) continue;
|
||||
|
||||
@@ -835,7 +854,11 @@ export async function elementsToMap(
|
||||
|
||||
if (isRawAppFile(path)) {
|
||||
const suffix = path.split(".raw_app" + SEP).pop();
|
||||
if (suffix?.startsWith("dist/") || suffix == "wmill.d.ts" || suffix == "package-lock.json") {
|
||||
if (
|
||||
suffix?.startsWith("dist/") ||
|
||||
suffix == "wmill.d.ts" ||
|
||||
suffix == "package-lock.json"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -867,7 +890,6 @@ export async function elementsToMap(
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Use getTypeStrFromPath for consistent type detection
|
||||
try {
|
||||
const fileType = getTypeStrFromPath(path);
|
||||
@@ -876,12 +898,15 @@ export async function elementsToMap(
|
||||
if (skips.skipFlows && fileType === "flow") continue;
|
||||
if (skips.skipApps && fileType === "app") continue;
|
||||
if (skips.skipFolders && fileType === "folder") continue;
|
||||
if (skips.skipWorkspaceDependencies && fileType === "workspace_dependencies") continue;
|
||||
if (
|
||||
skips.skipWorkspaceDependencies &&
|
||||
fileType === "workspace_dependencies"
|
||||
)
|
||||
continue;
|
||||
} catch {
|
||||
// If getTypeStrFromPath can't determine the type, continue processing the file
|
||||
}
|
||||
|
||||
|
||||
// Handle branch-specific files - skip files for other branches
|
||||
if (specificItems && isBranchSpecificFile(path)) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
@@ -945,7 +970,7 @@ export async function elementsToMap(
|
||||
// No specific items configuration, use regular path
|
||||
map[entry.path] = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1090,7 +1115,6 @@ async function compareDynFSElement(
|
||||
|
||||
const remoteCodebase: Record<string, string> = {};
|
||||
for (const [k] of Object.entries(m2)) {
|
||||
|
||||
if (m1[k] === undefined) {
|
||||
if (
|
||||
!ignoreMetadataDeletion ||
|
||||
@@ -1239,7 +1263,7 @@ export const isWhitelisted = (p: string) => {
|
||||
p == "f" ||
|
||||
p == "g" ||
|
||||
p == "users" ||
|
||||
p == "groups" ||
|
||||
p == "groups" ||
|
||||
p == "dependencies"
|
||||
);
|
||||
};
|
||||
@@ -1308,7 +1332,10 @@ export async function ignoreF(wmillconf: {
|
||||
if (wmillconf.includeKey && fileType === "encryption_key") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
if (!wmillconf.skipWorkspaceDependencies && fileType === "workspace_dependencies") {
|
||||
if (
|
||||
!wmillconf.skipWorkspaceDependencies &&
|
||||
fileType === "workspace_dependencies"
|
||||
) {
|
||||
return false; // Don't ignore workspace dependencies (they are always included unless explicitly skipped)
|
||||
}
|
||||
} catch {
|
||||
@@ -1440,22 +1467,22 @@ export async function pull(
|
||||
// ignore
|
||||
}
|
||||
const zipFile = await downloadZip(
|
||||
workspace,
|
||||
opts.plainSecrets,
|
||||
opts.skipVariables,
|
||||
opts.skipResources,
|
||||
opts.skipResourceTypes,
|
||||
opts.skipSecrets,
|
||||
opts.includeSchedules,
|
||||
opts.includeTriggers,
|
||||
opts.includeUsers,
|
||||
opts.includeGroups,
|
||||
opts.includeSettings,
|
||||
opts.includeKey,
|
||||
opts.skipWorkspaceDependencies,
|
||||
opts.defaultTs
|
||||
);
|
||||
|
||||
workspace,
|
||||
opts.plainSecrets,
|
||||
opts.skipVariables,
|
||||
opts.skipResources,
|
||||
opts.skipResourceTypes,
|
||||
opts.skipSecrets,
|
||||
opts.includeSchedules,
|
||||
opts.includeTriggers,
|
||||
opts.includeUsers,
|
||||
opts.includeGroups,
|
||||
opts.includeSettings,
|
||||
opts.includeKey,
|
||||
opts.skipWorkspaceDependencies,
|
||||
opts.defaultTs
|
||||
);
|
||||
|
||||
const remote = ZipFSElement(
|
||||
zipFile!,
|
||||
!opts.json,
|
||||
@@ -1482,7 +1509,6 @@ export async function pull(
|
||||
`remote (${workspace.name}) -> local: ${changes.length} changes to apply`
|
||||
);
|
||||
|
||||
|
||||
// Handle JSON output for dry-run
|
||||
if (opts.dryRun && opts.jsonOutput) {
|
||||
const result = {
|
||||
@@ -1671,7 +1697,8 @@ export async function pull(
|
||||
await readLockfile(); // ensure wmill-lock.yaml exists
|
||||
|
||||
const tracker: ChangeTracker = await buildTracker(changes);
|
||||
const rawWorkspaceDependencies: Record<string, string> = await getRawWorkspaceDependencies();
|
||||
const rawWorkspaceDependencies: Record<string, string> =
|
||||
await getRawWorkspaceDependencies();
|
||||
|
||||
for (const change of tracker.scripts) {
|
||||
await generateScriptMetadataInternal(
|
||||
@@ -1687,7 +1714,14 @@ export async function pull(
|
||||
}
|
||||
for (const change of tracker.flows) {
|
||||
log.info(`Updating lock for flow ${change}`);
|
||||
await generateFlowLockInternal(change, false, workspace, opts, true, false);
|
||||
await generateFlowLockInternal(
|
||||
change,
|
||||
false,
|
||||
workspace,
|
||||
opts,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
if (tracker.apps.length > 0) {
|
||||
log.info(
|
||||
@@ -1700,7 +1734,14 @@ export async function pull(
|
||||
}
|
||||
for (const change of tracker.rawApps) {
|
||||
log.info(`Updating lock metadata for raw app ${change}`);
|
||||
await generateAppLocksInternal(change, false, workspace, opts, true, true);
|
||||
await generateAppLocksInternal(
|
||||
change,
|
||||
false,
|
||||
workspace,
|
||||
opts,
|
||||
true,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (opts.jsonOutput) {
|
||||
const result = {
|
||||
@@ -1899,6 +1940,7 @@ export async function push(
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const remote = ZipFSElement(
|
||||
(await downloadZip(
|
||||
workspace,
|
||||
@@ -1935,13 +1977,13 @@ export async function push(
|
||||
specificItems
|
||||
);
|
||||
|
||||
|
||||
const rawWorkspaceDependencies = await getRawWorkspaceDependencies();
|
||||
|
||||
const tracker: ChangeTracker = await buildTracker(changes);
|
||||
|
||||
const staleScripts: string[] = [];
|
||||
const staleFlows: string[] = [];
|
||||
|
||||
for (const change of tracker.scripts) {
|
||||
const stale = await generateScriptMetadataInternal(
|
||||
change,
|
||||
@@ -1995,7 +2037,9 @@ export async function push(
|
||||
}
|
||||
|
||||
const version = await fetchVersion(workspace.remote);
|
||||
|
||||
if (version) {
|
||||
updateGlobalVersions(version);
|
||||
}
|
||||
log.info(colors.gray("Remote version: " + version));
|
||||
|
||||
log.info(
|
||||
@@ -2097,14 +2141,18 @@ export async function push(
|
||||
const alreadySynced: string[] = [];
|
||||
const isRawApp = isRawAppFile(changes[0].path);
|
||||
if (isRawApp) {
|
||||
const deleteRawApp = changes.find(change => change.name === "deleted" && change.path.endsWith(".raw_app/raw_app.yaml"))
|
||||
const deleteRawApp = changes.find(
|
||||
(change) =>
|
||||
change.name === "deleted" &&
|
||||
change.path.endsWith(".raw_app/raw_app.yaml")
|
||||
);
|
||||
if (deleteRawApp) {
|
||||
changes = [deleteRawApp];
|
||||
} else {
|
||||
changes.splice(1, changes.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for await (const change of changes) {
|
||||
let stateTarget = undefined;
|
||||
if (stateful) {
|
||||
@@ -2324,13 +2372,16 @@ export async function push(
|
||||
});
|
||||
break;
|
||||
case "raw_app":
|
||||
if (target.endsWith(".raw_app/raw_app.yaml") || target.endsWith(".raw_app/raw_app.json")) {
|
||||
if (
|
||||
target.endsWith(".raw_app/raw_app.yaml") ||
|
||||
target.endsWith(".raw_app/raw_app.json")
|
||||
) {
|
||||
await wmill.deleteApp({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".raw_app/raw_app.json"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case "schedule":
|
||||
await wmill.deleteSchedule({
|
||||
workspace: workspaceId,
|
||||
@@ -2426,18 +2477,25 @@ export async function push(
|
||||
});
|
||||
break;
|
||||
case "workspace_dependencies":
|
||||
const relativePath = removePathPrefix(change.path, "dependencies");
|
||||
|
||||
const res = workspaceDependenciesPathToLanguageAndFilename(change.path);
|
||||
const relativePath = removePathPrefix(
|
||||
change.path,
|
||||
"dependencies"
|
||||
);
|
||||
|
||||
const res = workspaceDependenciesPathToLanguageAndFilename(
|
||||
change.path
|
||||
);
|
||||
if (!res) {
|
||||
throw new Error(`Unknown workspace dependencies file format: ${change.path}`);
|
||||
throw new Error(
|
||||
`Unknown workspace dependencies file format: ${change.path}`
|
||||
);
|
||||
}
|
||||
const { name, language } = res;
|
||||
|
||||
await wmill.deleteWorkspaceDependencies({
|
||||
workspace: workspaceId,
|
||||
language,
|
||||
name
|
||||
name,
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -2537,7 +2595,10 @@ const command = new Command()
|
||||
.option("--skip-flows", "Skip syncing flows")
|
||||
.option("--skip-apps", "Skip syncing apps")
|
||||
.option("--skip-folders", "Skip syncing folders")
|
||||
.option("--skip-workspace-dependencies", "Skip syncing workspace dependencies")
|
||||
.option(
|
||||
"--skip-workspace-dependencies",
|
||||
"Skip syncing workspace dependencies"
|
||||
)
|
||||
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
|
||||
.option("--include-schedules", "Include syncing schedules")
|
||||
.option("--include-triggers", "Include syncing triggers")
|
||||
@@ -2586,7 +2647,10 @@ const command = new Command()
|
||||
.option("--skip-flows", "Skip syncing flows")
|
||||
.option("--skip-apps", "Skip syncing apps")
|
||||
.option("--skip-folders", "Skip syncing folders")
|
||||
.option("--skip-workspace-dependencies", "Skip syncing workspace dependencies")
|
||||
.option(
|
||||
"--skip-workspace-dependencies",
|
||||
"Skip syncing workspace dependencies"
|
||||
)
|
||||
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
|
||||
.option("--include-schedules", "Include syncing schedules")
|
||||
.option("--include-triggers", "Include syncing triggers")
|
||||
|
||||
@@ -757,8 +757,8 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
readOnlyMode={false}
|
||||
on:selectAndClose={(s3obj) => {
|
||||
let s = `'${formatS3Object(s3obj.detail)}'`
|
||||
onSelectAndClose={(s3obj) => {
|
||||
let s = `'${formatS3Object(s3obj)}'`
|
||||
if (lang === 'duckdb') {
|
||||
editor?.insertAtCursor(`SELECT * FROM ${s}`)
|
||||
} else if (lang === 'python3') {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { emptyString, type S3Object } from '$lib/utils'
|
||||
import { Button, Drawer } from './common'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import { createEventDispatcher, tick, untrack } from 'svelte'
|
||||
import { tick, untrack } from 'svelte'
|
||||
import S3FilePickerInner from './S3FilePickerInner.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { FileUp } from 'lucide-svelte'
|
||||
@@ -10,11 +10,6 @@
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
let dispatch = createEventDispatcher<{
|
||||
close: { s3: string; storage: string | undefined } | undefined
|
||||
selectAndClose: { s3: string; storage: string | undefined }
|
||||
}>()
|
||||
|
||||
interface Props {
|
||||
fromWorkspaceSettings?: boolean
|
||||
readOnlyMode: boolean
|
||||
@@ -22,6 +17,8 @@
|
||||
selectedFileKey?: { s3: string; storage?: string } | undefined
|
||||
folderOnly?: boolean
|
||||
regexFilter?: RegExp | undefined
|
||||
onClose?: () => void
|
||||
onSelectAndClose?: (selected: { s3: string; storage: string | undefined }) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -30,7 +27,9 @@
|
||||
initialFileKey = $bindable(undefined),
|
||||
selectedFileKey = $bindable(undefined),
|
||||
folderOnly = false,
|
||||
regexFilter = undefined
|
||||
regexFilter = undefined,
|
||||
onClose,
|
||||
onSelectAndClose
|
||||
}: Props = $props()
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
@@ -76,7 +75,7 @@
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
onClose?.()
|
||||
s3FilePickerInner?.close?.()
|
||||
}}
|
||||
size="1200px"
|
||||
@@ -93,7 +92,7 @@
|
||||
<S3FilePickerInner
|
||||
bind:this={s3FilePickerInner}
|
||||
on:selectAndClose={(e) => {
|
||||
dispatch('selectAndClose', e.detail)
|
||||
onSelectAndClose?.(e.detail)
|
||||
drawer?.closeDrawer?.()
|
||||
}}
|
||||
{fromWorkspaceSettings}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
bind:selectedFileKey={value}
|
||||
on:close={() => {
|
||||
onClose={() => {
|
||||
rawValue = JSON.stringify(value, null, 2)
|
||||
editor?.setCode(rawValue)
|
||||
}}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import ComponentErrorHandler from '../helpers/ComponentErrorHandler.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { isPartialS3Object, getS3File } from '../../editor/appUtilsS3'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
@@ -37,7 +39,8 @@
|
||||
initConfig(components['downloadcomponent'].initialData.configuration, configuration)
|
||||
)
|
||||
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { app, worldStore, appPath, workspace, isEditor } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
//used so that we can count number of outputs setup for first refresh
|
||||
initOutput($worldStore, id, {})
|
||||
@@ -45,6 +48,10 @@
|
||||
let beforeIconComponent: any = $state()
|
||||
let afterIconComponent: any = $state()
|
||||
|
||||
let downloadUrl: string | undefined = $state(undefined)
|
||||
|
||||
let token = getContext<{ token?: string }>('AuthToken')
|
||||
|
||||
async function handleBeforeIcon() {
|
||||
if (resolvedConfig.beforeIcon) {
|
||||
beforeIconComponent = await loadIcon(
|
||||
@@ -69,6 +76,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSource() {
|
||||
if (isPartialS3Object(resolvedConfig.source)) {
|
||||
downloadUrl = await getS3File({
|
||||
source: resolvedConfig.source.s3,
|
||||
storage: resolvedConfig.source.storage,
|
||||
presigned: resolvedConfig.source.presigned,
|
||||
appPath: $appPath,
|
||||
username: $userStore?.username,
|
||||
workspace,
|
||||
token: token?.token,
|
||||
isEditor,
|
||||
configuration
|
||||
})
|
||||
} else if (resolvedConfig.source && typeof resolvedConfig.source !== 'string') {
|
||||
throw new Error('Invalid source object' + typeof resolvedConfig.source)
|
||||
} else if (resolvedConfig.source?.startsWith('s3://')) {
|
||||
downloadUrl = await getS3File({
|
||||
source: resolvedConfig.source?.replace('s3://', ''),
|
||||
appPath: $appPath,
|
||||
username: $userStore?.username,
|
||||
workspace,
|
||||
token: token?.token,
|
||||
isEditor,
|
||||
configuration
|
||||
})
|
||||
} else {
|
||||
downloadUrl = transformBareBase64IfNecessary(resolvedConfig.source)
|
||||
}
|
||||
}
|
||||
|
||||
let css = $state(initCss($app.css?.downloadcomponent, customCss))
|
||||
$effect(() => {
|
||||
resolvedConfig.beforeIcon && beforeIconComponent && untrack(() => handleBeforeIcon())
|
||||
@@ -76,6 +113,9 @@
|
||||
$effect(() => {
|
||||
resolvedConfig.afterIcon && afterIconComponent && untrack(() => handleAfterIcon())
|
||||
})
|
||||
$effect(() => {
|
||||
resolvedConfig && loadSource()
|
||||
})
|
||||
</script>
|
||||
|
||||
<InitializeComponent {id} />
|
||||
@@ -102,7 +142,9 @@
|
||||
{#if render}
|
||||
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
|
||||
<ComponentErrorHandler
|
||||
hasError={resolvedConfig?.source != undefined && typeof resolvedConfig.source !== 'string'}
|
||||
hasError={resolvedConfig?.source != undefined &&
|
||||
typeof resolvedConfig.source !== 'string' &&
|
||||
!isPartialS3Object(resolvedConfig.source)}
|
||||
>
|
||||
<Button
|
||||
on:pointerdown={(e) => e.stopPropagation()}
|
||||
@@ -122,7 +164,7 @@
|
||||
extendedSize={resolvedConfig.size}
|
||||
color={resolvedConfig.color}
|
||||
download={resolvedConfig.filename}
|
||||
href={transformBareBase64IfNecessary(resolvedConfig.source)}
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
ref="external"
|
||||
nonCaptureEvent
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import { defaultIfEmptyString } from '$lib/utils'
|
||||
|
||||
import { userStore } from '$lib/stores'
|
||||
import { computeS3ImageViewerPolicy, isPartialS3Object } from '../../editor/appUtilsS3'
|
||||
import { isPartialS3Object, getS3File } from '../../editor/appUtilsS3'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
@@ -25,14 +25,6 @@
|
||||
|
||||
let { id, configuration, customCss = undefined, render }: Props = $props()
|
||||
|
||||
function computeForceViewerPolicies() {
|
||||
if (!isEditor) {
|
||||
return undefined
|
||||
}
|
||||
const policy = computeS3ImageViewerPolicy(configuration)
|
||||
return policy
|
||||
}
|
||||
|
||||
const resolvedConfig = $state(
|
||||
initConfig(components['imagecomponent'].initialData.configuration, configuration)
|
||||
)
|
||||
@@ -54,43 +46,34 @@
|
||||
|
||||
let token = getContext<{ token?: string }>('AuthToken')
|
||||
|
||||
async function getS3Image(source: string | undefined, storage?: string, presigned?: string) {
|
||||
if (!source) return ''
|
||||
const appPathOrUser = defaultIfEmptyString(
|
||||
$appPath,
|
||||
`u/${$userStore?.username ?? 'unknown'}/newapp`
|
||||
)
|
||||
const params = new URLSearchParams()
|
||||
params.append('s3', source)
|
||||
if (storage) {
|
||||
params.append('storage', storage)
|
||||
}
|
||||
|
||||
if (token?.token && token.token != '') {
|
||||
params.append('token', token.token)
|
||||
}
|
||||
const forceViewerPolicies = computeForceViewerPolicies()
|
||||
if (forceViewerPolicies) {
|
||||
params.append('force_viewer_allowed_s3_keys', JSON.stringify([forceViewerPolicies]))
|
||||
}
|
||||
|
||||
return `/api/w/${workspace}/apps_u/download_s3_file/${appPathOrUser}?${params.toString()}${presigned ? `&${presigned}` : ''}`
|
||||
}
|
||||
|
||||
async function loadImage() {
|
||||
if (isPartialS3Object(resolvedConfig.source)) {
|
||||
imageUrl = await getS3Image(
|
||||
resolvedConfig.source.s3,
|
||||
resolvedConfig.source.storage,
|
||||
resolvedConfig.source.presigned
|
||||
)
|
||||
imageUrl = await getS3File({
|
||||
source: resolvedConfig.source.s3,
|
||||
storage: resolvedConfig.source.storage,
|
||||
presigned: resolvedConfig.source.presigned,
|
||||
appPath: $appPath,
|
||||
username: $userStore?.username,
|
||||
workspace,
|
||||
token: token?.token,
|
||||
isEditor,
|
||||
configuration
|
||||
})
|
||||
} else if (resolvedConfig.source && typeof resolvedConfig.source !== 'string') {
|
||||
throw new Error('Invalid image object' + typeof resolvedConfig.source)
|
||||
} else if (
|
||||
resolvedConfig.sourceKind === 's3 (workspace storage)' ||
|
||||
resolvedConfig.source?.startsWith('s3://')
|
||||
) {
|
||||
imageUrl = await getS3Image(resolvedConfig.source?.replace('s3://', ''))
|
||||
imageUrl = await getS3File({
|
||||
source: resolvedConfig.source?.replace('s3://', ''),
|
||||
appPath: $appPath,
|
||||
username: $userStore?.username,
|
||||
workspace,
|
||||
token: token?.token,
|
||||
isEditor,
|
||||
configuration
|
||||
})
|
||||
} else if (resolvedConfig.sourceKind === 'png encoded as base64') {
|
||||
imageUrl = 'data:image/png;base64,' + resolvedConfig.source
|
||||
} else if (resolvedConfig.sourceKind === 'jpeg encoded as base64') {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { isPartialS3Object, getS3File } from '../../editor/appUtilsS3'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
@@ -17,7 +19,8 @@
|
||||
|
||||
let { id, configuration, customCss = undefined, render }: Props = $props()
|
||||
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { app, worldStore, appPath, workspace, isEditor } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
loading: false
|
||||
@@ -26,6 +29,44 @@
|
||||
let source: string | ArrayBuffer | undefined = $state(undefined)
|
||||
let zoom: number | undefined = $state(undefined)
|
||||
|
||||
let pdfSource: string | ArrayBuffer | undefined = $state(undefined)
|
||||
|
||||
let token = getContext<{ token?: string }>('AuthToken')
|
||||
|
||||
async function loadSource() {
|
||||
if (isPartialS3Object(source)) {
|
||||
pdfSource = await getS3File({
|
||||
source: source.s3,
|
||||
storage: source.storage,
|
||||
presigned: source.presigned,
|
||||
appPath: $appPath,
|
||||
username: $userStore?.username,
|
||||
workspace,
|
||||
token: token?.token,
|
||||
isEditor,
|
||||
configuration
|
||||
})
|
||||
} else if (source && typeof source !== 'string' && !(source instanceof ArrayBuffer)) {
|
||||
throw new Error('Invalid PDF source object' + typeof source)
|
||||
} else if (typeof source === 'string' && source?.startsWith('s3://')) {
|
||||
pdfSource = await getS3File({
|
||||
source: source?.replace('s3://', ''),
|
||||
appPath: $appPath,
|
||||
username: $userStore?.username,
|
||||
workspace,
|
||||
token: token?.token,
|
||||
isEditor,
|
||||
configuration
|
||||
})
|
||||
} else {
|
||||
pdfSource = source
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
source && loadSource()
|
||||
})
|
||||
|
||||
let css = $state(initCss($app.css?.pdfcomponent, customCss))
|
||||
</script>
|
||||
|
||||
@@ -50,7 +91,7 @@
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
{source}
|
||||
source={pdfSource}
|
||||
{zoom}
|
||||
class={css?.container?.class}
|
||||
style={css?.container?.style}
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { App } from '../types'
|
||||
import {
|
||||
computeS3FileInputPolicy,
|
||||
computeWorkspaceS3FileInputPolicy,
|
||||
computeS3ImageViewerPolicy
|
||||
computeS3FileViewerPolicy
|
||||
} from './appUtilsS3'
|
||||
import { collectStaticFields, type TriggerableV2 } from './commonAppUtils'
|
||||
import type { Policy } from '$lib/gen'
|
||||
@@ -165,11 +165,16 @@ export async function updatePolicy(app: App, currentPolicy: Policy | undefined):
|
||||
}
|
||||
|
||||
const s3FileKeys = items
|
||||
.filter((x) => (x.data as AppComponent).type === 'imagecomponent')
|
||||
.filter(
|
||||
(x) =>
|
||||
(x.data as AppComponent).type === 'imagecomponent' ||
|
||||
(x.data as AppComponent).type === 'pdfcomponent' ||
|
||||
(x.data as AppComponent).type === 'downloadcomponent'
|
||||
)
|
||||
.map((x) => {
|
||||
const c = x.data as AppComponent
|
||||
const config = c.configuration
|
||||
return computeS3ImageViewerPolicy(config)
|
||||
return computeS3FileViewerPolicy(config)
|
||||
})
|
||||
.filter(Boolean) as { s3_path: string; storage?: string | undefined }[]
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultIfEmptyString } from '$lib/utils'
|
||||
import type { AppInput, EvalInputV2 } from '../inputType'
|
||||
import type { App, RichConfigurations } from '../types'
|
||||
import { collectOneOfFields } from './appUtilsCore'
|
||||
@@ -86,7 +87,61 @@ export function isPartialS3Object(
|
||||
return input != undefined && typeof input === 'object' && typeof input['s3'] === 'string'
|
||||
}
|
||||
|
||||
export function computeS3ImageViewerPolicy(config: RichConfigurations) {
|
||||
function computeForceViewerPolicies({
|
||||
isEditor,
|
||||
configuration
|
||||
}: {
|
||||
isEditor: boolean
|
||||
configuration: RichConfigurations
|
||||
}) {
|
||||
if (!isEditor) {
|
||||
return undefined
|
||||
}
|
||||
const policy = computeS3FileViewerPolicy(configuration)
|
||||
return policy
|
||||
}
|
||||
|
||||
export async function getS3File({
|
||||
source,
|
||||
storage,
|
||||
presigned,
|
||||
appPath,
|
||||
username,
|
||||
workspace,
|
||||
token,
|
||||
isEditor,
|
||||
configuration
|
||||
}: {
|
||||
source: string | undefined
|
||||
storage?: string
|
||||
presigned?: string
|
||||
appPath: string
|
||||
username: string | undefined
|
||||
workspace: string
|
||||
token: string | undefined
|
||||
isEditor: boolean
|
||||
configuration: RichConfigurations
|
||||
}) {
|
||||
if (!source) return ''
|
||||
const appPathOrUser = defaultIfEmptyString(appPath, `u/${username ?? 'unknown'}/newapp`)
|
||||
const params = new URLSearchParams()
|
||||
params.append('s3', source)
|
||||
if (storage) {
|
||||
params.append('storage', storage)
|
||||
}
|
||||
|
||||
if (token && token != '') {
|
||||
params.append('token', token)
|
||||
}
|
||||
const forceViewerPolicies = computeForceViewerPolicies({ isEditor, configuration })
|
||||
if (forceViewerPolicies) {
|
||||
params.append('force_viewer_allowed_s3_keys', JSON.stringify([forceViewerPolicies]))
|
||||
}
|
||||
|
||||
return `/api/w/${workspace}/apps_u/download_s3_file/${appPathOrUser}?${params.toString()}${presigned ? `&${presigned}` : ''}`
|
||||
}
|
||||
|
||||
export function computeS3FileViewerPolicy(config: RichConfigurations) {
|
||||
if (config.source.type === 'uploadS3' && isPartialS3Object(config.source.value)) {
|
||||
return {
|
||||
s3_path: config.source.value.s3,
|
||||
|
||||
@@ -1642,6 +1642,9 @@ export const components = {
|
||||
accept: '*',
|
||||
convertTo: 'base64'
|
||||
},
|
||||
fileUploadS3: {
|
||||
accept: '*'
|
||||
},
|
||||
placeholder: 'Enter URL or upload file (base64)'
|
||||
},
|
||||
filename: {
|
||||
@@ -3459,8 +3462,7 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
convertTo: 'base64'
|
||||
},
|
||||
fileUploadS3: {
|
||||
accept: 'image/*',
|
||||
convertTo: 'base64'
|
||||
accept: 'image/*'
|
||||
}
|
||||
},
|
||||
sourceKind: {
|
||||
@@ -3660,6 +3662,9 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
accept: 'application/pdf',
|
||||
convertTo: 'base64'
|
||||
},
|
||||
fileUploadS3: {
|
||||
accept: 'application/pdf'
|
||||
},
|
||||
placeholder: 'Enter URL or upload file (base64)'
|
||||
},
|
||||
zoom: {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import ConnectionButton from '$lib/components/common/button/ConnectionButton.svelte'
|
||||
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
@@ -108,6 +109,7 @@
|
||||
let s3PickerSelection: { s3: string; storage?: string } | undefined = $state(undefined)
|
||||
let s3FolderPrefix: string = $state('')
|
||||
let s3FileUploadRawMode = $state(componentInput?.type == 'uploadS3' && !!componentInput.value?.s3)
|
||||
let s3JsonEditor: SimpleEditor | undefined = $state()
|
||||
|
||||
function updateSelectedS3File() {
|
||||
if (s3PickerSelection) {
|
||||
@@ -115,6 +117,7 @@
|
||||
componentInput.value = {
|
||||
...s3PickerSelection
|
||||
}
|
||||
s3JsonEditor?.setCode(JSON.stringify(s3PickerSelection, null, 2))
|
||||
}
|
||||
s3FileUploadRawMode = true
|
||||
}
|
||||
@@ -317,6 +320,7 @@
|
||||
<Module.default
|
||||
code={JSON.stringify(componentInput.value ?? { s3: '' }, null, 2)}
|
||||
bind:value={componentInput.value}
|
||||
bind:editor={s3JsonEditor}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
@@ -351,8 +355,8 @@
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
folderOnly={false}
|
||||
on:close={(e) => {
|
||||
s3PickerSelection = e.detail
|
||||
onSelectAndClose={(selected) => {
|
||||
s3PickerSelection = selected
|
||||
updateSelectedS3File()
|
||||
}}
|
||||
readOnlyMode={false}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
|
||||
import FileUpload from '$lib/components/common/fileUpload/FileUpload.svelte'
|
||||
import DucklakePicker from '$lib/components/DucklakePicker.svelte'
|
||||
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
|
||||
interface Props {
|
||||
componentInput: StaticInput<any> | undefined
|
||||
@@ -56,8 +57,11 @@
|
||||
componentInput && appContext?.onchange?.()
|
||||
})
|
||||
|
||||
let s3FileUploadRawMode = $state(false)
|
||||
let s3FileUploadRawMode = $state(
|
||||
componentInput?.value && typeof componentInput.value == 'object' && !!componentInput.value?.s3
|
||||
)
|
||||
let s3FilePicker: S3FilePicker | undefined = $state(undefined)
|
||||
let s3JsonEditor: SimpleEditor | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#key subFieldType}
|
||||
@@ -216,6 +220,7 @@
|
||||
<Module.default
|
||||
code={JSON.stringify(componentInput.value ?? { s3: '' }, null, 2)}
|
||||
bind:value={componentInput.value}
|
||||
bind:editor={s3JsonEditor}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
@@ -255,13 +260,12 @@
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
readOnlyMode={false}
|
||||
on:close={(e) => {
|
||||
if (e.detail) {
|
||||
if (componentInput) {
|
||||
componentInput.value = e.detail
|
||||
}
|
||||
s3FileUploadRawMode = true
|
||||
onSelectAndClose={(selected) => {
|
||||
if (componentInput) {
|
||||
componentInput.value = selected
|
||||
s3JsonEditor?.setCode(JSON.stringify(selected, null, 2))
|
||||
}
|
||||
s3FileUploadRawMode = true
|
||||
}}
|
||||
/>
|
||||
{:else if format?.startsWith('resource-') && (componentInput.value == undefined || typeof componentInput.value == 'string')}
|
||||
|
||||
@@ -51,16 +51,16 @@
|
||||
{#if $userStore}
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
on:selectAndClose={(ev) => {
|
||||
onSelectAndClose={(selected) => {
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
value.push(ev.detail)
|
||||
value.push(selected)
|
||||
} else {
|
||||
value = [ev.detail]
|
||||
value = [selected]
|
||||
}
|
||||
fileUpload?.addUpload(ev.detail)
|
||||
fileUpload?.addUpload(selected)
|
||||
} else {
|
||||
value = ev.detail
|
||||
value = selected
|
||||
fileUpload?.setUpload(value)
|
||||
}
|
||||
editor?.setCode(JSON.stringify(value))
|
||||
|
||||
@@ -377,7 +377,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update config for captures
|
||||
function getCaptureConfig() {
|
||||
const newCaptureConfig = {
|
||||
@@ -408,7 +407,7 @@
|
||||
bind:this={s3FilePicker}
|
||||
folderOnly={is_static_website}
|
||||
bind:selectedFileKey={static_asset_config}
|
||||
on:close={() => {
|
||||
onClose={() => {
|
||||
s3Editor?.setCode(JSON.stringify(static_asset_config, null, 2))
|
||||
s3FileUploadRawMode = true
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user