mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 00:16:53 +00:00
Merge branch 'main' into rf/strip2
This commit is contained in:
@@ -305,6 +305,9 @@ upcoming CLI tool.
|
||||
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
|
||||
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
|
||||
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
|
||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
||||
|
||||
|
||||
|
||||
|
||||
## Run a local dev setup
|
||||
|
||||
@@ -179,6 +179,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
|
||||
"git" => "GitPython",
|
||||
"u" => "requests",
|
||||
"f" => "requests",
|
||||
"shopify" => "ShopifyAPI",
|
||||
"seleniumwire" => "selenium-wire",
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::{require_owner_of_path, Authed},
|
||||
variables::get_workspace_key,
|
||||
BaseUrl, TimeoutWaitResult,
|
||||
BaseUrl, QueueLimitWaitResult, TimeoutWaitResult,
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -1298,12 +1298,13 @@ pub async fn run_wait_result_job_by_path(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(timeout): Extension<Arc<TimeoutWaitResult>>,
|
||||
Extension(queue_limit): Extension<Arc<QueueLimitWaitResult>>,
|
||||
Path((w_id, script_path)): Path<(String, StripPath)>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
||||
) -> error::JsonResult<serde_json::Value> {
|
||||
check_queue_too_long(db, run_query.queue_limit).await?;
|
||||
check_queue_too_long(db, queue_limit.0.or(run_query.queue_limit)).await?;
|
||||
let script_path = script_path.to_path();
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
|
||||
|
||||
@@ -54,6 +54,7 @@ pub struct CookieDomain(Option<String>);
|
||||
pub struct CloudHosted(bool);
|
||||
pub struct ContentSecurityPolicy(String);
|
||||
pub struct TimeoutWaitResult(i32);
|
||||
pub struct QueueLimitWaitResult(Option<i64>);
|
||||
|
||||
pub use users::delete_expired_items_perdiodically;
|
||||
|
||||
@@ -117,14 +118,18 @@ pub async fn run_server(
|
||||
.nest("/scripts", scripts::workspaced_service())
|
||||
.nest(
|
||||
"/jobs",
|
||||
jobs::workspaced_service().layer(Extension(Arc::new(
|
||||
TimeoutWaitResult(
|
||||
jobs::workspaced_service()
|
||||
.layer(Extension(Arc::new(TimeoutWaitResult(
|
||||
std::env::var("TIMEOUT_WAIT_RESULT")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(20),
|
||||
),
|
||||
))),
|
||||
))))
|
||||
.layer(Extension(Arc::new(QueueLimitWaitResult(
|
||||
std::env::var("QUEUE_LIMIT_WAIT_RESULT")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok()),
|
||||
)))),
|
||||
)
|
||||
.nest(
|
||||
"/users",
|
||||
|
||||
@@ -3,12 +3,11 @@ name: "bash run script"
|
||||
mode: ONCE
|
||||
hostname: "bash"
|
||||
log_level: ERROR
|
||||
time_limit: 300
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_as: 4096
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
rlimit_fsize: 1000
|
||||
rlimit_nofile: 10000
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
@@ -55,7 +54,7 @@ mount {
|
||||
dst: "/tmp"
|
||||
fstype: "tmpfs"
|
||||
rw: true
|
||||
options: "size=500000000"
|
||||
options: "size=800000000"
|
||||
}
|
||||
|
||||
mount {
|
||||
@@ -87,4 +86,4 @@ iface_no_lo: true
|
||||
|
||||
{SHARED_MOUNT}
|
||||
|
||||
|
||||
envar: "HOME=/tmp"
|
||||
|
||||
@@ -3,12 +3,11 @@ name: "deno run script"
|
||||
mode: ONCE
|
||||
hostname: "deno"
|
||||
log_level: ERROR
|
||||
time_limit: 300
|
||||
|
||||
rlimit_as: 16000
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
rlimit_fsize: 1000
|
||||
rlimit_nofile: 10000
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ name: "go run script"
|
||||
mode: ONCE
|
||||
hostname: "go"
|
||||
log_level: ERROR
|
||||
time_limit: 300
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_as: 4096
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
rlimit_fsize: 1000
|
||||
rlimit_nofile: 10000
|
||||
|
||||
cwd: "/tmp/go"
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ name: "python run script"
|
||||
mode: ONCE
|
||||
hostname: "python"
|
||||
log_level: ERROR
|
||||
time_limit: 300
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_as: 4096
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
rlimit_fsize: 1000
|
||||
rlimit_nofile: 10000
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use sqlx::{Pool, Postgres, Transaction};
|
||||
use std::{borrow::Borrow, collections::HashMap, io, panic, process::Stdio, time::Duration};
|
||||
use std::{
|
||||
borrow::Borrow, collections::HashMap, io, os::unix::process::ExitStatusExt, panic,
|
||||
process::Stdio, time::Duration,
|
||||
};
|
||||
use tracing::{trace_span, Instrument};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -2420,9 +2423,12 @@ async fn handle_child(
|
||||
} else if let Some(code) = status.code() {
|
||||
Err(error::Error::ExitStatus(code))
|
||||
} else {
|
||||
Err(error::Error::ExecutionErr(
|
||||
"process terminated by signal".to_string(),
|
||||
))
|
||||
Err(error::Error::ExecutionErr(format!(
|
||||
"process terminated by signal: {:#?}, stopped_signal: {:#?}, core_dumped: {}",
|
||||
status.signal(),
|
||||
status.stopped_signal(),
|
||||
status.core_dumped()
|
||||
)))
|
||||
}
|
||||
}
|
||||
Ok(Err(kill_reason)) => Err(Error::ExecutionErr(format!(
|
||||
|
||||
@@ -3,6 +3,9 @@ FROM ghcr.io/windmill-labs/windmill-ee
|
||||
RUN /usr/local/bin/python3 -m pip install nltk
|
||||
RUN mkdir -p /nsjail_data/python && HOME=/nsjail_data/python /usr/local/bin/python3 -m nltk.downloader vader_lexicon
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - &&\
|
||||
apt-get install -y nodejs
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y tesseract-ocr \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
Generated
+11
@@ -24,6 +24,7 @@
|
||||
"monaco-yaml": "^4.0.2",
|
||||
"svelte-autosize": "^1.0.1",
|
||||
"svelte-chartjs": "^3.1.0",
|
||||
"svelte-portal": "^2.2.0",
|
||||
"svelte-select": "^5.0.2",
|
||||
"vscode-ws-jsonrpc": "^2.0.1"
|
||||
},
|
||||
@@ -6256,6 +6257,11 @@
|
||||
"svelte": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-portal": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-portal/-/svelte-portal-2.2.0.tgz",
|
||||
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
|
||||
},
|
||||
"node_modules/svelte-preprocess": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
|
||||
@@ -11548,6 +11554,11 @@
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"svelte-portal": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-portal/-/svelte-portal-2.2.0.tgz",
|
||||
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
|
||||
},
|
||||
"svelte-preprocess": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"@sveltejs/package": "^1.0.1",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelvet": "^4.0.20",
|
||||
@@ -77,6 +77,7 @@
|
||||
"monaco-yaml": "^4.0.2",
|
||||
"svelte-autosize": "^1.0.1",
|
||||
"svelte-chartjs": "^3.1.0",
|
||||
"svelte-portal": "^2.2.0",
|
||||
"svelte-select": "^5.0.2",
|
||||
"vscode-ws-jsonrpc": "^2.0.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import type { ConnectedAppInput, RowAppInput, StaticAppInput, UserAppInput } from '../inputType'
|
||||
import type { InlineScript } from '../types'
|
||||
import RunnableComponent from './helpers/RunnableComponent.svelte'
|
||||
|
||||
export let id: string
|
||||
export let name: string
|
||||
export let inlineScript: InlineScript | undefined
|
||||
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
|
||||
|
||||
let result: any = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['result', 'loading']
|
||||
</script>
|
||||
|
||||
<RunnableComponent
|
||||
{id}
|
||||
bind:fields
|
||||
bind:result
|
||||
runnable={{
|
||||
name,
|
||||
inlineScript,
|
||||
type: 'runnableByName'
|
||||
}}
|
||||
wrapperClass="hidden"
|
||||
>
|
||||
<slot />
|
||||
</RunnableComponent>
|
||||
@@ -86,11 +86,11 @@
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
}}
|
||||
on:click={(e) => {
|
||||
on:click={async (e) => {
|
||||
e?.stopPropagation()
|
||||
e?.preventDefault()
|
||||
ownClick = true
|
||||
runnableComponent?.runComponent()
|
||||
await runnableComponent?.runComponent()
|
||||
|
||||
if (recomputeIds) {
|
||||
recomputeIds.forEach((id) => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputDefaultValue from '../helpers/InputDefaultValue.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
export let id: string
|
||||
@@ -17,6 +18,7 @@
|
||||
let labelValue: string = 'Title'
|
||||
let minValue: string = ''
|
||||
let maxValue: string = ''
|
||||
let defaultValue: string | undefined = undefined
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<string>
|
||||
@@ -25,11 +27,16 @@
|
||||
function handleInput() {
|
||||
outputs?.result.set(input.value)
|
||||
}
|
||||
|
||||
$: input && handleInput()
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.label} bind:value={labelValue} />
|
||||
<InputValue {id} input={configuration.minDate} bind:value={minValue} />
|
||||
<InputValue {id} input={configuration.maxDate} bind:value={maxValue} />
|
||||
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
|
||||
|
||||
<InputDefaultValue bind:input {defaultValue} />
|
||||
|
||||
<AlignWrapper {verticalAlignment}>
|
||||
<input
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<script lang="ts">
|
||||
import { Button, type ButtonType } from '$lib/components/common'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import Portal from 'svelte-portal'
|
||||
import Modal from '$lib/components/common/modal/Modal.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
export let configuration: Record<string, AppInput>
|
||||
export let recomputeIds: string[] | undefined = undefined
|
||||
export let extraQueryParams: Record<string, any> = {}
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
const { runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let labelValue: string = 'Default label'
|
||||
let color: ButtonType.Color
|
||||
let size: ButtonType.Size
|
||||
let runnableComponent: RunnableComponent
|
||||
let disabled: boolean | undefined = undefined
|
||||
|
||||
let isLoading: boolean = false
|
||||
let ownClick: boolean = false
|
||||
|
||||
let errors: Record<string, string> = {}
|
||||
$: errorsMessage = Object.values(errors)
|
||||
.filter((x) => x != '')
|
||||
.join('\n')
|
||||
|
||||
$: noInputs =
|
||||
componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<Array<any>>
|
||||
loading: Output<boolean>
|
||||
}
|
||||
|
||||
$: if (outputs?.loading != undefined) {
|
||||
outputs.loading.set(false, true)
|
||||
}
|
||||
|
||||
$: outputs?.loading.subscribe({
|
||||
next: (value) => {
|
||||
isLoading = value
|
||||
if (ownClick && !value) {
|
||||
ownClick = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$: loading = isLoading && ownClick
|
||||
|
||||
let open: boolean = false
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.label} bind:value={labelValue} />
|
||||
<InputValue {id} input={configuration.color} bind:value={color} />
|
||||
<InputValue {id} input={configuration.size} bind:value={size} />
|
||||
<InputValue
|
||||
row={extraQueryParams['row']}
|
||||
{id}
|
||||
input={configuration.disabled}
|
||||
bind:value={disabled}
|
||||
bind:error={errors.disabled}
|
||||
/>
|
||||
|
||||
<Portal>
|
||||
<Modal
|
||||
{open}
|
||||
title={labelValue}
|
||||
on:canceled={() => {
|
||||
open = false
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
open = false
|
||||
}}
|
||||
>
|
||||
<RunnableWrapper
|
||||
defaultUserInput
|
||||
noMinH
|
||||
bind:runnableComponent
|
||||
bind:componentInput
|
||||
{id}
|
||||
{extraQueryParams}
|
||||
autoRefresh={false}
|
||||
forceSchemaDisplay={true}
|
||||
runnableClass="!block"
|
||||
>
|
||||
<div class="flex flex-col gap-2 px-4 w-full">
|
||||
<div>
|
||||
{#if noInputs}
|
||||
<div class="text-gray-600 italic text-sm my-4">
|
||||
Run forms are associated with a runnable that has user inputs.
|
||||
<br />
|
||||
Once a script or flow is chosen, set some <strong>Runnable Inputs</strong> to
|
||||
<strong>
|
||||
User Input
|
||||
<Icon data={faUser} scale={1.3} class="rounded-sm bg-gray-200 p-1 ml-0.5" />
|
||||
</strong>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
{loading}
|
||||
btnClasses="my-1"
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
}}
|
||||
on:click={async () => {
|
||||
await runnableComponent?.runComponent()
|
||||
|
||||
if (recomputeIds) {
|
||||
recomputeIds.forEach((id) => {
|
||||
$runnableComponents[id]?.()
|
||||
})
|
||||
}
|
||||
|
||||
open = false
|
||||
}}
|
||||
size="xs"
|
||||
color="dark"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
{#if errorsMessage}
|
||||
<div class="text-red-500 text-xs">{errorsMessage}</div>
|
||||
{/if}
|
||||
<Button
|
||||
{disabled}
|
||||
{size}
|
||||
{color}
|
||||
on:click={(e) => {
|
||||
open = true
|
||||
}}
|
||||
>
|
||||
{labelValue}
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
type InputType = string | number | boolean
|
||||
export let input: HTMLInputElement | undefined = undefined
|
||||
export let defaultValue: InputType | undefined = undefined
|
||||
|
||||
function setInputValueToDefaultValue() {
|
||||
if (defaultValue !== undefined && input) {
|
||||
input.value = String(defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
function clearInputValue() {
|
||||
if (input) {
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
$: input && defaultValue && setInputValueToDefaultValue()
|
||||
$: input &&
|
||||
(defaultValue === '' || defaultValue === undefined || defaultValue === null) &&
|
||||
clearInputValue()
|
||||
</script>
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputDefaultValue from '../helpers/InputDefaultValue.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
export let id: string
|
||||
@@ -12,31 +13,39 @@
|
||||
export const staticOutputs: string[] = ['result']
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
let value: number
|
||||
let labelValue: string = 'Title'
|
||||
let input: HTMLInputElement
|
||||
|
||||
let defaultValue: number | undefined = undefined
|
||||
let placeholder: string | undefined = undefined
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<number | null>
|
||||
}
|
||||
$: if (value || !value) {
|
||||
// Disallow 'e' character in numbers
|
||||
// if(value && value.toString().includes('e')) {
|
||||
// value = +value.toString().replaceAll('e', '')
|
||||
// }
|
||||
|
||||
function handleInput() {
|
||||
const value = input?.value
|
||||
const num = isNaN(+value) ? null : +value
|
||||
outputs?.result.set(num)
|
||||
}
|
||||
|
||||
$: input && handleInput()
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.label} bind:value={labelValue} />
|
||||
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
|
||||
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
|
||||
<InputDefaultValue bind:input {defaultValue} />
|
||||
|
||||
<AlignWrapper {verticalAlignment}>
|
||||
<input
|
||||
bind:value
|
||||
bind:this={input}
|
||||
on:input={handleInput}
|
||||
on:focus={(e) => {
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
}}
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
pattern="\d*"
|
||||
placeholder="Type..."
|
||||
class="h-full"
|
||||
{placeholder}
|
||||
/>
|
||||
</AlignWrapper>
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
result.length > 0 &&
|
||||
// We need to wait until the component is mounted so the world is created
|
||||
mounted &&
|
||||
outputs &&
|
||||
toggleRow({ original: result[0] }, 0)
|
||||
|
||||
function setOptions(filteredResult: Array<Record<string, any>>) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputDefaultValue from '../helpers/InputDefaultValue.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
export let id: string
|
||||
@@ -16,6 +17,7 @@
|
||||
let input: HTMLInputElement
|
||||
|
||||
let placeholder: string | undefined = undefined
|
||||
let defaultValue: string | undefined = undefined
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<string>
|
||||
@@ -24,9 +26,13 @@
|
||||
function handleInput() {
|
||||
outputs?.result.set(input.value)
|
||||
}
|
||||
|
||||
$: input && handleInput()
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
|
||||
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
|
||||
<InputDefaultValue bind:input {defaultValue} />
|
||||
|
||||
<AlignWrapper {verticalAlignment}>
|
||||
<input
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import AppTimeseries from '../components/dataDisplay/AppTimeseries.svelte'
|
||||
import AppHtml from '../components/dataDisplay/AppHtml.svelte'
|
||||
import AppSliderInputs from '../components/numberInputs/AppSliderInputs.svelte'
|
||||
import AppFormButton from '../components/form/AppFormButton.svelte'
|
||||
import VegaLiteHtml from '../components/dataDisplay/VegaLiteHtml.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
@@ -124,6 +125,12 @@
|
||||
bind:componentInput={component.componentInput}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{:else if component.type === 'formbuttoncomponent'}
|
||||
<AppFormButton
|
||||
{...component}
|
||||
bind:componentInput={component.componentInput}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{:else if component.type === 'checkboxcomponent'}
|
||||
<CheckboxComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{:else if component.type === 'textinputcomponent'}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import HiddenComponent from '../components/HiddenComponent.svelte'
|
||||
|
||||
export let policy: Policy
|
||||
|
||||
@@ -176,6 +177,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $app.hiddenInlineScripts}
|
||||
{#each $app.hiddenInlineScripts as script, index}
|
||||
{#if script}
|
||||
<HiddenComponent
|
||||
id={`bg_${index}`}
|
||||
inlineScript={script.inlineScript}
|
||||
name={script.name}
|
||||
bind:fields={script.fields}
|
||||
bind:staticOutputs={$staticOutputs[`bg_${index}`]}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.svlt-grid-shadow) {
|
||||
/* Back shadow */
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import PanelSection from './settingsPanel/common/PanelSection.svelte'
|
||||
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
|
||||
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
|
||||
import TablePanel from './TablePanel.svelte'
|
||||
|
||||
const { selectedComponent, lazyGrid } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { selectedComponent, lazyGrid, app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
</script>
|
||||
|
||||
{#each $lazyGrid as gridItem (gridItem.data.id)}
|
||||
@@ -14,3 +16,16 @@
|
||||
<TablePanel bind:component={gridItem.data} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#each $app?.hiddenInlineScripts ?? [] as script, index (script.name)}
|
||||
{#if $selectedComponent === `bg_${index}`}
|
||||
<PanelSection title={`Background script inputs`}>
|
||||
<InputsSpecsEditor
|
||||
id={`bg_${index}`}
|
||||
shouldCapitalize={false}
|
||||
bind:inputSpecs={script.fields}
|
||||
userInputEnabled={false}
|
||||
/>
|
||||
</PanelSection>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -15,8 +15,13 @@ const inputs: ComponentSet = {
|
||||
type: 'static',
|
||||
value: 'Type...',
|
||||
fieldType: 'text',
|
||||
onlyStatic: true,
|
||||
onlyStatic: true
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'static',
|
||||
value: undefined,
|
||||
fieldType: 'text'
|
||||
}
|
||||
},
|
||||
card: false
|
||||
},
|
||||
@@ -31,8 +36,8 @@ const inputs: ComponentSet = {
|
||||
type: 'static',
|
||||
value: 'Password',
|
||||
fieldType: 'text',
|
||||
onlyStatic: true,
|
||||
},
|
||||
onlyStatic: true
|
||||
}
|
||||
},
|
||||
card: false
|
||||
},
|
||||
@@ -47,8 +52,13 @@ const inputs: ComponentSet = {
|
||||
type: 'static',
|
||||
value: 'Type...',
|
||||
fieldType: 'text',
|
||||
onlyStatic: true,
|
||||
onlyStatic: true
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'static',
|
||||
value: undefined,
|
||||
fieldType: 'number'
|
||||
}
|
||||
},
|
||||
card: false
|
||||
},
|
||||
@@ -63,14 +73,14 @@ const inputs: ComponentSet = {
|
||||
type: 'static',
|
||||
value: 0,
|
||||
fieldType: 'number',
|
||||
onlyStatic: true,
|
||||
onlyStatic: true
|
||||
},
|
||||
max: {
|
||||
type: 'static',
|
||||
value: 42,
|
||||
fieldType: 'number',
|
||||
onlyStatic: true,
|
||||
},
|
||||
onlyStatic: true
|
||||
}
|
||||
},
|
||||
card: false
|
||||
},
|
||||
@@ -84,15 +94,20 @@ const inputs: ComponentSet = {
|
||||
minDate: {
|
||||
type: 'static',
|
||||
value: '',
|
||||
fieldType: 'date',
|
||||
fieldType: 'date'
|
||||
},
|
||||
maxDate: {
|
||||
type: 'static',
|
||||
value: '',
|
||||
fieldType: 'date',
|
||||
fieldType: 'date'
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'static',
|
||||
value: undefined,
|
||||
fieldType: 'date'
|
||||
}
|
||||
},
|
||||
card: false,
|
||||
card: false
|
||||
},
|
||||
{
|
||||
...defaultAlignement,
|
||||
@@ -104,7 +119,12 @@ const inputs: ComponentSet = {
|
||||
label: {
|
||||
type: 'static',
|
||||
value: 'Label',
|
||||
fieldType: 'text',
|
||||
fieldType: 'text'
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'static',
|
||||
value: undefined,
|
||||
fieldType: 'boolean'
|
||||
}
|
||||
},
|
||||
card: false
|
||||
@@ -148,8 +168,7 @@ const buttons: ComponentSet = {
|
||||
type: 'runnable',
|
||||
fieldType: 'any',
|
||||
fields: {},
|
||||
runnable: undefined,
|
||||
|
||||
runnable: undefined
|
||||
},
|
||||
recomputeIds: undefined,
|
||||
configuration: {
|
||||
@@ -189,29 +208,64 @@ const buttons: ComponentSet = {
|
||||
type: 'runnable',
|
||||
fieldType: 'any',
|
||||
fields: {},
|
||||
runnable: undefined,
|
||||
|
||||
runnable: undefined
|
||||
},
|
||||
recomputeIds: undefined,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
value: 'Submit',
|
||||
fieldType: 'text',
|
||||
fieldType: 'text'
|
||||
},
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
value: 'dark',
|
||||
optionValuesKey: 'buttonColorOptions',
|
||||
optionValuesKey: 'buttonColorOptions'
|
||||
},
|
||||
size: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
value: 'xs',
|
||||
onlyStatic: true,
|
||||
optionValuesKey: 'buttonSizeOptions',
|
||||
optionValuesKey: 'buttonSizeOptions'
|
||||
}
|
||||
},
|
||||
|
||||
card: true
|
||||
},
|
||||
{
|
||||
horizontalAlignment: 'center',
|
||||
verticalAlignment: 'center',
|
||||
id: 'formbuttoncomponent',
|
||||
type: 'formbuttoncomponent',
|
||||
componentInput: {
|
||||
type: 'runnable',
|
||||
fieldType: 'any',
|
||||
fields: {},
|
||||
runnable: undefined
|
||||
},
|
||||
recomputeIds: undefined,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
value: 'Open popup',
|
||||
fieldType: 'text'
|
||||
},
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
value: 'dark',
|
||||
optionValuesKey: 'buttonColorOptions'
|
||||
},
|
||||
size: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
value: 'xs',
|
||||
onlyStatic: true,
|
||||
optionValuesKey: 'buttonSizeOptions'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -235,7 +289,7 @@ const display: ComponentSet = {
|
||||
>
|
||||
<h1 class="absolute top-4 left-2 text-white">
|
||||
Hello \${ctx.username}
|
||||
</h1>`,
|
||||
</h1>`
|
||||
},
|
||||
configuration: {},
|
||||
card: false
|
||||
@@ -253,7 +307,7 @@ const display: ComponentSet = {
|
||||
{ a: 'A', b: 28 },
|
||||
{ a: 'B', b: 55 },
|
||||
{ a: 'C', b: 43 },
|
||||
{ a: 'D', b: 91 },
|
||||
{ a: 'D', b: 91 }
|
||||
]
|
||||
},
|
||||
mark: 'bar',
|
||||
@@ -269,9 +323,8 @@ const display: ComponentSet = {
|
||||
onlyStatic: true,
|
||||
fieldType: 'boolean',
|
||||
value: false,
|
||||
tooltip: "use the canvas renderer instead of the svg one for more interactive plots"
|
||||
tooltip: 'use the canvas renderer instead of the svg one for more interactive plots'
|
||||
}
|
||||
|
||||
},
|
||||
card: false
|
||||
},
|
||||
@@ -284,7 +337,7 @@ const display: ComponentSet = {
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'template',
|
||||
value: 'Hello ${ctx.username}',
|
||||
value: 'Hello ${ctx.username}'
|
||||
},
|
||||
configuration: {
|
||||
style: {
|
||||
@@ -298,19 +351,18 @@ const display: ComponentSet = {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
value: '',
|
||||
tooltip: 'CSS rules like "color: blue;"',
|
||||
tooltip: 'CSS rules like "color: blue;"'
|
||||
},
|
||||
copyButton: {
|
||||
type: 'static',
|
||||
value: false,
|
||||
fieldType: 'boolean',
|
||||
onlyStatic: true,
|
||||
},
|
||||
onlyStatic: true
|
||||
}
|
||||
},
|
||||
card: false
|
||||
},
|
||||
{
|
||||
|
||||
id: 'tablecomponent',
|
||||
type: 'tablecomponent',
|
||||
configuration: {
|
||||
@@ -337,8 +389,7 @@ const display: ComponentSet = {
|
||||
name: 'A briefer cell',
|
||||
age: 84
|
||||
}
|
||||
],
|
||||
|
||||
]
|
||||
},
|
||||
card: true,
|
||||
actionButtons: []
|
||||
@@ -385,7 +436,6 @@ const display: ComponentSet = {
|
||||
fieldType: 'boolean',
|
||||
value: false
|
||||
}
|
||||
|
||||
},
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
@@ -409,30 +459,32 @@ const display: ComponentSet = {
|
||||
onlyStatic: true,
|
||||
fieldType: 'boolean',
|
||||
value: false
|
||||
},
|
||||
}
|
||||
},
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'object',
|
||||
value: [{
|
||||
label: 'foo',
|
||||
data: [
|
||||
{ x: 25, y: 50 },
|
||||
{ x: 23, y: 23 },
|
||||
{ x: 12, y: 37 }
|
||||
],
|
||||
backgroundColor: 'rgb(255, 12, 137)'
|
||||
},
|
||||
{
|
||||
label: 'foobar',
|
||||
data: [
|
||||
{ x: 32, y: 32 },
|
||||
{ x: 25, y: 42 },
|
||||
{ x: 3, y: 27 }
|
||||
],
|
||||
backgroundColor: 'orange'
|
||||
}]
|
||||
value: [
|
||||
{
|
||||
label: 'foo',
|
||||
data: [
|
||||
{ x: 25, y: 50 },
|
||||
{ x: 23, y: 23 },
|
||||
{ x: 12, y: 37 }
|
||||
],
|
||||
backgroundColor: 'rgb(255, 12, 137)'
|
||||
},
|
||||
{
|
||||
label: 'foobar',
|
||||
data: [
|
||||
{ x: 32, y: 32 },
|
||||
{ x: 25, y: 42 },
|
||||
{ x: 3, y: 27 }
|
||||
],
|
||||
backgroundColor: 'orange'
|
||||
}
|
||||
]
|
||||
},
|
||||
card: true
|
||||
},
|
||||
@@ -457,40 +509,50 @@ const display: ComponentSet = {
|
||||
onlyStatic: true,
|
||||
fieldType: 'boolean',
|
||||
value: false
|
||||
},
|
||||
}
|
||||
},
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'object',
|
||||
value: [{
|
||||
label: 'foo',
|
||||
data: [{
|
||||
x: '2021-11-06 23:39:30',
|
||||
y: 50
|
||||
}, {
|
||||
x: '2021-11-07 01:00:28',
|
||||
y: 60
|
||||
}, {
|
||||
x: '2021-11-07 09:00:28',
|
||||
y: 20
|
||||
}],
|
||||
backgroundColor: 'rgb(255, 12, 137)'
|
||||
},
|
||||
{
|
||||
label: 'foobar',
|
||||
data: [{
|
||||
x: '2021-11-06 23:39:30',
|
||||
y: 20
|
||||
}, {
|
||||
x: '2021-11-07 01:00:28',
|
||||
y: 13
|
||||
}, {
|
||||
x: '2021-11-07 09:00:28',
|
||||
y: 45
|
||||
}],
|
||||
backgroundColor: 'orange'
|
||||
}]
|
||||
value: [
|
||||
{
|
||||
label: 'foo',
|
||||
data: [
|
||||
{
|
||||
x: '2021-11-06 23:39:30',
|
||||
y: 50
|
||||
},
|
||||
{
|
||||
x: '2021-11-07 01:00:28',
|
||||
y: 60
|
||||
},
|
||||
{
|
||||
x: '2021-11-07 09:00:28',
|
||||
y: 20
|
||||
}
|
||||
],
|
||||
backgroundColor: 'rgb(255, 12, 137)'
|
||||
},
|
||||
{
|
||||
label: 'foobar',
|
||||
data: [
|
||||
{
|
||||
x: '2021-11-06 23:39:30',
|
||||
y: 20
|
||||
},
|
||||
{
|
||||
x: '2021-11-07 01:00:28',
|
||||
y: 13
|
||||
},
|
||||
{
|
||||
x: '2021-11-07 09:00:28',
|
||||
y: 45
|
||||
}
|
||||
],
|
||||
backgroundColor: 'orange'
|
||||
}
|
||||
]
|
||||
},
|
||||
card: true
|
||||
},
|
||||
@@ -500,13 +562,11 @@ const display: ComponentSet = {
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'object',
|
||||
value: { "foo": 42 },
|
||||
|
||||
value: { foo: 42 }
|
||||
},
|
||||
configuration: {},
|
||||
card: false
|
||||
},
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
return displayData[component?.data.type].name
|
||||
} else if (componentId == 'ctx') {
|
||||
return 'Context'
|
||||
} else if (componentId.startsWith('bg_')) {
|
||||
return 'Background'
|
||||
} else {
|
||||
return 'Table action'
|
||||
}
|
||||
|
||||
+3
-2
@@ -82,7 +82,7 @@
|
||||
>
|
||||
Format <Tooltip>Ctrl+S</Tooltip>
|
||||
</Button>
|
||||
{#if id.startsWith('unused-')}
|
||||
{#if id.startsWith('unused-') || id.startsWith('bg_')}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
@@ -91,7 +91,8 @@
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => dispatch('delete')}
|
||||
/>
|
||||
{:else if $runnableComponents[id] != undefined}
|
||||
{/if}
|
||||
{#if $runnableComponents[id] != undefined}
|
||||
<Button
|
||||
loading={runLoading}
|
||||
size="xs"
|
||||
|
||||
+29
-1
@@ -6,8 +6,9 @@
|
||||
import InlineScriptsPanelList from './InlineScriptsPanelList.svelte'
|
||||
import InlineScriptEditorPanel from './InlineScriptEditorPanel.svelte'
|
||||
import InlineScriptEditor from './InlineScriptEditor.svelte'
|
||||
import EmptyInlineScript from './EmptyInlineScript.svelte'
|
||||
|
||||
const { lazyGrid, app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { lazyGrid, app, staticOutputs } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let selectedScriptComponentId: string | undefined = undefined
|
||||
</script>
|
||||
@@ -51,6 +52,33 @@
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each $app?.hiddenInlineScripts ?? [] as hiddenInlineScript, index (index)}
|
||||
{#if `bg_${index}` === selectedScriptComponentId}
|
||||
{#if hiddenInlineScript.inlineScript}
|
||||
<InlineScriptEditor
|
||||
id={`bg_${index}`}
|
||||
bind:inlineScript={hiddenInlineScript.inlineScript}
|
||||
bind:name={hiddenInlineScript.name}
|
||||
on:delete={() => {
|
||||
// remove the script from the array at the index
|
||||
$app.hiddenInlineScripts.splice(index, 1)
|
||||
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
|
||||
|
||||
delete $staticOutputs[`bg_${index}`]
|
||||
$staticOutputs = $staticOutputs
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<EmptyInlineScript
|
||||
id={`b_${index}`}
|
||||
name={hiddenInlineScript.name}
|
||||
on:new={(e) => {
|
||||
hiddenInlineScript.inlineScript = e.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
|
||||
+61
-2
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/common'
|
||||
import { Badge, Button } from '$lib/components/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import { getAllScriptNames } from '../../utils'
|
||||
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
|
||||
import { getAppScripts } from './utils'
|
||||
|
||||
@@ -11,7 +14,7 @@
|
||||
|
||||
function selectInlineScript(id: string) {
|
||||
selectedScriptComponentId = id
|
||||
if (!id.startsWith('unused-')) {
|
||||
if (!id.startsWith('unused-') || !id.startsWith('bg_')) {
|
||||
$selectedComponent = selectedScriptComponentId
|
||||
}
|
||||
}
|
||||
@@ -24,6 +27,29 @@
|
||||
selectedScriptComponentId = $selectedComponent
|
||||
}
|
||||
}
|
||||
|
||||
function createBackgroundScript() {
|
||||
let index = 0
|
||||
let newScriptPath = `Background Script ${index}`
|
||||
|
||||
const names = getAllScriptNames($app)
|
||||
|
||||
// Find a name that is not used by any other inline script
|
||||
while (names.includes(newScriptPath)) {
|
||||
newScriptPath = `Background Script ${++index}`
|
||||
}
|
||||
|
||||
if (!$app.hiddenInlineScripts) {
|
||||
$app.hiddenInlineScripts = []
|
||||
}
|
||||
|
||||
$app.hiddenInlineScripts.push({
|
||||
name: newScriptPath,
|
||||
inlineScript: undefined,
|
||||
fields: {}
|
||||
})
|
||||
$app.hiddenInlineScripts = $app.hiddenInlineScripts
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-full flex flex-col gap-4">
|
||||
@@ -96,4 +122,37 @@
|
||||
{/if}
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Background scripts" smallPadding>
|
||||
<svelte:fragment slot="action">
|
||||
<Button size="xs" color="dark" startIcon={{ icon: faPlus }} on:click={createBackgroundScript}>
|
||||
Add
|
||||
</Button>
|
||||
<Tooltip>
|
||||
Background scripts are triggered upon global refresh or when their input changes. The result
|
||||
of a background script can be shared among many components.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if $app.hiddenInlineScripts?.length > 0}
|
||||
<div class="flex gap-2 flex-col ">
|
||||
{#each $app.hiddenInlineScripts as { name }, index (index)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{classNames(
|
||||
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
|
||||
selectedScriptComponentId === `bg_${index}` ? 'bg-blue-100 text-blue-600' : ''
|
||||
)},"
|
||||
on:click={() => selectInlineScript(`bg_${index}`)}
|
||||
>
|
||||
<span class="text-xs truncate">{name}</span>
|
||||
<Badge color="yellow">Background</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-gray-500">No items</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
{/if}
|
||||
|
||||
<AlignmentEditor bind:component />
|
||||
{#if component.type === 'buttoncomponent' || component.type === 'formcomponent'}
|
||||
{#if component.type === 'buttoncomponent' || component.type === 'formcomponent' || component.type === 'formbuttoncomponent'}
|
||||
<Recompute bind:recomputeIds={component.recomputeIds} ownId={component.id} />
|
||||
{/if}
|
||||
|
||||
|
||||
+2
@@ -20,6 +20,8 @@
|
||||
<input type="number" bind:value={componentInput.value} />
|
||||
{:else if componentInput.fieldType === 'textarea'}
|
||||
<textarea bind:value={componentInput.value} />
|
||||
{:else if componentInput.fieldType === 'date'}
|
||||
<input type="date" bind:value={componentInput.value} />
|
||||
{:else if componentInput.fieldType === 'boolean'}
|
||||
<Toggle bind:checked={componentInput.value} />
|
||||
{:else if componentInput.fieldType === 'select'}
|
||||
|
||||
+3
-26
@@ -10,7 +10,7 @@
|
||||
import type { AppEditorContext, GridItem, InlineScript } from '$lib/components/apps/types'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { loadSchema, schemaToInputsSpec } from '$lib/components/apps/utils'
|
||||
import { getAllScriptNames, loadSchema, schemaToInputsSpec } from '$lib/components/apps/utils'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
|
||||
type Tab = 'hubscripts' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
|
||||
@@ -94,33 +94,10 @@
|
||||
let index = 0
|
||||
let newScriptPath = `Inline Script ${index}`
|
||||
|
||||
const names = $app.grid.reduce((acc, gridItem: GridItem) => {
|
||||
const { componentInput } = gridItem.data
|
||||
|
||||
if (
|
||||
componentInput?.type === 'runnable' &&
|
||||
componentInput?.runnable?.type === 'runnableByName'
|
||||
) {
|
||||
acc.push(componentInput.runnable.name)
|
||||
}
|
||||
|
||||
if (componentInput?.type === 'tablecomponent') {
|
||||
componentInput.actionButtons.forEach((actionButton) => {
|
||||
if (actionButton.componentInput?.type === 'runnable') {
|
||||
if (actionButton.componentInput.runnable?.type === 'runnableByName') {
|
||||
acc.push(actionButton.componentInput.runnable.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as string[])
|
||||
|
||||
const unusedNames = $app.unusedInlineScripts.map((x) => x.name)
|
||||
const names = getAllScriptNames($app)
|
||||
|
||||
// Find a name that is not used by any other inline script
|
||||
while (names.includes(newScriptPath) || unusedNames.includes(newScriptPath)) {
|
||||
while (names.includes(newScriptPath)) {
|
||||
newScriptPath = `Inline Script ${++index}`
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ export type FormComponent = BaseComponent<'formcomponent'> & {
|
||||
recomputeIds: string[] | undefined
|
||||
}
|
||||
|
||||
export type FormButtonComponent = BaseComponent<'formbuttoncomponent'> & {
|
||||
recomputeIds: string[] | undefined
|
||||
}
|
||||
|
||||
export type RunFormComponent = BaseComponent<'runformcomponent'>
|
||||
export type BarChartComponent = BaseComponent<'barchartcomponent'>
|
||||
export type PieChartComponent = BaseComponent<'piechartcomponent'>
|
||||
@@ -60,11 +64,14 @@ export type Aligned = {
|
||||
export interface BaseAppComponent extends Partial<Aligned> {
|
||||
id: ComponentID
|
||||
componentInput: AppInput | undefined
|
||||
configuration: Record<string, (StaticAppInput | ConnectedAppInput | UserAppInput | RowAppInput | EvalAppInput) & {
|
||||
onlyStatic?: boolean,
|
||||
evaluatedValue?: boolean,
|
||||
tooltip?: string
|
||||
}>
|
||||
configuration: Record<
|
||||
string,
|
||||
(StaticAppInput | ConnectedAppInput | UserAppInput | RowAppInput | EvalAppInput) & {
|
||||
onlyStatic?: boolean
|
||||
evaluatedValue?: boolean
|
||||
tooltip?: string
|
||||
}
|
||||
>
|
||||
card: boolean | undefined
|
||||
/**
|
||||
* If `true` then the wrapper will allow items to flow outside of it's borders.
|
||||
@@ -98,6 +105,7 @@ export type AppComponent = BaseAppComponent &
|
||||
| CheckboxComponent
|
||||
| RadioComponent
|
||||
| FormComponent
|
||||
| FormButtonComponent
|
||||
| VegaLiteComponent
|
||||
)
|
||||
|
||||
@@ -132,6 +140,11 @@ export type App = {
|
||||
name: string
|
||||
inlineScript: InlineScript
|
||||
}>
|
||||
hiddenInlineScripts: Array<{
|
||||
name: string
|
||||
inlineScript: InlineScript | undefined
|
||||
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
|
||||
}>
|
||||
}
|
||||
|
||||
export type ConnectingInput = {
|
||||
@@ -143,21 +156,21 @@ export type ConnectingInput = {
|
||||
|
||||
export type AppEditorContext = {
|
||||
worldStore: Writable<World | undefined>
|
||||
staticOutputs: Writable<Record<string, string[]>>,
|
||||
lazyGrid: Writable<GridItem[]>,
|
||||
app: Writable<App>,
|
||||
summary: Writable<string>,
|
||||
staticOutputs: Writable<Record<string, string[]>>
|
||||
lazyGrid: Writable<GridItem[]>
|
||||
app: Writable<App>
|
||||
summary: Writable<string>
|
||||
selectedComponent: Writable<string | undefined>
|
||||
mode: Writable<EditorMode>
|
||||
connectingInput: Writable<ConnectingInput>
|
||||
breakpoint: Writable<EditorBreakpoint>
|
||||
runnableComponents: Writable<Record<string, () => Promise<void>>>
|
||||
staticExporter: Writable<Record<string, () => any>>
|
||||
appPath: string,
|
||||
workspace: string,
|
||||
onchange: (() => void) | undefined,
|
||||
isEditor: boolean,
|
||||
jobs: Writable<{ job: string, component: string }[]>,
|
||||
appPath: string
|
||||
workspace: string
|
||||
onchange: (() => void) | undefined
|
||||
isEditor: boolean
|
||||
jobs: Writable<{ job: string; component: string }[]>
|
||||
noBackend: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,12 @@ import {
|
||||
ToggleLeft,
|
||||
GripHorizontal,
|
||||
Code2,
|
||||
SlidersHorizontal
|
||||
SlidersHorizontal,
|
||||
PlusSquare
|
||||
} from 'lucide-svelte'
|
||||
import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType'
|
||||
import type { Output } from './rx'
|
||||
import type { App, AppComponent } from './types'
|
||||
import type { App, AppComponent, GridItem } from './types'
|
||||
|
||||
export async function loadSchema(
|
||||
workspace: string,
|
||||
@@ -50,7 +51,11 @@ export async function loadSchema(
|
||||
const script = await ScriptService.getHubScriptByPath({
|
||||
path
|
||||
})
|
||||
if (script.schema == undefined || Object.keys(script.schema).length == 0 || typeof script.schema != 'object') {
|
||||
if (
|
||||
script.schema == undefined ||
|
||||
Object.keys(script.schema).length == 0 ||
|
||||
typeof script.schema != 'object'
|
||||
) {
|
||||
script.schema = emptySchema()
|
||||
}
|
||||
|
||||
@@ -59,14 +64,16 @@ export async function loadSchema(
|
||||
}
|
||||
}
|
||||
|
||||
export function schemaToInputsSpec(schema: Schema, defaultUserInput: boolean): 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]
|
||||
|
||||
|
||||
accu[key] = {
|
||||
type: defaultUserInput && !property.format?.startsWith('resource-') ? 'user' : 'static',
|
||||
value: property.default,
|
||||
@@ -74,7 +81,6 @@ export function schemaToInputsSpec(schema: Schema, defaultUserInput: boolean): R
|
||||
format: property.format
|
||||
}
|
||||
|
||||
|
||||
return accu
|
||||
}, {})
|
||||
}
|
||||
@@ -96,6 +102,10 @@ export const displayData: Record<AppComponent['type'], { name: string; icon: any
|
||||
name: 'Form',
|
||||
icon: FormInput
|
||||
},
|
||||
formbuttoncomponent: {
|
||||
name: 'Modal Form',
|
||||
icon: PlusSquare
|
||||
},
|
||||
piechartcomponent: {
|
||||
name: 'Pie Chart',
|
||||
icon: PieChart
|
||||
@@ -239,8 +249,11 @@ export function clearResultAppInput(appInput: ResultAppInput): ResultAppInput {
|
||||
return appInput
|
||||
}
|
||||
|
||||
|
||||
export function toStatic(app: App, staticExporter: Record<string, () => any>, summary: string): { app: App; summary: string } {
|
||||
export function toStatic(
|
||||
app: App,
|
||||
staticExporter: Record<string, () => any>,
|
||||
summary: string
|
||||
): { app: App; summary: string } {
|
||||
const newApp: App = JSON.parse(JSON.stringify(app))
|
||||
newApp.grid.forEach((x) => {
|
||||
let c: AppComponent = x.data
|
||||
@@ -251,16 +264,52 @@ export function toStatic(app: App, staticExporter: Record<string, () => any>, su
|
||||
return { app: newApp, summary }
|
||||
}
|
||||
|
||||
export function buildExtraLib(components: Record<string, Record<string, Output<any>>>, idToExclude: string, hasRows: boolean): string {
|
||||
return Object.entries(components)
|
||||
.filter(([k, v]) => k != idToExclude)
|
||||
.map(([k, v]) => [k, Object.fromEntries(Object.entries(v).map(([k, v]) => [k, v.peak()]))])
|
||||
.map(
|
||||
([k, v]) => `
|
||||
export function buildExtraLib(
|
||||
components: Record<string, Record<string, Output<any>>>,
|
||||
idToExclude: string,
|
||||
hasRows: boolean
|
||||
): string {
|
||||
return (
|
||||
Object.entries(components)
|
||||
.filter(([k, v]) => k != idToExclude)
|
||||
.map(([k, v]) => [k, Object.fromEntries(Object.entries(v).map(([k, v]) => [k, v.peak()]))])
|
||||
.map(
|
||||
([k, v]) => `
|
||||
|
||||
declare const ${k} = ${JSON.stringify(v)};
|
||||
|
||||
`
|
||||
)
|
||||
.join('\n') + (hasRows ? 'declare const row: Record<string, any>;' : '')
|
||||
}
|
||||
)
|
||||
.join('\n') + (hasRows ? 'declare const row: Record<string, any>;' : '')
|
||||
)
|
||||
}
|
||||
|
||||
export function getAllScriptNames(app: App): string[] {
|
||||
const names = app.grid.reduce((acc, gridItem: GridItem) => {
|
||||
const { componentInput } = gridItem.data
|
||||
|
||||
if (
|
||||
componentInput?.type === 'runnable' &&
|
||||
componentInput?.runnable?.type === 'runnableByName'
|
||||
) {
|
||||
acc.push(componentInput.runnable.name)
|
||||
}
|
||||
|
||||
if (componentInput?.type === 'tablecomponent') {
|
||||
componentInput.actionButtons.forEach((actionButton) => {
|
||||
if (actionButton.componentInput?.type === 'runnable') {
|
||||
if (actionButton.componentInput.runnable?.type === 'runnableByName') {
|
||||
acc.push(actionButton.componentInput.runnable.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as string[])
|
||||
|
||||
const unusedNames = app.unusedInlineScripts.map((x) => x.name)
|
||||
const backgroundNames = app.hiddenInlineScripts?.map((x) => x.name) ?? []
|
||||
|
||||
return [...names, ...unusedNames, ...backgroundNames]
|
||||
}
|
||||
|
||||
@@ -28,12 +28,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
function fadeFast(node: HTMLElement) {
|
||||
return fade(node, { duration: 100 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if open}
|
||||
<div transition:fade={{ duration: 100 }} class={'relative z-50'} role="dialog">
|
||||
<div
|
||||
transition:fadeFast|local
|
||||
class={'absolute top-0 bottom-0 left-0 right-0 z-50'}
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
|
||||
|
||||
+4
-4
@@ -48,9 +48,9 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span
|
||||
>Are you sure you want to discard change you have made? (A draft has been temporarily and
|
||||
locally saved)</span
|
||||
>
|
||||
<span>
|
||||
Are you sure you want to discard change you have made? (A draft has been temporarily and
|
||||
locally saved)
|
||||
</span>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Button from '../button/Button.svelte'
|
||||
import Badge from '../badge/Badge.svelte'
|
||||
|
||||
export let title: string
|
||||
export let open: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (open) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
dispatch('confirmed')
|
||||
break
|
||||
case 'Escape':
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
dispatch('canceled')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
function fadeFast(node: HTMLElement) {
|
||||
return fade(node, { duration: 100 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
transition:fadeFast|local
|
||||
class={'absolute top-0 bottom-0 left-0 right-0 z-50'}
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
|
||||
open ? 'ease-out duration-300 opacity-100' : 'ease-in duration-200 opacity-0'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div
|
||||
class={classNames(
|
||||
'relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
|
||||
open
|
||||
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
|
||||
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
)}
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="ml-4 text-left flex-1">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
{title}
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
|
||||
<Button on:click={() => dispatch('canceled')} color="light" size="sm">
|
||||
<span class="gap-2">Cancel <Badge color="dark-gray">Escape</Badge></span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -25,7 +25,8 @@
|
||||
let value: App = {
|
||||
grid: [],
|
||||
fullscreen: false,
|
||||
unusedInlineScripts: []
|
||||
unusedInlineScripts: [],
|
||||
hiddenInlineScripts: []
|
||||
}
|
||||
|
||||
if (nodraft) {
|
||||
|
||||
Reference in New Issue
Block a user