feat: lock inline scripts for apps on deploy

This commit is contained in:
Ruben Fiszel
2023-08-26 09:12:19 +02:00
parent 99a9d56325
commit 3d79187f31
24 changed files with 373 additions and 73 deletions

View File

@@ -39,7 +39,8 @@
"http",
"graphql",
"postgresql",
"noop"
"noop",
"appdependencies"
]
}
}
@@ -59,6 +60,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -44,7 +44,8 @@
"http",
"graphql",
"postgresql",
"noop"
"noop",
"appdependencies"
]
}
}
@@ -66,6 +67,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM app_version WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "29fce7e56bb4a874b4c1e3a6454273203aab7344abd03df4d86e4c5f5b9a394a"
}

View File

@@ -17,6 +17,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -22,7 +22,8 @@
"http",
"graphql",
"postgresql",
"noop"
"noop",
"appdependencies"
]
}
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app_version SET value = $1 WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Json",
"Int8"
]
},
"nullable": []
},
"hash": "6fb904692513c453989ffedb14b1eb737da453e9fcac6cfa098296d6c211d2a9"
}

View File

@@ -28,6 +28,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -38,7 +38,8 @@
"http",
"graphql",
"postgresql",
"noop"
"noop",
"appdependencies"
]
}
}
@@ -59,7 +60,8 @@
"http",
"graphql",
"postgresql",
"noop"
"noop",
"appdependencies"
]
}
}

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TYPE JOB_KIND ADD VALUE IF NOT EXISTS 'appdependencies';

View File

@@ -1873,7 +1873,8 @@ paths:
/w/{workspace}/resources/get_value_interpolated/{path}:
get:
summary: get resource interpolated (variables and resources are fully unrolled)
summary:
get resource interpolated (variables and resources are fully unrolled)
operationId: getResourceValueInterpolated
tags:
- resource
@@ -2328,7 +2329,8 @@ paths:
schema:
type: string
- name: first_parent_hash
description: mask to filter scripts whom first direct parent has exact hash
description:
mask to filter scripts whom first direct parent has exact hash
in: query
schema:
type: string
@@ -2496,7 +2498,8 @@ paths:
/workers/custom_tags:
get:
summary: get all instance custom tags (tags are used to dispatch jobs to
summary:
get all instance custom tags (tags are used to dispatch jobs to
different worker groups)
operationId: getCustomTags
tags:
@@ -2547,7 +2550,8 @@ paths:
/w/{workspace}/scripts/delete/h/{hash}:
post:
summary: delete script by hash (erase content but keep hash, require admin)
summary:
delete script by hash (erase content but keep hash, require admin)
operationId: deleteScriptByHash
tags:
- script
@@ -2739,14 +2743,16 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
description:
schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/NewJobId"
- name: invisible_to_owner
description: make the run invisible to the the script owner (default false)
description:
make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3624,7 +3630,8 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
description:
schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
@@ -3632,7 +3639,8 @@ paths:
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description: make the run invisible to the the flow owner (default false)
description:
make the run invisible to the the flow owner (default false)
in: query
schema:
type: boolean
@@ -3670,7 +3678,8 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
description:
schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
@@ -3678,7 +3687,8 @@ paths:
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description: make the run invisible to the the script owner (default false)
description:
make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3709,7 +3719,8 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description: make the run invisible to the the script owner (default false)
description:
make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3742,7 +3753,8 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description: make the run invisible to the the script owner (default false)
description:
make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3935,7 +3947,8 @@ paths:
- job
responses:
"200":
description: the timestamp of the db that can be used to compute the drift
description:
the timestamp of the db that can be used to compute the drift
content:
application/json:
schema:
@@ -4168,7 +4181,8 @@ paths:
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get:
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
summary:
get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls
tags:
- job
@@ -4681,7 +4695,8 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- name: only_member_of
in: query
description: only list the groups the user is member of (default false)
description:
only list the groups the user is member of (default false)
schema:
type: boolean
responses:
@@ -4869,7 +4884,8 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- name: only_member_of
in: query
description: only list the folders the user is member of (default false)
description:
only list the folders the user is member of (default false)
schema:
type: boolean
responses:
@@ -5507,7 +5523,8 @@ components:
type: integer
PerPage:
name: per_page
description: number of items to return for a given page (default 30, max 100)
description:
number of items to return for a given page (default 30, max 100)
in: query
schema:
type: integer
@@ -5641,7 +5658,8 @@ components:
type: boolean
ArgsFilter:
name: args
description: filter on jobs containing those args as a json subset (@> in postgres)
description:
filter on jobs containing those args as a json subset (@> in postgres)
in: query
schema:
type: string
@@ -5653,7 +5671,8 @@ components:
type: string
ResultFilter:
name: result
description: filter on jobs containing those result as a json subset (@> in postgres)
description:
filter on jobs containing those result as a json subset (@> in postgres)
in: query
schema:
type: string
@@ -6016,6 +6035,7 @@ components:
"preview",
"dependencies",
"flowdependencies",
"appdependencies",
"flow",
"flowpreview",
"script_hub",
@@ -6121,6 +6141,7 @@ components:
"dependencies",
"flow",
"flowdependencies",
"appdependencies",
"flowpreview",
"script_hub",
"identity",

View File

@@ -38,7 +38,7 @@ use windmill_common::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::push;
use windmill_queue::{push, PushIsolationLevel, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -372,14 +372,16 @@ async fn get_secret_id(
async fn create_app(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(mut app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
app.policy.on_behalf_of_email = Some(authed.email);
app.policy.on_behalf_of_email = Some(authed.email.clone());
if &app.path == "" {
return Err(Error::BadRequest("App path cannot be empty".to_string()));
@@ -390,7 +392,7 @@ async fn create_app(
&app.path,
w_id
)
.fetch_one(&mut *tx)
.fetch_one(&mut tx)
.await?
.unwrap_or(false);
@@ -406,7 +408,7 @@ async fn create_app(
&app.path,
&w_id
)
.execute(&mut *tx)
.execute(&mut tx)
.await?;
let id = sqlx::query_scalar!(
@@ -419,7 +421,7 @@ async fn create_app(
json!(app.policy),
app.draft_only,
)
.fetch_one(&mut *tx)
.fetch_one(&mut tx)
.await?;
let v_id = sqlx::query_scalar!(
@@ -431,7 +433,7 @@ async fn create_app(
serde_json::to_string(&app.value).unwrap(),
authed.username,
)
.fetch_one(&mut *tx)
.fetch_one(&mut tx)
.await?;
sqlx::query!(
@@ -439,11 +441,11 @@ async fn create_app(
v_id,
id
)
.execute(&mut *tx)
.execute(&mut tx)
.await?;
audit_log(
&mut *tx,
&mut tx,
&authed.username,
"apps.create",
ActionKind::Create,
@@ -453,6 +455,31 @@ async fn create_app(
)
.await?;
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, tx) = push(
&db,
tx,
&w_id,
JobPayload::AppDependencies { path: app.path.clone(), version: v_id },
serde_json::Map::new(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
None,
None,
false,
false,
None,
true,
None,
None,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
tx.commit().await?;
webhook.send_message(
w_id.clone(),
@@ -541,8 +568,10 @@ async fn delete_app(
async fn update_app(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
) -> Result<String> {
@@ -550,7 +579,7 @@ async fn update_app(
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let npath = if ns.policy.is_some() || ns.path.is_some() || ns.summary.is_some() {
let mut sqlb = SqlBuilder::update_table("app");
@@ -567,7 +596,7 @@ async fn update_app(
npath,
w_id
)
.fetch_one(&mut *tx)
.fetch_one(&mut tx)
.await?
.unwrap_or(false);
@@ -587,7 +616,7 @@ async fn update_app(
if let Some(mut npolicy) = ns.policy {
npolicy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
npolicy.on_behalf_of_email = Some(authed.email);
npolicy.on_behalf_of_email = Some(authed.email.clone());
sqlb.set(
"policy",
&format!(
@@ -602,18 +631,18 @@ async fn update_app(
sqlb.returning("path");
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut *tx).await?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
not_found_if_none(npath_o, "App", path)?
} else {
"".to_string()
};
if let Some(nvalue) = &ns.value {
let v_id = if let Some(nvalue) = &ns.value {
let app_id = sqlx::query_scalar!(
"SELECT id FROM app WHERE path = $1 AND workspace_id = $2",
npath,
w_id
)
.fetch_one(&mut *tx)
.fetch_one(&mut tx)
.await?;
let v_id = sqlx::query_scalar!(
@@ -625,7 +654,7 @@ async fn update_app(
serde_json::to_string(&nvalue).unwrap(),
authed.username,
)
.fetch_one(&mut *tx)
.fetch_one(&mut tx)
.await?;
sqlx::query!(
@@ -634,20 +663,23 @@ async fn update_app(
npath,
w_id
)
.execute(&mut *tx)
.execute(&mut tx)
.await?;
}
Some(v_id)
} else {
None
};
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
path,
&w_id
)
.execute(&mut *tx)
.execute(&mut tx)
.await?;
audit_log(
&mut *tx,
&mut tx,
&authed.username,
"apps.update",
ActionKind::Update,
@@ -656,7 +688,39 @@ async fn update_app(
None,
)
.await?;
tx.commit().await?;
if let Some(v_id) = v_id {
let tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, tx) = push(
&db,
tx,
&w_id,
JobPayload::AppDependencies { path: npath.clone(), version: v_id },
serde_json::Map::new(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
None,
None,
false,
false,
None,
true,
None,
None,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
tx.commit().await?;
} else {
tx.commit().await?;
}
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {

View File

@@ -244,11 +244,6 @@ async fn create_flow(
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
);
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, mut tx) = push(
&db,
@@ -284,6 +279,11 @@ async fn create_flow(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
);
Ok((StatusCode::CREATED, nf.path.to_string()))
}

View File

@@ -59,6 +59,9 @@ pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>(
#[cfg(not(feature = "enterprise"))]
let _resource: Option<&str> = Some("EE only");
#[cfg(not(feature = "enterprise"))]
let username: &str = "redacted";
tracing::info!(
operation = operation,
action_kind = ?action_kind,

View File

@@ -23,6 +23,7 @@ pub enum JobKind {
FlowPreview,
Identity,
FlowDependencies,
AppDependencies,
Noop,
}
@@ -180,6 +181,10 @@ pub enum JobPayload {
FlowDependencies {
path: String,
},
AppDependencies {
path: String,
version: i64,
},
Flow(String),
RawFlow {
value: FlowValue,

View File

@@ -1359,6 +1359,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
if usage > MAX_FREE_EXECS
&& !matches!(job_payload, JobPayload::Dependencies { .. })
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
&& !matches!(job_payload, JobPayload::AppDependencies { .. })
{
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
@@ -1492,6 +1493,16 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
None,
)
}
JobPayload::AppDependencies { path, version } => (
Some(version),
Some(path),
None,
JobKind::AppDependencies,
None,
None,
None,
None,
),
JobPayload::RawFlow { value, path } => (
None,
path,
@@ -1700,6 +1711,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
JobKind::Identity => "jobs.run.identity",
JobKind::Noop => "jobs.run.noop",
JobKind::FlowDependencies => "jobs.run.flow_dependencies",
JobKind::AppDependencies => "jobs.run.app_dependencies",
};
audit_log(

View File

@@ -1079,6 +1079,11 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
handle_flow_dependency_job(&job, &mut logs, job_dir, db, worker_name, worker_dir, base_internal_url, &client.get_token().await)
.await
.map(|()| Value::Null)
},
JobKind::AppDependencies => {
handle_app_dependency_job(&job, &mut logs, job_dir, db, worker_name, worker_dir, base_internal_url, &client.get_token().await)
.await
.map(|()| Value::Null)
}
JobKind::Identity => match job.args.clone() {
Some(Value::Object(args))
@@ -1540,7 +1545,6 @@ async fn handle_dependency_job(
}
}
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_flow_dependency_job(
job: &QueuedJob,
logs: &mut String,
@@ -1563,7 +1567,7 @@ async fn handle_flow_dependency_job(
})?;
let mut flow = serde_json::from_value::<FlowValue>(raw_flow).map_err(to_anyhow)?;
flow.modules = lock_modules(flow.modules, job, logs, job_dir, db, worker_name, worker_dir, job_path.clone(), base_internal_url, token).await?;
flow.modules = lock_modules(flow.modules, job, logs, job_dir, db, worker_name, worker_dir, &job_path, base_internal_url, token).await?;
let new_flow_value = serde_json::to_value(flow).map_err(to_anyhow)?;
// Re-check cancelation to ensure we don't accidentially override a flow.
@@ -1599,7 +1603,7 @@ async fn lock_modules(
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
worker_dir: &str,
job_path: String,
job_path: &str,
base_internal_url: &str,
token: &str) -> Result<Vec<FlowModule>> {
let mut new_flow_modules = Vec::new();
@@ -1635,7 +1639,7 @@ async fn lock_modules(
};
// sync with windmill-api/scripts
let dependencies = match language {
ScriptLang::Python3 => windmill_parser_py_imports::parse_python_imports(&content, &job.workspace_id, &path.clone().unwrap_or_else(|| job_path.clone()), &db).await?.join("\n"),
ScriptLang::Python3 => windmill_parser_py_imports::parse_python_imports(&content, &job.workspace_id, &path.clone().unwrap_or_else(|| job_path.to_string()), &db).await?.join("\n"),
_ => content.clone(),
};
let new_lock = capture_dependency_job(
@@ -1694,6 +1698,142 @@ async fn lock_modules(
}
Ok(new_flow_modules)
}
#[async_recursion]
async fn lock_modules_app(
value: Value,
job: &QueuedJob,
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
worker_dir: &str,
job_path: &str,
base_internal_url: &str,
token: &str) -> Result<Value> {
match value {
Value::Object(mut m) => {
if m.contains_key("inlineScript") {
let v = m.get_mut("inlineScript").unwrap();
if let Some(v) = v.as_object_mut() {
if v.contains_key("content") && v.contains_key("language") {
if let Ok(language) = serde_json::from_value::<ScriptLang>(v.get("language").unwrap().clone()) {
let content = v.get("content").unwrap().as_str().unwrap_or_default().to_string();
let dependencies = match language {
ScriptLang::Python3 => windmill_parser_py_imports::parse_python_imports(&content, &job.workspace_id, job_path, &db).await?.join("\n"),
_ => content.clone(),
};
logs.push_str("Found lockable inline script. Generating lock...\n");
let new_lock = capture_dependency_job(
&job.id,
&language,
&dependencies,
logs,
job_dir,
db,
worker_name,
&job.workspace_id,
worker_dir,
base_internal_url,
token,
job.script_path()
)
.await;
match new_lock {
Ok(new_lock) => {
v.insert("lock".to_string(), serde_json::Value::String(new_lock));
return Ok(Value::Object(m.clone()))
}
Err(e) => {
tracing::warn!(
language = ?language,
error = ?e,
logs = ?logs,
"Failed to generate flow lock for inline script"
);
()
}
}
}
}
}
}
for (a, b) in m.clone().into_iter() {
m.insert(
a.clone(),
lock_modules_app(b, job, logs, job_dir, db, worker_name, worker_dir, job_path, base_internal_url, token).await?,
);
}
Ok(Value::Object(m))
},
Value::Array(a) => {
let mut nv = vec![];
for b in a.clone().into_iter() {
nv.push(lock_modules_app(b, job, logs, job_dir, db, worker_name, worker_dir, job_path, base_internal_url, token).await?);
}
Ok(Value::Array(nv))
},
a @ _ => Ok(a),
}
}
async fn handle_app_dependency_job(
job: &QueuedJob,
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
worker_dir: &str,
base_internal_url: &str,
token: &str,
) -> error::Result<()> {
let job_path = job.script_path.clone().ok_or_else(|| {
error::Error::InternalErr(
"Cannot resolve flow dependencies for flow without path".to_string(),
)
})?;
let id = job.script_hash.clone().ok_or_else(|| {
Error::InternalErr(
"Flow Dependency requires script hash".to_owned(),
)})?.0;
let value = sqlx::query_scalar!("SELECT value FROM app_version WHERE id = $1", id)
.fetch_optional(db)
.await?;
if let Some(value) = value {
let value = lock_modules_app(value, job, logs, job_dir, db, worker_name, worker_dir, &job_path, base_internal_url, token).await?;
// Re-check cancelation to ensure we don't accidentially override a flow.
if sqlx::query_scalar!("SELECT canceled FROM queue WHERE id = $1", job.id)
.fetch_optional(db)
.await
.map(|v| Some(true) == v)
.unwrap_or_else(|err| {
tracing::error!(%job.id, %err, "error checking cancelation for job {0}: {err}", job.id);
false
})
{
return Ok(());
}
sqlx::query!(
"UPDATE app_version SET value = $1 WHERE id = $2",
value,
id,
)
.execute(db)
.await?;
Ok(())
} else {
Ok(())
}
}
async fn capture_dependency_job(
job_id: &Uuid,
job_language: &ScriptLang,

View File

@@ -246,7 +246,8 @@
requestBody['raw_code'] = {
content: inlineScript.content,
language: inlineScript.language,
path: inlineScript.path
path: inlineScript.path,
lock: inlineScript.lock
}
}
} else if (runnable?.type === 'runnableByPath') {

View File

@@ -262,6 +262,9 @@
}}
on:change={async (e) => {
if (inlineScript && inlineScript.language != 'frontend') {
if (inlineScript.lock) {
inlineScript.lock = undefined
}
const oldSchema = JSON.stringify(inlineScript.schema)
if (inlineScript.schema == undefined) {
inlineScript.schema = emptySchema()

View File

@@ -297,21 +297,6 @@
{/key}
</PanelSection>
{/if}
{#if Object.values(initialConfiguration).length > 0}
<PanelSection title="Configuration">
<InputsSpecsEditor
id={component.id}
inputSpecsConfiguration={initialConfiguration}
bind:inputSpecs={componentSettings.item.data.configuration}
userInputEnabled={false}
/>
</PanelSection>
{:else}
<div class="h-full w-full text-sm text-tertiary text-center py-8 px-2"
>{ccomponents[component.type].name} has no configuration</div
>
{/if}
{#if componentSettings.item.data.type === 'tabscomponent'}
<GridTab
bind:tabs={componentSettings.item.data.tabs}
@@ -339,6 +324,21 @@
<TableActions id={component.id} bind:components={componentSettings.item.data.actionButtons} />
{/if}
{#if Object.values(initialConfiguration).length > 0}
<PanelSection title="Configuration">
<InputsSpecsEditor
id={component.id}
inputSpecsConfiguration={initialConfiguration}
bind:inputSpecs={componentSettings.item.data.configuration}
userInputEnabled={false}
/>
</PanelSection>
{:else}
<div class="h-full w-full text-sm text-tertiary text-center py-8 px-2"
>{ccomponents[component.type].name} has no configuration</div
>
{/if}
{#if (`recomputeIds` in componentSettings.item.data && Array.isArray(componentSettings.item.data.recomputeIds)) || componentSettings.item.data.type === 'buttoncomponent' || componentSettings.item.data.type === 'formcomponent' || componentSettings.item.data.type === 'formbuttoncomponent' || componentSettings.item.data.type === 'checkboxcomponent'}
<Recompute
bind:recomputeIds={componentSettings.item.data.recomputeIds}

View File

@@ -45,7 +45,7 @@
}
</script>
<PanelSection title={`Table actions`}>
<PanelSection title={`Table Actions`}>
{#if components.length == 0}
<span class="text-xs text-tertiary">No action buttons</span>
{/if}

View File

@@ -106,6 +106,7 @@ export type InlineScript = {
language: Preview.language | 'frontend'
path?: string
schema?: Schema
lock?: String
refreshOn?: { id: string; key: string }[]
suggestedRefreshOn?: { id: string; key: string }[]
}

View File

@@ -177,6 +177,7 @@
JOBS_RUN_IDENTITY: 'jobs.run.identity',
JOBS_RUN_NOOP: 'jobs.run.noop',
JOBS_FLOW_DEPENDENCIES: 'jobs.flow_dependencies',
JOBS_APP_DEPENDENCIES: 'jobs.app_dependencies',
JOBS: 'jobs',
JOBS_CANCEL: 'jobs.cancel',
JOBS_FORCE_CANCEL: 'jobs.force_cancel',

View File

@@ -53,9 +53,9 @@
function computeJobKinds(jobKindsCat: string | undefined): string {
if (jobKindsCat == 'all') {
return `${CompletedJob.job_kind.SCRIPT},${CompletedJob.job_kind.FLOW},${CompletedJob.job_kind.DEPENDENCIES},${CompletedJob.job_kind.FLOWDEPENDENCIES},${CompletedJob.job_kind.PREVIEW},${CompletedJob.job_kind.FLOWPREVIEW},${CompletedJob.job_kind.SCRIPT_HUB}`
return `${CompletedJob.job_kind.SCRIPT},${CompletedJob.job_kind.FLOW},${CompletedJob.job_kind.DEPENDENCIES},${CompletedJob.job_kind.FLOWDEPENDENCIES},${CompletedJob.job_kind.APPDEPENDENCIES},${CompletedJob.job_kind.PREVIEW},${CompletedJob.job_kind.FLOWPREVIEW},${CompletedJob.job_kind.SCRIPT_HUB}`
} else if (jobKindsCat == 'dependencies') {
return `${CompletedJob.job_kind.DEPENDENCIES},${CompletedJob.job_kind.FLOWDEPENDENCIES}`
return `${CompletedJob.job_kind.DEPENDENCIES},${CompletedJob.job_kind.FLOWDEPENDENCIES},${CompletedJob.job_kind.APPDEPENDENCIES}`
} else if (jobKindsCat == 'previews') {
return `${CompletedJob.job_kind.PREVIEW},${CompletedJob.job_kind.FLOWPREVIEW}`
} else {