various improvements

This commit is contained in:
Ruben Fiszel
2023-01-10 11:21:16 +01:00
parent 4e0c2d06fc
commit 48d87ab12c
42 changed files with 375 additions and 168 deletions
+55 -14
View File
@@ -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": [],
+47 -3
View File
@@ -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
+19
View File
@@ -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>,
+16 -6
View File
@@ -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)
+7 -8
View File
@@ -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!(
+19
View File
@@ -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>,
+16
View File
@@ -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>,
}
+30 -13
View File
@@ -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}
+2 -1
View File
@@ -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"
+3 -1
View File
@@ -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}
+4 -2
View File
@@ -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: {
@@ -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}
@@ -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}
@@ -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
}
+5 -2
View File
@@ -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