mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
various improvements
This commit is contained in:
+55
-14
@@ -1465,6 +1465,26 @@
|
||||
},
|
||||
"query": "UPDATE app SET versions = array_append(versions, $1) WHERE id = $2"
|
||||
},
|
||||
"42e1b5634a9e51247115fa73f85a97b1467c913d012cac9c45bb6a349082dc71": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT distinct(path) FROM flow WHERE workspace_id = $1"
|
||||
},
|
||||
"438fb925ee90d5115bd3c3be8ae48b56ba86017af3ca519bd3a15829edaa7d1b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2790,20 +2810,6 @@
|
||||
},
|
||||
"query": "SELECT workspace_id, name, display_name, owners, extra_perms FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3"
|
||||
},
|
||||
"877829f87c4d94c2e385ab9c5d75d3f70929c89231650e20db57c4b7c1d8911f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO group_\n (workspace_id, name, summary)\n VALUES ($1, $2, $3) ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"8876fa929ffb175cd976a2bca1195704aa9fe7215013ae29e49ef15cb201ba57": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -4928,6 +4934,21 @@
|
||||
},
|
||||
"query": "SELECT count(path) FROM app WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2"
|
||||
},
|
||||
"d444e1c1e12a82e9aee5c2ffc4d1d3841bd41dd71344ab155c9842b45bcf30b6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Jsonb"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO folder\n (workspace_id, name, owners, extra_perms)\n VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"d47bff7d6b54cd6da8bb330f7321c37af5dcbd76f9acad73b5ba1b8a4afb5091": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -5763,6 +5784,26 @@
|
||||
},
|
||||
"query": "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 AND workspace_id = $4 RETURNING id"
|
||||
},
|
||||
"f2485c69b3ab6bec11c4e1eac2934d6e49f83b71a72fe74eba7b49abc225df7c": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT distinct(path) FROM script WHERE workspace_id = $1"
|
||||
},
|
||||
"f325a1262084bd3468e12dc8bcc289a96536f172b679af54dd0fbc82d4d7c987": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
||||
@@ -2004,6 +2004,25 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/Script"
|
||||
|
||||
/w/{workspace}/scripts/list_paths:
|
||||
get:
|
||||
summary: list all available scripts paths
|
||||
operationId: listScriptPaths
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: list of script paths
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/scripts/create:
|
||||
post:
|
||||
summary: create script
|
||||
@@ -2411,6 +2430,24 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/flows/list_paths:
|
||||
get:
|
||||
summary: list all available flow paths
|
||||
operationId: listFlowPaths
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: list of flow paths
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/flows/list:
|
||||
get:
|
||||
summary: list all available flows
|
||||
@@ -2986,7 +3023,8 @@ paths:
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/Success"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/Suspend"
|
||||
- $ref: "#/components/parameters/Suspended"
|
||||
- $ref: "#/components/parameters/Running"
|
||||
responses:
|
||||
"200":
|
||||
description: All available queued jobs
|
||||
@@ -4419,12 +4457,18 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
Suspend:
|
||||
name: suspend
|
||||
Suspended:
|
||||
name: suspended
|
||||
description: filter on suspended jobs
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
Running:
|
||||
name: running
|
||||
description: filter on running jobs
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
After:
|
||||
name: after
|
||||
description: filter on created after (exclusive) timestamp
|
||||
|
||||
@@ -42,6 +42,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/archive/*path", post(archive_flow_by_path))
|
||||
.route("/get/*path", get(get_flow_by_path))
|
||||
.route("/exists/*path", get(exists_flow_by_path))
|
||||
.route("/list_paths", get(list_paths))
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
@@ -122,6 +123,24 @@ async fn list_hub_flows(
|
||||
Ok(Json(flows))
|
||||
}
|
||||
|
||||
async fn list_paths(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let flows = sqlx::query_scalar!(
|
||||
"SELECT distinct(path) FROM flow WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(flows))
|
||||
}
|
||||
|
||||
pub async fn get_hub_flow_by_id(
|
||||
Authed { email, .. }: Authed,
|
||||
Path(id): Path<i32>,
|
||||
|
||||
@@ -215,28 +215,40 @@ pub async fn get_job_by_id<'c>(
|
||||
pub struct CompletedJob {
|
||||
pub workspace_id: String,
|
||||
pub id: Uuid,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_job: Option<Uuid>,
|
||||
pub created_by: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub started_at: chrono::DateTime<chrono::Utc>,
|
||||
pub duration_ms: i32,
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub script_hash: Option<ScriptHash>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub script_path: Option<String>,
|
||||
pub args: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logs: Option<String>,
|
||||
pub deleted: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw_code: Option<String>,
|
||||
pub canceled: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub canceled_by: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub canceled_reason: Option<String>,
|
||||
pub job_kind: JobKind,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub schedule_path: Option<String>,
|
||||
pub permissioned_as: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flow_status: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw_flow: Option<serde_json::Value>,
|
||||
pub is_flow_step: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<ScriptLang>,
|
||||
pub is_skipped: bool,
|
||||
pub email: String,
|
||||
@@ -336,6 +348,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
if let Some(dt) = &lq.created_after {
|
||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
|
||||
if let Some(s) = &lq.suspended {
|
||||
if *s {
|
||||
sqlb.and_where_gt("suspend", 0);
|
||||
@@ -451,7 +464,6 @@ async fn list_jobs(
|
||||
"job_kind",
|
||||
"schedule_path",
|
||||
"permissioned_as",
|
||||
"flow_status",
|
||||
"is_flow_step",
|
||||
"language",
|
||||
"false as is_skipped",
|
||||
@@ -486,7 +498,6 @@ async fn list_jobs(
|
||||
"job_kind",
|
||||
"schedule_path",
|
||||
"permissioned_as",
|
||||
"flow_status",
|
||||
"is_flow_step",
|
||||
"language",
|
||||
"is_skipped",
|
||||
@@ -964,7 +975,6 @@ struct UnifiedJob {
|
||||
job_kind: JobKind,
|
||||
schedule_path: Option<String>,
|
||||
permissioned_as: String,
|
||||
flow_status: Option<serde_json::Value>,
|
||||
is_flow_step: bool,
|
||||
language: Option<ScriptLang>,
|
||||
is_skipped: bool,
|
||||
@@ -990,6 +1000,7 @@ impl From<UnifiedJob> for Job {
|
||||
args: uj.args,
|
||||
result: None,
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
deleted: uj.deleted,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
@@ -998,7 +1009,6 @@ impl From<UnifiedJob> for Job {
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
flow_status: uj.flow_status,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
@@ -1019,6 +1029,7 @@ impl From<UnifiedJob> for Job {
|
||||
running: uj.running.unwrap(),
|
||||
scheduled_for: uj.scheduled_for.unwrap(),
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
raw_code: None,
|
||||
raw_lock: None,
|
||||
canceled: uj.canceled,
|
||||
@@ -1028,7 +1039,6 @@ impl From<UnifiedJob> for Job {
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
flow_status: uj.flow_status,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
@@ -1621,7 +1631,7 @@ async fn delete_completed_job(
|
||||
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let job_o = sqlx::query_as::<_, CompletedJob>(
|
||||
"UPDATE completed_job SET logs = '', deleted = true WHERE id = $1 AND workspace_id = $2 \
|
||||
"UPDATE completed_job SET logs = '', result = null, deleted = true WHERE id = $1 AND workspace_id = $2 \
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(id)
|
||||
|
||||
@@ -656,19 +656,18 @@ async fn connect_slack_callback(
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO group_
|
||||
(workspace_id, name, summary)
|
||||
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
|
||||
"INSERT INTO folder
|
||||
(workspace_id, name, owners, extra_perms)
|
||||
VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
|
||||
&w_id,
|
||||
"slack",
|
||||
"The group that runs the script triggered by the slack /windmill command.
|
||||
Share scripts to this group to make them executable from slack and add
|
||||
members to this group to let them manage the slack related owner space."
|
||||
"slack_bot",
|
||||
&[],
|
||||
serde_json::json!({})
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
let token_path = "g/slack/bot_token";
|
||||
let token_path = "f/slack_bot/bot_token";
|
||||
let mc = build_crypt(&mut tx, &w_id).await?;
|
||||
let value = encrypt(&mc, &token.bot.bot_access_token);
|
||||
sqlx::query!(
|
||||
|
||||
@@ -72,6 +72,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/get/h/:hash", get(get_script_by_hash))
|
||||
.route("/raw/h/:hash", get(raw_script_by_hash))
|
||||
.route("/deployment_status/h/:hash", get(get_deployment_status))
|
||||
.route("/list_paths", get(list_paths))
|
||||
}
|
||||
async fn list_scripts(
|
||||
authed: Authed,
|
||||
@@ -464,6 +465,24 @@ async fn get_script_by_path(
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn list_paths(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let scripts = sqlx::query_scalar!(
|
||||
"SELECT distinct(path) FROM script WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(scripts))
|
||||
}
|
||||
|
||||
async fn raw_script_by_path(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -571,33 +571,49 @@ pub async fn get_hub_script(path: String, email: &str) -> error::Result<HubScrip
|
||||
pub struct QueuedJob {
|
||||
pub workspace_id: String,
|
||||
pub id: Uuid,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_job: Option<Uuid>,
|
||||
pub created_by: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub scheduled_for: chrono::DateTime<chrono::Utc>,
|
||||
pub running: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub script_hash: Option<ScriptHash>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub script_path: Option<String>,
|
||||
pub args: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logs: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw_code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw_lock: Option<String>,
|
||||
pub canceled: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub canceled_by: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub canceled_reason: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_ping: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub job_kind: JobKind,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub schedule_path: Option<String>,
|
||||
pub permissioned_as: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flow_status: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw_flow: Option<serde_json::Value>,
|
||||
pub is_flow_step: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<ScriptLang>,
|
||||
pub same_worker: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pre_run_error: Option<String>,
|
||||
pub email: String,
|
||||
pub visible_to_owner: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suspend: Option<i32>,
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-col w-full min-w-[250px]">
|
||||
<div>
|
||||
{#if displayHeader}
|
||||
<FieldHeader {label} {required} {type} {contentEncoding} {format} {itemsType} />
|
||||
@@ -229,7 +229,10 @@
|
||||
{:else}
|
||||
<input
|
||||
{autofocus}
|
||||
on:focus
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
{disabled}
|
||||
type="number"
|
||||
class={valid
|
||||
@@ -321,7 +324,10 @@
|
||||
{:else}
|
||||
<textarea
|
||||
bind:this={el}
|
||||
on:focus
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
{autofocus}
|
||||
{disabled}
|
||||
use:autosize
|
||||
@@ -337,7 +343,15 @@
|
||||
/>
|
||||
{/if}
|
||||
{:else if inputCat == 'enum'}
|
||||
<select {disabled} class="px-6" bind:value>
|
||||
<select
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
{disabled}
|
||||
class="px-6"
|
||||
bind:value
|
||||
>
|
||||
{#each enum_ ?? [] as e}
|
||||
<option>{e}</option>
|
||||
{/each}
|
||||
@@ -347,7 +361,10 @@
|
||||
{:else if inputCat == 'sql' || inputCat == 'yaml'}
|
||||
<div class="border my-1 mb-4 w-full border-gray-400">
|
||||
<SimpleEditor
|
||||
on:focus={() => dispatch('focus')}
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={() => dispatch('blur')}
|
||||
bind:this={editor}
|
||||
lang={inputCat}
|
||||
@@ -383,7 +400,10 @@
|
||||
{autofocus}
|
||||
rows="1"
|
||||
bind:this={el}
|
||||
on:focus={() => dispatch('focus')}
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={() => dispatch('blur')}
|
||||
use:autosize
|
||||
type="text"
|
||||
@@ -399,16 +419,13 @@
|
||||
/>
|
||||
{#if itemPicker}
|
||||
<div class="ml-1 relative">
|
||||
<Button
|
||||
{disabled}
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="min-w-min min-h-[34px] items-center leading-4 py-0"
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="min-w-min min-h-[34px] items-center leading-4 px-3 text-blue-500 cursor-pointer border border-blue-500 rounded center-center"
|
||||
on:click={() => {
|
||||
pickForField = label
|
||||
itemPicker?.openDrawer?.()
|
||||
}}><Icon data={faDollarSign} /></Button
|
||||
}}><Icon data={faDollarSign} /></div
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import { MoreHorizontal } from 'lucide-svelte'
|
||||
import { Button, Menu } from './common'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
type Alignment = 'start' | 'end'
|
||||
type Side = 'top' | 'bottom'
|
||||
@@ -60,7 +61,7 @@
|
||||
{:else if item.href && !item.disabled}
|
||||
<a
|
||||
href={item.href}
|
||||
on:click|stopPropagation
|
||||
on:click|stopPropagation|preventDefault={() => goto(item.href ?? '')}
|
||||
class="block w-full px-4 font-semibold text-left py-2 text-sm text-gray-700 hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30
|
||||
{item.disabled ? 'bg-gray-200' : ''}"
|
||||
role="menuitem"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
faCube,
|
||||
faDollarSign,
|
||||
faEye,
|
||||
faPlus,
|
||||
faRotate,
|
||||
faRotateLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
@@ -164,11 +165,12 @@
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
variableEditor.initNew()
|
||||
}}
|
||||
>
|
||||
Create a new variable
|
||||
New variable
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
@@ -48,12 +48,12 @@
|
||||
/>
|
||||
|
||||
<Drawer bind:this={drawer} size="600px">
|
||||
<DrawerContent title="Search a {itemName}" on:close={drawer.closeDrawer}>
|
||||
<div class="w-full">
|
||||
<DrawerContent overflow_y={false} title="Search {itemName}s" on:close={drawer.closeDrawer}>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="w-12/12 pb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search {itemName}"
|
||||
placeholder="Search {itemName}s"
|
||||
bind:value={filter}
|
||||
class="search-item"
|
||||
/>
|
||||
@@ -67,10 +67,10 @@
|
||||
{@html noItemMessage}
|
||||
</div>
|
||||
{:else if filteredItems?.length}
|
||||
<div class="border rounded-md divide-y divide-gray-200 w-full">
|
||||
<div class="border rounded-md divide-y divide-gray-200 w-full overflow-auto pb-12 grow">
|
||||
{#each filteredItems as obj}
|
||||
<div
|
||||
class="hover:bg-gray-50 w-full inline-flex items-center p-4 gap-4 first-of-type:!border-t-0
|
||||
class="hover:bg-gray-50 w-full flex items-center p-4 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md"
|
||||
>
|
||||
<div class="inline-flex items-center grow">
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
} else {
|
||||
throw Error('not testable module type')
|
||||
}
|
||||
sendUserToast(`started test ${truncateRev(jobId ?? '', 10)}`)
|
||||
}
|
||||
|
||||
function jobDone() {
|
||||
@@ -67,6 +66,7 @@
|
||||
{/if}
|
||||
|
||||
<RunForm
|
||||
loading={testIsLoading}
|
||||
runnable={{ summary: mod.summary ?? '', schema, description: '' }}
|
||||
runAction={(_, args) => runTest(args)}
|
||||
schedulable={false}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
export let detailed = true
|
||||
export let autofocus = false
|
||||
export let topButton = false
|
||||
export let loading = false
|
||||
|
||||
export let args: Record<string, any> = decodeArgs($page.url.searchParams.get('args') ?? undefined)
|
||||
|
||||
@@ -166,17 +167,18 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<Toggle
|
||||
options={{
|
||||
right: `make run invisible to ${runnable?.path?.split('/').slice(0, 2).join('/')}`
|
||||
right: `run only visible to you`
|
||||
}}
|
||||
bind:checked={invisible_to_owner}
|
||||
/>
|
||||
<Tooltip
|
||||
>By default, runs are visible to the owner of the script or flow being triggered</Tooltip
|
||||
>By default, runs are visible to the owner(s) of the script or flow being triggered</Tooltip
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-row-reverse flex grow">
|
||||
<Button
|
||||
{loading}
|
||||
btnClasses="!px-6 !py-1"
|
||||
disabled={!isValid}
|
||||
on:click={() => runAction(scheduledForStr, args, invisible_to_owner)}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { VariableService, type InputTransform } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { slide } from 'svelte/transition'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
@@ -56,7 +57,7 @@
|
||||
let variableEditor: VariableEditor | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-4' : ''}">
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 gap-y-2' : ''}">
|
||||
{#if Object.keys(schema?.properties ?? {}).length > 0}
|
||||
{#each Object.keys(schema?.properties ?? {}) as argName, i (argName)}
|
||||
{#if !filter || filter.includes(argName)}
|
||||
@@ -123,7 +124,7 @@
|
||||
}
|
||||
}}
|
||||
itemName="Variable"
|
||||
extraField="name"
|
||||
extraField="path"
|
||||
loadItems={async () =>
|
||||
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
||||
name: x.path,
|
||||
@@ -132,17 +133,18 @@
|
||||
>
|
||||
<div
|
||||
slot="submission"
|
||||
class="flex flex-row-reverse w-full p-5 bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
|
||||
class="flex flex-row-reverse w-full bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
|
||||
>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
variableEditor?.initNew?.()
|
||||
}}
|
||||
>
|
||||
Create a new variable
|
||||
New variable
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<InputValue {id} input={configuration.size} bind:value={size} />
|
||||
|
||||
<RunnableWrapper
|
||||
defaultUserInput
|
||||
noMinH
|
||||
bind:runnableComponent
|
||||
bind:componentInput
|
||||
@@ -61,8 +62,8 @@
|
||||
<div>
|
||||
{#if componentInput?.type != 'runnable' || Object.values(componentInput?.fields ?? {}).filter((x) => x.type == 'user').length == 0}
|
||||
<span class="text-gray-600 italic text-sm py-2">
|
||||
Run forms are meant to be associated with a runnable with some user inputs. Pick a
|
||||
runnable and set some 'Runnable Inputs' to 'User Input'
|
||||
Run forms are associated with a runnable that has user inputs. Once a runnable is
|
||||
chosen, set some 'Runnable Inputs' to 'User Input'
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
export function getValue(input: AppInput) {
|
||||
if (input.type === 'template' && isCodeInjection(input.eval)) {
|
||||
console.log(computeGlobalContext())
|
||||
try {
|
||||
return eval_like('`' + input.eval + '`', computeGlobalContext())
|
||||
} catch (e) {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
export let result: any = undefined
|
||||
export let forceSchemaDisplay: boolean = false
|
||||
export let noMinH = false
|
||||
export let defaultUserInput = false
|
||||
|
||||
const { worldStore, runnableComponents, workspace, appPath, isEditor, jobs, noBackend } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -117,7 +118,7 @@
|
||||
let schemaCopy: Schema = JSON.parse(JSON.stringify(schema))
|
||||
|
||||
const result = {}
|
||||
const newInputs = schemaToInputsSpec(schemaCopy)
|
||||
const newInputs = schemaToInputsSpec(schemaCopy, defaultUserInput)
|
||||
if (!fields) {
|
||||
return newInputs
|
||||
}
|
||||
@@ -231,7 +232,7 @@
|
||||
})
|
||||
})
|
||||
if (njob) {
|
||||
$jobs = [...$jobs, { job: njob, component: id }]
|
||||
$jobs = [{ job: njob, component: id }, ...$jobs]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let autoRefresh: boolean = true
|
||||
export let runnableComponent: RunnableComponent | undefined = undefined
|
||||
export let forceSchemaDisplay: boolean = false
|
||||
export let defaultUserInput = false
|
||||
|
||||
const { staticExporter, noBackend } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
@@ -34,6 +35,7 @@
|
||||
<slot />
|
||||
{:else if componentInput.type === 'runnable' && isRunnableDefined()}
|
||||
<RunnableComponent
|
||||
{defaultUserInput}
|
||||
bind:this={runnableComponent}
|
||||
bind:fields={componentInput.fields}
|
||||
bind:result
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<AlignWrapper {verticalAlignment}>
|
||||
<div class="flex w-full gap-1 px-1">
|
||||
<span>{min}</span>
|
||||
<div class="grow">
|
||||
<div class="grow ">
|
||||
<Range bind:value {min} {max} />
|
||||
</div>
|
||||
<span>{max}</span>
|
||||
|
||||
@@ -155,7 +155,6 @@
|
||||
'h-full w-full flex justify-center align-center items-center',
|
||||
gridComponent.data.card ? 'border border-gray-100' : ''
|
||||
)}
|
||||
on:click|preventDefault|capture|once|stopPropagation
|
||||
>
|
||||
<ComponentEditor
|
||||
{pointerdown}
|
||||
|
||||
@@ -128,7 +128,10 @@ declare const ${k} = ${JSON.stringify(v)};
|
||||
{:else if component.componentInput.type === 'connected' && component.componentInput !== undefined}
|
||||
<ConnectedInputEditor bind:componentInput={component.componentInput} />
|
||||
{:else if component.componentInput?.type === 'runnable' && component.componentInput !== undefined}
|
||||
<RunnableInputEditor bind:appInput={component.componentInput} />
|
||||
<RunnableInputEditor
|
||||
bind:appInput={component.componentInput}
|
||||
defaultUserInput={component.type == 'formcomponent'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if component.componentInput?.type === 'runnable' && Object.keys(component.componentInput.fields ?? {}).length > 0}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
} from '../../inputType'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
export let inputSpecs: Record<
|
||||
string,
|
||||
@@ -66,14 +67,16 @@
|
||||
/>
|
||||
{#if rowColumns}
|
||||
<ToggleButton
|
||||
title="From Row"
|
||||
title="Column"
|
||||
position="center"
|
||||
value="row"
|
||||
startIcon={{ icon: faTableCells }}
|
||||
size="xs"
|
||||
iconOnly
|
||||
disabled={staticOnly}
|
||||
/>
|
||||
><Tooltip
|
||||
>Use the column name to have the value of the cell be passed to the action</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
{/if}
|
||||
{#if userInputEnabled && (!input.format?.startsWith('resource-') || true)}
|
||||
<ToggleButton
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
const actionId = getNextId(components.map((x) => x.id.split('-')[1]))
|
||||
|
||||
const newComponent: BaseAppComponent & ButtonComponent = {
|
||||
id: `${id}-${actionId}`,
|
||||
id: `${id}_${actionId}`,
|
||||
type: 'buttoncomponent',
|
||||
configuration: {
|
||||
label: {
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@
|
||||
</script>
|
||||
|
||||
{#if componentInput}
|
||||
<input type="text" placeholder="column" bind:value={componentInput.column} />
|
||||
<input type="text" placeholder="column name" bind:value={componentInput.column} />
|
||||
{/if}
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@
|
||||
import SelectedRunnable from '../SelectedRunnable.svelte'
|
||||
|
||||
export let appInput: ResultAppInput
|
||||
export let defaultUserInput = false
|
||||
|
||||
$: isRunnableSelected = isScriptByPathDefined(appInput) || isScriptByNameDefined(appInput)
|
||||
</script>
|
||||
@@ -12,5 +13,5 @@
|
||||
{#if isRunnableSelected}
|
||||
<SelectedRunnable bind:appInput />
|
||||
{:else}
|
||||
<RunnableSelector bind:appInput />
|
||||
<RunnableSelector {defaultUserInput} bind:appInput />
|
||||
{/if}
|
||||
|
||||
+4
-3
@@ -16,6 +16,7 @@
|
||||
type Tab = 'hubscripts' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
|
||||
|
||||
export let appInput: ResultAppInput
|
||||
export let defaultUserInput = false
|
||||
|
||||
let tab: Tab = 'inlinescripts'
|
||||
let filter: string = ''
|
||||
@@ -33,7 +34,7 @@
|
||||
async function pickScript(path: string) {
|
||||
if (appInput.type === 'runnable') {
|
||||
const schema = await loadSchemaFromTriggerable(path, 'script')
|
||||
const fields = schemaToInputsSpec(schema)
|
||||
const fields = schemaToInputsSpec(schema, defaultUserInput)
|
||||
appInput.runnable = {
|
||||
type: 'runnableByPath',
|
||||
path,
|
||||
@@ -47,7 +48,7 @@
|
||||
async function pickFlow(path: string) {
|
||||
if (appInput.type === 'runnable') {
|
||||
const schema = await loadSchemaFromTriggerable(path, 'flow')
|
||||
const fields = schemaToInputsSpec(schema)
|
||||
const fields = schemaToInputsSpec(schema, defaultUserInput)
|
||||
appInput.runnable = {
|
||||
type: 'runnableByPath',
|
||||
path,
|
||||
@@ -61,7 +62,7 @@
|
||||
async function pickHubScript(path: string) {
|
||||
if (appInput.type === 'runnable') {
|
||||
const schema = await loadSchemaFromTriggerable(path, 'hubscript')
|
||||
const fields = schemaToInputsSpec(schema)
|
||||
const fields = schemaToInputsSpec(schema, defaultUserInput)
|
||||
appInput.runnable = {
|
||||
type: 'runnableByPath',
|
||||
path,
|
||||
|
||||
@@ -4,8 +4,12 @@ export function defaultCode(component: string, language: string): string | undef
|
||||
return [
|
||||
{ foo: x, bar: 42 },
|
||||
{ foo: "static", bar: 84 }]
|
||||
}
|
||||
`
|
||||
}`
|
||||
} else if (component === 'tablecomponent' && language === 'python3') {
|
||||
return `def main(x: str):
|
||||
return [
|
||||
{ "foo": x, "bar": 42 },
|
||||
{ "foo": "static", "bar": 84 }]`
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -58,21 +58,24 @@ export async function loadSchema(
|
||||
}
|
||||
}
|
||||
|
||||
export function schemaToInputsSpec(schema: Schema): Record<string, StaticAppInput> {
|
||||
export function schemaToInputsSpec(schema: Schema, defaultUserInput: boolean): Record<string, StaticAppInput> {
|
||||
if (schema?.properties == undefined) {
|
||||
return {}
|
||||
}
|
||||
return Object.keys(schema.properties).reduce((accu, key) => {
|
||||
const property = schema.properties[key]
|
||||
|
||||
|
||||
console.log(defaultUserInput)
|
||||
accu[key] = {
|
||||
type: 'static',
|
||||
type: defaultUserInput ? 'user' : 'static',
|
||||
value: property.default,
|
||||
visible: property.format ? false : true,
|
||||
fieldType: property.type,
|
||||
format: property.format
|
||||
}
|
||||
|
||||
|
||||
return accu
|
||||
}, {})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
blue: {
|
||||
border:
|
||||
'border-blue-500 hover:border-blue-700 focus:border-blue-700 bg-white hover:bg-blue-100 focus:bg-blue-100 text-blue-500 hover:text-blue-700 focus:text-blue-700 focus:ring-blue-300',
|
||||
contained: 'bg-blue-500 hover:bg-blue-700 focus:bg-blue-700 text-white focus:ring-blue-300'
|
||||
contained: 'bg-blue-800/90 hover:bg-blue-900 focus:bg-blue-900 text-white focus:ring-blue-300'
|
||||
},
|
||||
red: {
|
||||
border:
|
||||
|
||||
@@ -34,5 +34,7 @@
|
||||
baseClass={classNames(baseClassByLanguage, 'flex gap-1 items-center')}
|
||||
>
|
||||
<LanguageIcon lang={language} width={12} height={12} />
|
||||
{languageLabel}
|
||||
<span class="hidden lg:inline">
|
||||
{languageLabel}
|
||||
</span>
|
||||
</Badge>
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
}[kind]
|
||||
</script>
|
||||
|
||||
<div class="hover:bg-gray-50 w-full inline-flex items-center p-4 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md {color}">
|
||||
<div
|
||||
class="hover:bg-gray-50 w-full inline-flex items-center p-4 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
|
||||
>
|
||||
<RowIcon {href} {kind} />
|
||||
|
||||
<a {href} class="min-w-0 grow hover:underline decoration-gray-400">
|
||||
@@ -38,7 +40,7 @@ first-of-type:rounded-t-md last-of-type:rounded-b-md {color}">
|
||||
</div>
|
||||
</a>
|
||||
{#if $$slots.badges}
|
||||
<div class="w-32 hidden lg:flex flex-row gap-1 items-start flex-wrap">
|
||||
<div class="w-32 hidden md:flex flex-row gap-1 items-start flex-wrap">
|
||||
<slot name="badges" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -27,9 +27,7 @@
|
||||
startIcon={{ icon: faPlus }}
|
||||
href="/apps/add?nodraft=true"
|
||||
>
|
||||
<svelte:fragment slot="main"
|
||||
>New App (alpha) <LayoutDashboard class="ml-1.5" size={18} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="main">App <LayoutDashboard class="ml-1.5" size={18} /></svelte:fragment>
|
||||
<ButtonPopupItem on:click={() => drawer?.toggleDrawer?.()}>
|
||||
Import from raw JSON
|
||||
</ButtonPopupItem>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
href="/flows/add?nodraft=true"
|
||||
>
|
||||
<svelte:fragment slot="main"
|
||||
>New Flow <Icon data={faBarsStaggered} scale={0.8} class="ml-1.5" />
|
||||
>Flow <Icon data={faBarsStaggered} scale={0.8} class="ml-1.5" />
|
||||
</svelte:fragment>
|
||||
<ButtonPopupItem on:click={() => drawer?.toggleDrawer?.()}>
|
||||
Import from raw JSON
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { Folder, User } from 'lucide-svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Badge } from '../common'
|
||||
@@ -28,6 +29,10 @@
|
||||
<span style="height: 12px" class="-mt-0.5">
|
||||
{#if resourceType}
|
||||
<svelte:component this={APP_TO_ICON_COMPONENT[filter]} height="14px" width="14px" />
|
||||
{:else if filter.startsWith('u/')}
|
||||
<User class="mr-0.5" size={14} />
|
||||
{:else if filter.startsWith('f/')}
|
||||
<Folder class="mr-0.5" size={14} />
|
||||
{/if}
|
||||
</span>
|
||||
{filter}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- Buttons -->
|
||||
<div class="flex flex-row gap-2">
|
||||
<ButtonPopup size="sm" spacingSize="xl" startIcon={{ icon: faPlus }} href="/scripts/add">
|
||||
<svelte:fragment slot="main">New Script <Code2 class="ml-1.5" size={18} /></svelte:fragment>
|
||||
<svelte:fragment slot="main">Script <Code2 class="ml-1.5" size={18} /></svelte:fragment>
|
||||
<ButtonPopupItem on:click={() => drawer?.toggleDrawer?.()}>
|
||||
Import from template
|
||||
</ButtonPopupItem>
|
||||
|
||||
@@ -192,8 +192,9 @@
|
||||
</Alert>
|
||||
{/if}
|
||||
<PageHeader title="Home">
|
||||
<div class="flex flex-row gap-3 flex-wrap justify-end">
|
||||
<div class="flex flex-row gap-4 flex-wrap justify-end items-center">
|
||||
{#if !$userStore?.operator}
|
||||
<span class="text-sm text-gray-500">Create a new:</span>
|
||||
<CreateActionsScript />
|
||||
<CreateActionsFlow />
|
||||
<CreateActionsApp />
|
||||
|
||||
@@ -98,12 +98,14 @@
|
||||
|
||||
let runForm: RunForm | undefined
|
||||
let isValid = true
|
||||
let loading = false
|
||||
|
||||
async function runFlow(
|
||||
scheduledForStr: string | undefined,
|
||||
args: Record<string, any>,
|
||||
invisibleToOwner?: boolean
|
||||
) {
|
||||
loading = true
|
||||
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
|
||||
let run = await JobService.runFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -236,6 +238,7 @@
|
||||
<div class="col-span-2">
|
||||
<h2 class="mb-2">Preview</h2>
|
||||
<RunForm
|
||||
{loading}
|
||||
autofocus
|
||||
detailed={false}
|
||||
bind:isValid
|
||||
|
||||
@@ -37,11 +37,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
let loading = false
|
||||
|
||||
async function runFlow(
|
||||
scheduledForStr: string | undefined,
|
||||
args: Record<string, any>,
|
||||
invisibleToOwner?: boolean
|
||||
) {
|
||||
loading = true
|
||||
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
|
||||
let run = await JobService.runFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -127,6 +130,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<RunForm
|
||||
{loading}
|
||||
autofocus
|
||||
bind:this={runForm}
|
||||
bind:isValid
|
||||
|
||||
@@ -24,13 +24,14 @@
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
import LogViewer from '$lib/components/LogViewer.svelte'
|
||||
import { Button, ActionRow, Skeleton, Tab } from '$lib/components/common'
|
||||
import { Button, ActionRow, Skeleton, Tab, Alert } from '$lib/components/common'
|
||||
import FlowMetadata from '$lib/components/FlowMetadata.svelte'
|
||||
import JobArgs from '$lib/components/JobArgs.svelte'
|
||||
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
|
||||
$: workspace_id = $page.url.searchParams.get('workspace') ?? $workspaceStore
|
||||
$: not_same_workspace = workspace_id !== $workspaceStore
|
||||
@@ -123,16 +124,21 @@
|
||||
{@const isScript = job?.job_kind === 'script'}
|
||||
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
|
||||
{#if job && 'deleted' in job && !job?.deleted && ($superadmin || ($userStore?.is_admin ?? false))}
|
||||
<Button
|
||||
disabled={not_same_workspace}
|
||||
variant="border"
|
||||
color="red"
|
||||
size="md"
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => job?.id && deleteCompletedJob(job.id)}
|
||||
<Dropdown
|
||||
btnClasses="!text-red-500"
|
||||
placement="bottom-start"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'delete log and results (admin only)',
|
||||
icon: faTrash,
|
||||
action: () => {
|
||||
job?.id && deleteCompletedJob(job.id)
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
delete
|
||||
</Dropdown>
|
||||
<Button
|
||||
disabled={not_same_workspace}
|
||||
href={runsHref}
|
||||
@@ -275,11 +281,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
</h1>
|
||||
{#if job && 'deleted' in job && job?.deleted}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4" role="alert">
|
||||
<p class="font-bold">Deleted</p>
|
||||
<p>The content of this run was deleted (by an admin, no less)</p>
|
||||
</div>
|
||||
{#if job?.['deleted']}
|
||||
<Alert type="error" title="Deleted">
|
||||
The content of this run was deleted (by an admin, no less)
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- Arguments and actions -->
|
||||
@@ -329,7 +334,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{:else if !job?.['deleted']}
|
||||
<div class="mt-10" />
|
||||
<FlowProgressBar {job} class="py-4" />
|
||||
<div class="w-full mt-10 mb-20">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
import { JobService, Job, CompletedJob } from '$lib/gen'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { JobService, Job, CompletedJob, ScriptService, FlowService } from '$lib/gen'
|
||||
import { setQuery } from '$lib/utils'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
@@ -14,8 +14,10 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import RunChart from '$lib/components/RunChart.svelte'
|
||||
import { faSearch, faSearchMinus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSearchMinus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
|
||||
let jobs: Job[] | undefined
|
||||
let error: Error | undefined
|
||||
@@ -31,8 +33,7 @@
|
||||
? $page.url.searchParams.get('is_skipped') == 'true'
|
||||
: false
|
||||
|
||||
let showOlderJobs = true
|
||||
const jobsPerPage = 100
|
||||
let nbOfJobs = 30
|
||||
|
||||
$: path = $page.params.path
|
||||
|
||||
@@ -48,11 +49,11 @@
|
||||
} else if (jobKindsCat == 'previews') {
|
||||
return `${CompletedJob.job_kind.PREVIEW},${CompletedJob.job_kind.FLOWPREVIEW}`
|
||||
} else {
|
||||
return `${CompletedJob.job_kind.SCRIPT},${CompletedJob.job_kind.FLOW},${CompletedJob.job_kind.SCRIPT_HUB}`
|
||||
return `${CompletedJob.job_kind.SCRIPT},${CompletedJob.job_kind.FLOW}`
|
||||
}
|
||||
}
|
||||
|
||||
$: ($workspaceStore && loadJobs(createdBefore)) || (success && isSkipped && jobKinds)
|
||||
$: ($workspaceStore && loadJobs(createdBefore)) || (path && success && isSkipped && jobKinds)
|
||||
|
||||
async function fetchJobs(
|
||||
createdBefore: string | undefined,
|
||||
@@ -70,21 +71,9 @@
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchCompletedJobs(createdBefore: string | undefined): Promise<CompletedJob[]> {
|
||||
return JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore!,
|
||||
createdBefore,
|
||||
scriptPathExact: path === '' ? undefined : path,
|
||||
jobKinds: jobKinds,
|
||||
success,
|
||||
isSkipped
|
||||
})
|
||||
}
|
||||
|
||||
async function loadJobs(createdBefore: string | undefined): Promise<void> {
|
||||
try {
|
||||
const newJobs = await fetchJobs(createdBefore, undefined)
|
||||
showOlderJobs = newJobs.length === jobsPerPage
|
||||
jobs = newJobs
|
||||
} catch (err) {
|
||||
sendUserToast(`There was a problem fetching jobs: ${err}`, true)
|
||||
@@ -95,16 +84,13 @@
|
||||
|
||||
async function loadOlderJobs() {
|
||||
if (jobs) {
|
||||
const ts = jobs[jobs.length - 1].created_at
|
||||
const olderJobs = await fetchCompletedJobs(ts!)
|
||||
showOlderJobs = olderJobs.length === jobsPerPage
|
||||
jobs = jobs.concat(...olderJobs)
|
||||
nbOfJobs += 30
|
||||
}
|
||||
}
|
||||
|
||||
async function syncer() {
|
||||
if (jobs && createdBefore === undefined) {
|
||||
const reversedJobs = jobs.slice(0, jobsPerPage).reverse()
|
||||
const reversedJobs = [...jobs].reverse()
|
||||
const lastIndex = reversedJobs.findIndex((x) => x.type == Job.type.QUEUED_JOB) - 1
|
||||
let ts = lastIndex >= 0 ? reversedJobs[lastIndex].created_at : undefined
|
||||
if (!ts) {
|
||||
@@ -124,18 +110,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
loadJobs(createdBefore)
|
||||
path // trigger on path change
|
||||
success && isSkipped && jobKinds
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
intervalId = setInterval(syncer, 5000)
|
||||
}
|
||||
}
|
||||
onMount(() => {
|
||||
loadPaths()
|
||||
intervalId = setInterval(syncer, 5000)
|
||||
})
|
||||
|
||||
let paths: string[] = []
|
||||
|
||||
async function loadPaths() {
|
||||
const npaths_scripts = await ScriptService.listScriptPaths({ workspace: $workspaceStore ?? '' })
|
||||
const npaths_flows = await FlowService.listFlowPaths({ workspace: $workspaceStore ?? '' })
|
||||
paths = npaths_scripts.concat(npaths_flows).sort()
|
||||
}
|
||||
async function syncCatWithURL() {
|
||||
await setQuery($page.url, 'job_kinds', jobKindsCat)
|
||||
}
|
||||
@@ -144,6 +130,7 @@
|
||||
|
||||
$: completedJobs =
|
||||
jobs?.filter((x) => x.type == 'CompletedJob').map((x) => x as CompletedJob) ?? []
|
||||
|
||||
onDestroy(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
@@ -153,6 +140,12 @@
|
||||
$: searchPath = path
|
||||
let minTs = undefined
|
||||
let maxTs = undefined
|
||||
|
||||
$: searchPath && onSearchPathChange()
|
||||
|
||||
function onSearchPathChange() {
|
||||
goto(`/runs/${searchPath}?${$page.url.searchParams.toString()}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<CenteredPage>
|
||||
@@ -161,27 +154,6 @@
|
||||
tooltip="All past and schedule executions of scripts and flows, including previews.
|
||||
You only see your own runs or runs of groups you belong to unless you are an admin."
|
||||
/>
|
||||
<div class="flex flex-row gap-x-2">
|
||||
<input placeholder="Search jobs at a given path" type="text" bind:value={searchPath} />
|
||||
<Button
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
goto('/runs?' + $page.url.searchParams.toString())
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
<Icon data={faSearchMinus} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
goto('/runs?' + $page.url.searchParams.toString())
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
<Icon data={faSearch} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7x mt-2">
|
||||
<div class="flex flex-row space-x-4">
|
||||
@@ -237,12 +209,23 @@
|
||||
><span class="text-xs absolute -top-4">max datetime</span>
|
||||
<input type="text" value={maxTs ?? 'zoom x axis to set max'} disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-2 mb-2 w-full">
|
||||
{#key path}
|
||||
<AutoComplete
|
||||
items={paths}
|
||||
value={path}
|
||||
bind:selectedItem={searchPath}
|
||||
placeholder="Search by path of script/flow"
|
||||
/>
|
||||
{/key}
|
||||
<Button
|
||||
variant="border"
|
||||
on:click={async () => {
|
||||
on:click={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
goto('/runs?' + $page.url.searchParams.toString())
|
||||
fetchJobs(createdBefore, undefined)
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
@@ -250,13 +233,17 @@
|
||||
</Button>
|
||||
</div>
|
||||
<Skeleton loading={!jobs} layout={[[6], 1, [6], 1, [6], 1, [6], 1, [6]]} />
|
||||
|
||||
{#if jobs}
|
||||
<div class="space-y-0">
|
||||
{#each jobs as job}
|
||||
{#each jobs.slice(0, nbOfJobs) as job (job.id)}
|
||||
<JobDetail {job} />
|
||||
<div class="line w-20 h-4" />
|
||||
{/each}
|
||||
</div>
|
||||
{#if jobs.length == 0}
|
||||
<NoItemFound />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
@@ -266,13 +253,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-center pb-6">
|
||||
{#if jobs && jobs.length >= jobsPerPage && showOlderJobs}
|
||||
{#if (jobs?.length ?? 0) >= nbOfJobs}
|
||||
<div class="text-center pb-6">
|
||||
<button class=" mt-4 text-blue-500 text-center text-sm" on:click={loadOlderJobs}>
|
||||
Load older jobs
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -137,12 +137,15 @@
|
||||
|
||||
let isValid = true
|
||||
let runForm: RunForm | undefined
|
||||
|
||||
let runLoading = false
|
||||
async function runScript(
|
||||
scheduledForStr: string | undefined,
|
||||
args: Record<string, any>,
|
||||
invisibleToOwner?: boolean
|
||||
) {
|
||||
try {
|
||||
runLoading = true
|
||||
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
|
||||
let run = await JobService.runScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -153,6 +156,7 @@
|
||||
})
|
||||
await goto('/run/' + run + '?workspace=' + $workspaceStore)
|
||||
} catch (err) {
|
||||
runLoading = false
|
||||
sendUserToast(`Could not create job: ${err.body}`, true)
|
||||
}
|
||||
}
|
||||
@@ -333,6 +337,7 @@
|
||||
<div class="col-span-2">
|
||||
<h2 class="mb-2">Preview</h2>
|
||||
<RunForm
|
||||
loading={runLoading}
|
||||
autofocus
|
||||
detailed={false}
|
||||
bind:isValid
|
||||
|
||||
@@ -51,12 +51,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
let loading = false
|
||||
async function runScript(
|
||||
scheduledForStr: string | undefined,
|
||||
args: Record<string, any>,
|
||||
invisibleToOwner?: boolean
|
||||
) {
|
||||
try {
|
||||
loading = true
|
||||
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
|
||||
let run = await JobService.runScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -67,6 +69,7 @@
|
||||
})
|
||||
await goto('/run/' + run + '?workspace=' + $workspaceStore)
|
||||
} catch (err) {
|
||||
loading = false
|
||||
sendUserToast(`Could not create job: ${err.body}`, true)
|
||||
}
|
||||
}
|
||||
@@ -179,6 +182,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<RunForm
|
||||
{loading}
|
||||
autofocus
|
||||
detailed={false}
|
||||
bind:isValid
|
||||
|
||||
Reference in New Issue
Block a user