Compare commits

..

12 Commits

Author SHA1 Message Date
Faton Ramadani
4140117b6a Merge branch 'main' into app-preview-scroll 2023-03-07 16:50:59 +01:00
Faton Ramadani
f20ea37eb0 feat(frontend): Fix app preview error display 2023-03-07 16:48:23 +01:00
Faton Ramadani
87d31ee63b Merge branch 'main' into app-preview-scroll 2023-03-07 15:29:24 +01:00
Faton Ramadani
fb2d52f99b feat(frontend): Fix recomputeIds + app table 2023-03-07 15:28:58 +01:00
Faton Ramadani
2e3271257f Merge branch 'main' into app-preview-scroll 2023-03-07 11:42:10 +01:00
Faton Ramadani
b013996585 Merge branch 'main' into app-preview-scroll 2023-03-06 18:49:55 +01:00
Faton Ramadani
0a76e3533c feat(frontend): remove useless softWrap 2023-03-06 15:46:00 +01:00
Faton Ramadani
7648b382ff feat(frontend): remvove useless softWrap 2023-03-06 15:14:23 +01:00
Faton Ramadani
e2747e824f feat(frontend): merge main 2023-03-06 13:59:08 +01:00
Faton Ramadani
2060445d20 feat(frontend): fix text resize 2023-03-06 13:40:07 +01:00
Faton Ramadani
94e30052bf Merge branch 'main' into app-preview-scroll 2023-03-06 08:23:05 +01:00
Faton Ramadani
68add7932e feat(frontend): add a way to automatically resize (wip) + add automatic resizable component 2023-03-06 08:21:54 +01:00
226 changed files with 3103 additions and 5908 deletions

View File

@@ -0,0 +1,20 @@
name: Deploy to windmill.dev
on:
push:
branches: [main]
paths:
- "community/**"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to windmill.dev
uses: windmill-labs/windmill-gh-action-deploy@v2.0.0
with:
dry_run: false
input_dir: community
windmill_workspace: starter
windmill_token: ${{ secrets.WINDMILL_API_TOKEN }}

View File

@@ -109,38 +109,38 @@ jobs:
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
# disabled until we make it 100% reliable and add more meaningful tests
# playwright:
# runs-on: [self-hosted, new]
# needs: [build]
# services:
# postgres:
# image: postgres
# env:
# POSTGRES_DB: windmill
# POSTGRES_USER: admin
# POSTGRES_PASSWORD: changeme
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# steps:
# - uses: actions/checkout@v3
# - name: "Docker"
# run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
# id: docker-container
# - uses: actions/setup-node@v3
# with:
# node-version: 16
# - name: "Playwright run"
# timeout-minutes: 2
# run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
# - name: "Clean up"
# run: docker kill ${{ steps.docker-container.outputs.id }}
# if: always()
playwright:
runs-on: [self-hosted, new]
needs: [build]
services:
postgres:
image: postgres
env:
POSTGRES_DB: windmill
POSTGRES_USER: admin
POSTGRES_PASSWORD: changeme
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: "Docker"
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
id: docker-container
- uses: actions/setup-node@v3
with:
node-version: 16
- name: "Playwright run"
timeout-minutes: 2
run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
- name: "Clean up"
run: docker kill ${{ steps.docker-container.outputs.id }}
if: always()
publish_privately_heavy:

View File

@@ -1,102 +1,6 @@
# Changelog
## [1.76.0](https://github.com/windmill-labs/windmill/compare/v1.75.0...v1.76.0) (2023-03-13)
### Features
* **frontend:** add frontend (JS) scripts to apps ([f0b1b1f](https://github.com/windmill-labs/windmill/commit/f0b1b1f752731ba434b960a75624118152f53c00))
* **frontend:** Copy, Cut and Paste ([#1279](https://github.com/windmill-labs/windmill/issues/1279)) ([82c139e](https://github.com/windmill-labs/windmill/commit/82c139ed0992be401e250cfb7ecc0fca61b76772))
* **frontend:** disabled for action buttons can now depend on row ([75f87e7](https://github.com/windmill-labs/windmill/commit/75f87e7e1117a9c12afcf626379e94b134a9a493))
* **frontend:** improve drag-n-drop behavior ([cfd489a](https://github.com/windmill-labs/windmill/commit/cfd489a55059e7b6843f99bab261c70b3852e6a2))
### Bug Fixes
* **backend:** improve worker ping api ([c958480](https://github.com/windmill-labs/windmill/commit/c958480ce83844a989f58dd5a70eb288582e2194))
* **frontend:** General fixes and updates ([#1281](https://github.com/windmill-labs/windmill/issues/1281)) ([3e5a179](https://github.com/windmill-labs/windmill/commit/3e5a179eb8cd8001f49c92305141dade1571e20f))
## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11)
### Features
* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753))
* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1))
* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337))
* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e))
* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874))
### Bug Fixes
* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b))
* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646))
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)
### Bug Fixes
* **frontend:** fix splitpanes navigation ([#1276](https://github.com/windmill-labs/windmill/issues/1276)) ([8d5c5b8](https://github.com/windmill-labs/windmill/commit/8d5c5b88a35d7a3bad1d8ddf2d940026825241eb))
## [1.74.1](https://github.com/windmill-labs/windmill/compare/v1.74.0...v1.74.1) (2023-03-09)
### Bug Fixes
* **apps:** proper reactivity for non rendered static components ([ae53baf](https://github.com/windmill-labs/windmill/commit/ae53bafaf6777f928113f84b2c6ed6a2ed341844))
* **ci:** make windmill compile again by pinning swc deps ([2ea15d5](https://github.com/windmill-labs/windmill/commit/2ea15d5035e5e15473968db3c0501a4dddff5cd0))
## [1.74.0](https://github.com/windmill-labs/windmill/compare/v1.73.1...v1.74.0) (2023-03-09)
### Features
* add delete by path for scripts ([0c2cf92](https://github.com/windmill-labs/windmill/commit/0c2cf92dd3df9610e649f15e23921a4ca0d94e6a))
* **frontend:** Add color picker input to app ([#1270](https://github.com/windmill-labs/windmill/issues/1270)) ([88e537a](https://github.com/windmill-labs/windmill/commit/88e537ad1fb4c207f38fbe951c82106bef6491a3))
* **frontend:** add expand ([#1268](https://github.com/windmill-labs/windmill/issues/1268)) ([b854ee3](https://github.com/windmill-labs/windmill/commit/b854ee34393534bde104e2e6f606108fd66d38dc))
* **frontend:** add hash to ctx in apps ([b1a45b1](https://github.com/windmill-labs/windmill/commit/b1a45b1e708aa6f19f8be9c949507083e044f2d8))
* **frontend:** Add key navigation in app editor ([#1273](https://github.com/windmill-labs/windmill/issues/1273)) ([6b0fb75](https://github.com/windmill-labs/windmill/commit/6b0fb75d23e2151c88b07814139d203c1bd0578d))
### Bug Fixes
* **cli:** improve visibility of the active workspace ([e6344da](https://github.com/windmill-labs/windmill/commit/e6344dac6d1be04b46231fa8ef8579fd12ca8f37))
* **frontend:** add confirmation modal to delete script/flow/app ([a4adcb5](https://github.com/windmill-labs/windmill/commit/a4adcb5192c11f7bf47a0d259825e474779378d7))
* **frontend:** Clean up app editor ([#1267](https://github.com/windmill-labs/windmill/issues/1267)) ([0a5e181](https://github.com/windmill-labs/windmill/commit/0a5e181a3aa966fb8211bee0d9174fc16353b31f))
* **frontend:** Minor changes ([#1272](https://github.com/windmill-labs/windmill/issues/1272)) ([3b6ae0c](https://github.com/windmill-labs/windmill/commit/3b6ae0cc49461b858d9cfff79eae9a7569465235))
* **frontend:** simplify input bindings ([b2de531](https://github.com/windmill-labs/windmill/commit/b2de531a46e4b120d7106d361b727746bec516dd))
## [1.73.1](https://github.com/windmill-labs/windmill/compare/v1.73.0...v1.73.1) (2023-03-07)
### Bug Fixes
* **frontend:** load flow is not initialized ([719d475](https://github.com/windmill-labs/windmill/commit/719d4752621d462b1cfaa0d27930fba7586be779))
## [1.73.0](https://github.com/windmill-labs/windmill/compare/v1.72.0...v1.73.0) (2023-03-07)
### Features
* **frontend:** add a way to automatically resize ([#1259](https://github.com/windmill-labs/windmill/issues/1259)) ([24f58ef](https://github.com/windmill-labs/windmill/commit/24f58efd9994a2201c1b1d9bbfb11734c57068e3))
* **frontend:** add ability to move nodes ([614fb50](https://github.com/windmill-labs/windmill/commit/614fb5022aa7d5428fb96b7ee3a20794edd1e9d3))
* **frontend:** Add app PDF viewer ([#1254](https://github.com/windmill-labs/windmill/issues/1254)) ([3e5d09e](https://github.com/windmill-labs/windmill/commit/3e5d09ef0b5619186bee5ec6d442cbfd12a6e8d5))
* **frontend:** add fork/save buttons + consistent styling for slider/range ([9e9f8ef](https://github.com/windmill-labs/windmill/commit/9e9f8efb8ee389ea75e99b67ef720756959ca737))
* **frontend:** add history to flows and apps ([9e4d90a](https://github.com/windmill-labs/windmill/commit/9e4d90ad37a57ff1f515eea0c82cf603649e915d))
* **frontend:** Fix object viewer style ([#1255](https://github.com/windmill-labs/windmill/issues/1255)) ([94f1aad](https://github.com/windmill-labs/windmill/commit/94f1aadef2b09ac1962478f11b27cc708b8328f1))
* **frontend:** refactor entire flow builder UX ([2ac51b0](https://github.com/windmill-labs/windmill/commit/2ac51b0af08bdef7ce3c7e874e9983b9fc00478a))
### Bug Fixes
* **frontend:** arginput + apppreview fixes ([e2c4545](https://github.com/windmill-labs/windmill/commit/e2c45452401022b00285b21551ffaf35a114be33))
* **frontend:** fix app map reactivity ([#1260](https://github.com/windmill-labs/windmill/issues/1260)) ([2557e13](https://github.com/windmill-labs/windmill/commit/2557e136bd0df1a023819b7d9b2235e30d7140b6))
* **frontend:** fix branch deletion ([#1261](https://github.com/windmill-labs/windmill/issues/1261)) ([a999eb2](https://github.com/windmill-labs/windmill/commit/a999eb21121a7c0010621448324e0c77caf2b3f6))
* **frontend:** Side menu z-index issue ([#1265](https://github.com/windmill-labs/windmill/issues/1265)) ([c638897](https://github.com/windmill-labs/windmill/commit/c638897fdcd58f55b0929f91641b21a6f9d25ead))
## [1.72.0](https://github.com/windmill-labs/windmill/compare/v1.71.0...v1.72.0) (2023-03-02)

View File

@@ -73,7 +73,7 @@ ARG features=""
COPY --from=planner /windmill/recipe.json recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef cook --release --features "$features" --recipe-path recipe.json
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
@@ -86,7 +86,6 @@ RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$feature
FROM python:3.11.2-slim-buster
ARG TARGETPLATFORM
ARG APP=/usr/src/app
@@ -130,10 +129,6 @@ COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
# docker does not support conditional COPY and we want to use the same Dockerfile for both amd64 and arm64 and privilege the official image
COPY --from=lukechannings/deno:latest /usr/bin/deno /usr/bin/deno-arm
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then rm /usr/bin/deno-arm; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then mv /usr/bin/deno-arm /usr/bin/deno; fi
RUN mkdir -p ${APP}
WORKDIR ${APP}

364
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.76.0"
version = "1.72.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.76.0"
version = "1.72.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -123,8 +123,8 @@ regex = "^1"
deno_core = "^0"
async-recursion = "^1"
swc_common = "^0"
swc_ecma_parser = "0.128.2"
swc_ecma_ast = "0.98.1"
swc_ecma_parser = "^0"
swc_ecma_ast = "^0"
base64 = "0.21.0"
unicode-general-category = "^0"
hmac = "0.12.1"

View File

@@ -16,5 +16,4 @@ unicode-general-category.workspace = true
itertools.workspace = true
anyhow.workspace = true
regex.workspace = true
lazy_static.workspace = true
serde_json.workspace = true
lazy_static.workspace = true

View File

@@ -1,8 +1,6 @@
#![allow(non_snake_case)] // TODO: switch to parse_* function naming
use anyhow::anyhow;
use regex::Regex;
use serde_json::json;
use std::collections::HashMap;
use windmill_parser::{Arg, MainArgSignature, Typ};
@@ -19,32 +17,19 @@ pub fn parse_bash_sig(code: &str) -> windmill_common::error::Result<MainArgSigna
}
}
lazy_static::lazy_static! {
static ref RE: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+):-(.*)\})"$"#).unwrap();
}
fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut hm: HashMap<i32, (String, Option<String>)> = HashMap::new();
for cap in RE.captures_iter(code) {
hm.insert(
cap.get(2)
.or(cap.get(3))
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
(
cap[1].to_string(),
cap.get(4).map(|x| x.as_str().to_string()),
),
);
let mut hm = HashMap::new();
let re = Regex::new(r#"(?m)^(\w+)="\$(\d+)"$"#).unwrap();
for cap in re.captures_iter(code) {
hm.insert(cap[2].parse::<i32>()?, cap[1].to_string());
}
let mut args = vec![];
for i in 1..20 {
if hm.contains_key(&i) {
let (name, default) = hm.get(&i).unwrap();
args.push(Arg {
name: name.clone(),
name: hm[&i].clone(),
typ: Typ::Str(None),
default: default.clone().map(|x| json!(x)),
default: None,
otyp: None,
has_default: false,
});
@@ -58,8 +43,6 @@ fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
@@ -67,7 +50,8 @@ mod tests {
let code = r#"
token="$1"
image="$2"
digest="${3:-latest with spaces}"
digest="${3:-latest}"
foo="$4"
"#;
//println!("{}", serde_json::to_string()?);
@@ -90,13 +74,6 @@ digest="${3:-latest with spaces}"
typ: Typ::Str(None),
default: None,
has_default: false
},
Arg {
otyp: None,
name: "digest".to_string(),
typ: Typ::Str(None),
default: Some(json!("latest with spaces")),
has_default: false
}
]
}

View File

@@ -468,27 +468,6 @@
},
"query": "DELETE FROM workspace_settings WHERE workspace_id = $1"
},
"0e7d95f4913e5775651971d741a3b5c1ef5dfe079be5325abe2866d39a7fe5fb": {
"describe": {
"columns": [
{
"name": "path",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path"
},
"11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d": {
"describe": {
"columns": [],
@@ -2501,57 +2480,6 @@
},
"query": "SELECT set_config('session.user', $1, true)"
},
"6d50a8dc9cfc040b6f37b58053daa0709671e008a10ab4189114ceff66efe603": {
"describe": {
"columns": [
{
"name": "worker",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "worker_instance",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "last_ping",
"ordinal": 2,
"type_info": "Int4"
},
{
"name": "started_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "ip",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "jobs_executed",
"ordinal": 5,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
null,
false,
false,
false
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
}
},
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2"
},
"6dfd1ac7168f098a9e7c92d530a470c0a156041d5081053d43d9efb00d81c464": {
"describe": {
"columns": [
@@ -2573,6 +2501,57 @@
},
"query": "SELECT policy from app WHERE path = $1 AND workspace_id = $2"
},
"6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": {
"describe": {
"columns": [
{
"name": "worker",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "worker_instance",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "ping_at",
"ordinal": 2,
"type_info": "Timestamptz"
},
{
"name": "started_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "ip",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "jobs_executed",
"ordinal": 5,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
false,
false,
false,
false
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
}
},
"query": "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2"
},
"701f215eb14ba67a79afea15d7effc0dd394ba6c4a72c95d4560c5a377015d4e": {
"describe": {
"columns": [],
@@ -2984,6 +2963,20 @@
},
"query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3"
},
"83fc9bf32b9b40987feff61fea82e20613e74efb7bf8ba52485566667ccf2284": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3"
},
"8543f029d9784234e4c6a6dcd7b03e62d544b98be261334ee210594e0bb839f2": {
"describe": {
"columns": [
@@ -3154,21 +3147,6 @@
},
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage"
},
"8c0131a9cc61f2daa258d49767242bcaab6bb34a977ff7fb0c18aa9202d11f47": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Jsonb",
"Text",
"Text"
]
}
},
"query": "UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4"
},
"8c11511a74a41a65f448249a00ebe6964a61d00c2f7b4875a55e64741bf1f0ca": {
"describe": {
"columns": [
@@ -4698,27 +4676,6 @@
},
"query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE"
},
"be1de9116b18a40681cb8c5ec7578fd7c5f7ce77f7af63f766c2899d85fe0bef": {
"describe": {
"columns": [
{
"name": "value",
"ordinal": 0,
"type_info": "Jsonb"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT value FROM resource WHERE path = $1 AND workspace_id = $2"
},
"be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d": {
"describe": {
"columns": [],
@@ -5062,18 +5019,6 @@
},
"query": "\n SELECT id, flow_status, suspend, script_path\n FROM queue\n WHERE id = ( SELECT parent_job FROM queue WHERE id = $1 UNION ALL SELECT parent_job FROM completed_job WHERE id = $1)\n FOR UPDATE\n "
},
"c9d97800eb0ec87df8e8959b283dacb2c6cce422365ed394375641488ceb6b65": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "UPDATE worker_ping SET ping_at = now() WHERE worker = $1"
},
"cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748": {
"describe": {
"columns": [],

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.76.0
version: 1.72.0
title: Windmill API
contact:
@@ -204,6 +204,7 @@ paths:
schema:
type: string
/w/{workspace}/users/is_owner/{path}:
get:
summary: is owner of path
@@ -1548,7 +1549,7 @@ paths:
required: true
content:
application/json:
schema:
schema:
type: object
properties:
value: {}
@@ -2102,6 +2103,7 @@ paths:
items:
type: string
/w/{workspace}/scripts/create:
post:
summary: create script
@@ -2276,7 +2278,7 @@ paths:
/w/{workspace}/scripts/delete/h/{hash}:
post:
summary: delete script by hash (erase content but keep hash, require admin)
summary: delete script by hash (erase content but keep hash)
operationId: deleteScriptByHash
tags:
- script
@@ -2291,23 +2293,6 @@ paths:
schema:
$ref: "#/components/schemas/Script"
/w/{workspace}/scripts/delete/p/{path}:
post:
summary: delete all scripts at a given path (require admin)
operationId: deleteScriptByPath
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script path
content:
application/json:
schema:
type: string
/w/{workspace}/scripts/get/p/{path}:
get:
summary: get script by path
@@ -2833,6 +2818,7 @@ paths:
schema:
$ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/apps/secret_of/{path}:
get:
summary: get public secret of app
@@ -3165,14 +3151,12 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/Suspended"
- $ref: "#/components/parameters/Running"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
responses:
"200":
description: All available queued jobs
@@ -3197,12 +3181,10 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped
description: is the job skipped
in: query
@@ -3236,11 +3218,9 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped
description: is the job skipped
in: query
@@ -3441,7 +3421,7 @@ paths:
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get:
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls
tags:
- job
@@ -3467,7 +3447,7 @@ paths:
properties:
approvalPage:
type: string
resume:
resume:
type: string
cancel:
type: string
@@ -4112,7 +4092,7 @@ paths:
type: string
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4146,7 +4126,7 @@ paths:
properties:
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4317,8 +4297,7 @@ paths:
required: true
schema:
type: string
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
enum: [script, group_, resource, schedule, variable, flow, folder, app]
responses:
"200":
description: acls
@@ -4343,8 +4322,7 @@ paths:
required: true
schema:
type: string
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
enum: [script, group_, resource, schedule, variable, flow, folder, app]
requestBody:
description: acl to add
required: true
@@ -4380,8 +4358,7 @@ paths:
required: true
schema:
type: string
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
enum: [script, group_, resource, schedule, variable, flow, folder, app]
requestBody:
description: acl to add
required: true
@@ -4401,18 +4378,19 @@ paths:
schema:
type: string
/w/{workspace}/capture_u/{path}:
post:
summary: update flow preview capture
operationId: updateCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"204":
description: flow preview captured
post:
summary: update flow preview capture
operationId: updateCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"204":
description: flow preview captured
/w/{workspace}/capture/{path}:
put:
@@ -4619,15 +4597,15 @@ components:
in: query
schema:
type: string
StartedBefore:
name: started_before
CreatedBefore:
name: created_before
description: filter on created before (inclusive) timestamp
in: query
schema:
type: string
format: date-time
StartedAfter:
name: started_after
CreatedAfter:
name: created_after
description: filter on created after (exclusive) timestamp
in: query
schema:
@@ -4651,19 +4629,6 @@ components:
in: query
schema:
type: boolean
ArgsFilter:
name: args
description: filter on jobs containing those args as a json subset (@> in postgres)
in: query
schema:
type: string
ResultFilter:
name: result
description: filter on jobs containing those result as a json subset (@> in postgres)
in: query
schema:
type: string
After:
name: after
description: filter on created after (exclusive) timestamp
@@ -5568,8 +5533,9 @@ components:
type: string
worker_instance:
type: string
last_ping:
type: number
ping_at:
type: string
format: date-time
started_at:
type: string
format: date-time

View File

@@ -313,13 +313,10 @@ async fn create_app(
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(mut app): Json<CreateApp>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
app.policy.on_behalf_of_email = Some(authed.email);
let id = sqlx::query_scalar!(
"INSERT INTO app
(workspace_id, path, summary, policy, versions)
@@ -466,9 +463,7 @@ async fn update_app(
sqlb.set_str("summary", nsummary);
}
if let Some(mut npolicy) = ns.policy {
npolicy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
npolicy.on_behalf_of_email = Some(authed.email);
if let Some(npolicy) = ns.policy {
sqlb.set(
"policy",
&format!(
@@ -733,9 +728,6 @@ fn build_args(
path: String,
args: &Map<String, Value>,
) -> Result<Map<String, Value>> {
// disallow var and res access in args coming from the user for security reasons
args.into_iter()
.try_for_each(|x| disallow_var_res_access(x.1))?;
let static_args = policy
.triggerables
.get(&path)
@@ -756,20 +748,3 @@ fn build_args(
}
Ok(args)
}
fn disallow_var_res_access(args: &serde_json::Value) -> Result<()> {
match args {
Value::Object(v) => v.into_iter().try_for_each(|x| disallow_var_res_access(x.1)),
Value::Array(arr) => arr.into_iter().try_for_each(|v| disallow_var_res_access(v)),
Value::String(s) => {
if s.starts_with("$var:") || s.starts_with("$res:") {
Err(Error::BadRequest(format!(
"For security reasons, variable or resource access is not allowed as dynamic argument"
)))
} else {
Ok(())
}
}
_ => Ok(()),
}
}

View File

@@ -640,6 +640,8 @@ mod tests {
"type": "script",
"path": "test"
},
"stop_after_if": null,
"summary": null
},
{
"id": "b",
@@ -648,12 +650,15 @@ mod tests {
"input_transforms": {},
"type": "rawscript",
"content": "test",
"lock": null,
"path": null,
"language": "deno"
},
"stop_after_if": {
"expr": "foo = 'bar'",
"skip_if_stopped": false
}
},
"summary": null
},
{
"id": "c",
@@ -675,7 +680,8 @@ mod tests {
"stop_after_if": {
"expr": "previous.isEmpty()",
"skip_if_stopped": false,
}
},
"summary": null
}
],
"failure_module": {
@@ -689,7 +695,8 @@ mod tests {
"stop_after_if": {
"expr": "previous.isEmpty()",
"skip_if_stopped": false
}
},
"summary": null
}
});
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));

View File

@@ -16,9 +16,10 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use itertools::Itertools;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error, JsonResult, Result},
error::{self, Error, JsonResult, Result},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
@@ -261,26 +262,13 @@ async fn update_folder(
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(display_name) = ng.display_name {
sqlb.set("display_name", "?".bind(&display_name));
sqlb.set("display_name", display_name);
}
if let Some(owners) = ng.owners {
sqlb.set(
"owners",
"?".bind(&format!(
"{{{}}}",
owners
.iter()
.map(|x| format!("\"{x}\""))
.collect::<Vec<_>>()
.join(","),
)),
);
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
}
if let Some(extra_perms) = ng.extra_perms {
sqlb.set(
"extra_perms",
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
);
sqlb.set_str("extra_perms", extra_perms.to_string());
}
sqlb.returning("*");

View File

@@ -327,15 +327,13 @@ pub struct ListQueueQuery {
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub running: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
pub job_kinds: Option<String>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
}
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -364,11 +362,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj));
}
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(s) = &lq.suspended {
@@ -378,7 +376,6 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
sqlb.and_where_eq("suspend", 0);
}
}
if let Some(jk) = &lq.job_kinds {
sqlb.and_where_in(
"job_kind",
@@ -386,10 +383,6 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
);
}
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
sqlb
}
@@ -462,14 +455,13 @@ async fn list_jobs(
script_path_exact: lq.script_path_exact,
script_hash: lq.script_hash,
created_by: lq.created_by,
started_before: lq.started_before,
started_after: lq.started_after,
created_before: lq.created_before,
created_after: lq.created_after,
running: None,
parent_job: lq.parent_job,
order_desc: Some(true),
job_kinds: lq.job_kinds,
suspended: lq.suspended,
args: lq.args,
},
&[
"'QueuedJob' as typ",
@@ -1144,14 +1136,14 @@ where
struct InPayload {
payload: Option<String>,
}
}
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
}
}
pub async fn run_flow_by_path(
authed: Authed,
@@ -1695,11 +1687,11 @@ fn list_completed_jobs_query(
if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj));
}
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(sk) = &lq.is_skipped {
sqlb.and_where_eq("is_skipped", sk);
@@ -1714,14 +1706,6 @@ fn list_completed_jobs_query(
);
}
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
if let Some(result) = &lq.result {
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
}
sqlb
}
#[derive(Deserialize, Clone)]
@@ -1730,8 +1714,8 @@ pub struct ListCompletedQuery {
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub success: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
@@ -1739,10 +1723,6 @@ pub struct ListCompletedQuery {
pub is_skipped: Option<bool>,
pub is_flow_step: Option<bool>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
// filter by matching a subset of the result using base64 encoded json subset
pub result: Option<String>,
}
async fn list_completed_jobs(

View File

@@ -70,7 +70,6 @@ pub fn workspaced_service() -> Router {
.route("/exists/p/*path", get(exists_script_by_path))
.route("/archive/h/:hash", post(archive_script_by_hash))
.route("/delete/h/:hash", post(delete_script_by_hash))
.route("/delete/p/*path", post(delete_script_by_path))
.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))
@@ -721,46 +720,6 @@ async fn delete_script_by_hash(
Ok(Json(script))
}
async fn delete_script_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
require_admin(authed.is_admin, &authed.username)?;
let script = sqlx::query_scalar!(
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.fetch_one(&db)
.await
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e}")))?;
audit_log(
&mut tx,
&authed.username,
"scripts.delete",
ActionKind::Delete,
&w_id,
Some(&path),
Some([("workspace", w_id.as_str())].into()),
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScriptPath { workspace: w_id, path: path.to_string() },
);
Ok(Json(script))
}
async fn parse_python_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {

View File

@@ -26,7 +26,6 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde_json::Value;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -379,26 +378,9 @@ async fn update_variable(
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
let mut v = sqlx::query_scalar!(
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_one(&mut tx)
.await?;
if let Some(old_v) = v {
v = Some(replace_path(
old_v,
&format!("$var:{path}"),
&format!("$var:{npath}"),
))
}
sqlx::query!(
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4",
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
v,
path,
w_id
)
@@ -437,23 +419,6 @@ async fn update_variable(
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}
fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
match v {
Value::Object(v) => Value::Object(
v.into_iter()
.map(|(k, v)| (k, replace_path(v, path, npath)))
.collect(),
),
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| replace_path(v, path, npath))
.collect(),
),
Value::String(s) if s == path => Value::String(npath.to_owned()),
_ => v,
}
}
pub async fn build_crypt<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,

View File

@@ -37,7 +37,6 @@ pub enum WebhookMessage {
CreateScript { workspace: String, path: String, hash: String },
UpdateScript { workspace: String, path: String, hash: String },
DeleteScript { workspace: String, hash: String },
DeleteScriptPath { workspace: String, path: String },
CreateVariable { workspace: String, path: String },
UpdateVariable { workspace: String, old_path: String, new_path: String },
DeleteVariable { workspace: String, path: String },

View File

@@ -28,7 +28,7 @@ pub fn global_service() -> Router {
struct WorkerPing {
worker: String,
worker_instance: String,
last_ping: Option<i32>,
ping_at: chrono::DateTime<chrono::Utc>,
started_at: chrono::DateTime<chrono::Utc>,
ip: String,
jobs_executed: i32,
@@ -45,7 +45,7 @@ async fn list_worker_pings(
let rows = sqlx::query_as!(
WorkerPing,
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
"SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
per_page as i64,
offset as i64
)

View File

@@ -1073,7 +1073,6 @@ struct ScriptMetadata {
schema: Option<Schema>,
is_template: bool,
lock: Vec<String>,
kind: String,
}
enum ArchiveImpl {
@@ -1236,7 +1235,6 @@ async fn tarball_workspace(
description: script.description,
schema: script.schema,
is_template: script.is_template,
kind: script.kind.to_string(),
lock,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();

View File

@@ -7,7 +7,7 @@
*/
use std::{
fmt::{self, Display},
fmt::Display,
hash::{Hash, Hasher},
};
@@ -103,18 +103,6 @@ pub enum ScriptKind {
Approval,
}
impl Display for ScriptKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match self {
ScriptKind::Trigger => "trigger",
ScriptKind::Failure => "failure",
ScriptKind::Script => "script",
ScriptKind::Approval => "approval",
})?;
Ok(())
}
}
#[derive(Serialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Script {

View File

@@ -34,7 +34,7 @@ use tokio::{
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
process::{Child, Command},
sync::{
mpsc::{self, Sender}, watch, broadcast,
mpsc::{self, Sender}, watch,
},
time::{interval, sleep, Instant, MissedTickBehavior},
};
@@ -264,7 +264,7 @@ const ROOT_CACHE_DIR: &str = "/tmp/windmill/cache/";
const PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "pip");
const DENO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "deno");
const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
const NUM_SECS_PING: u64 = 5;
const NUM_SECS_ENV_CHECK: u64 = 15;
const NUM_SECS_SYNC: u64 = 60 * 10;
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
@@ -553,7 +553,7 @@ pub async fn run_worker(
let do_break = async {
if last_ping.elapsed().as_secs() > NUM_SECS_PING {
if last_ping.elapsed().as_secs() > NUM_SECS_ENV_CHECK {
sqlx::query!(
"UPDATE worker_ping SET ping_at = now(), jobs_executed = $1 WHERE worker = $2",
jobs_executed,
@@ -840,7 +840,7 @@ async fn handle_queued_job(
job_dir: &str,
metrics: Metrics,
same_worker_tx: Sender<Uuid>,
base_internal_url: &str,
base_internal_url: &str
) -> windmill_common::error::Result<()> {
if job.canceled {
return Err(Error::JsonErr(canceled_job_to_result(&job)))?;
@@ -888,10 +888,10 @@ async fn handle_queued_job(
logs.push_str(&format!("job {} on worker {}\n", &job.id, &worker_name));
let result = match job.job_kind {
JobKind::Dependencies => {
handle_dependency_job(&job, &mut logs, job_dir, db, worker_name).await
handle_dependency_job(&job, &mut logs, job_dir, db).await
}
JobKind::FlowDependencies => {
handle_flow_dependency_job(&job, &mut logs, job_dir, db, worker_name)
handle_flow_dependency_job(&job, &mut logs, job_dir, db)
.await
.map(|()| Value::Null)
}
@@ -912,8 +912,7 @@ async fn handle_queued_job(
job_dir,
worker_dir,
&mut logs,
base_internal_url,
worker_name
base_internal_url
)
.await
}
@@ -1065,9 +1064,7 @@ async fn handle_code_execution_job(
job_dir: &str,
worker_dir: &str,
logs: &mut String,
base_internal_url: &str,
worker_name: &str
base_internal_url: &str
) -> error::Result<serde_json::Value> {
let (inner_content, requirements_o, language) = match job.job_kind {
JobKind::Preview | JobKind::Script_Hub => (
@@ -1089,6 +1086,7 @@ async fn handle_code_execution_job(
"handle_code_execution_job should never be reachable with a non-code execution job"
),
};
let worker_name = worker_dir.split("/").last().unwrap_or("unknown");
let lang_str = job
.language
.as_ref()
@@ -1138,7 +1136,7 @@ mount {{
token,
&inner_content,
&shared_mount,
base_internal_url,
base_internal_url
)
.await
}
@@ -1153,8 +1151,7 @@ mount {{
&inner_content,
&shared_mount,
requirements_o,
base_internal_url,
worker_name
base_internal_url
)
.await
}
@@ -1169,8 +1166,7 @@ mount {{
job_dir,
requirements_o,
&shared_mount,
base_internal_url,
worker_name
base_internal_url
)
.await
}
@@ -1183,8 +1179,7 @@ mount {{
&inner_content,
job_dir,
&shared_mount,
base_internal_url,
worker_name
base_internal_url
)
.await
}
@@ -1213,7 +1208,6 @@ async fn handle_go_job(
requirements_o: Option<String>,
shared_mount: &str,
base_internal_url: &str,
worker_name: &str,
) -> Result<serde_json::Value, Error> {
//go does not like executing modules at temp root
let job_dir = &format!("{job_dir}/go");
@@ -1242,7 +1236,6 @@ async fn handle_go_job(
db,
true,
skip_go_mod,
worker_name
)
.await?;
@@ -1360,7 +1353,7 @@ func Run(req Req) (interface{{}}, error){{
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(&job.id, db, logs, build_go, false, worker_name).await?;
handle_child(&job.id, db, logs, build_go, false).await?;
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
@@ -1386,7 +1379,7 @@ func Run(req Req) (interface{{}}, error){{
.stderr(Stdio::piped())
.spawn()?
};
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
read_result(job_dir).await
}
@@ -1400,7 +1393,6 @@ async fn handle_bash_job(
job_dir: &str,
shared_mount: &str,
base_internal_url: &str,
worker_name: &str,
) -> Result<serde_json::Value, Error> {
logs.push_str("\n\n--- BASH CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
@@ -1464,7 +1456,7 @@ async fn handle_bash_job(
.stderr(Stdio::piped())
.spawn()?
};
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
//for now bash jobs have an empty result object
Ok(serde_json::json!(logs
.lines()
@@ -1492,8 +1484,7 @@ async fn handle_deno_job(
inner_content: &String,
shared_mount: &str,
lockfile: Option<String>,
base_internal_url: &str,
worker_name: &str
base_internal_url: &str
) -> error::Result<serde_json::Value> {
logs.push_str("\n\n--- DENO CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
@@ -1628,7 +1619,7 @@ run().catch(async (e) => {{
}
.instrument(trace_span!("create_deno_jail"))
.await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
read_result(job_dir).await
}
@@ -1666,7 +1657,7 @@ async fn handle_python_job(
token: String,
inner_content: &String,
shared_mount: &str,
base_internal_url: &str,
base_internal_url: &str
) -> error::Result<serde_json::Value> {
create_dependencies_dir(job_dir).await;
@@ -1680,7 +1671,7 @@ async fn handle_python_job(
if requirements.is_empty() {
"".to_string()
} else {
pip_compile(&job.id, &requirements, logs, job_dir, db, worker_name)
pip_compile(&job.id, &requirements, logs, job_dir, db)
.await
.map_err(|e| {
Error::ExecutionErr(format!("pip compile failed: {}", e.to_string()))
@@ -1897,7 +1888,7 @@ mount {{
.spawn()?
};
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
read_result(job_dir).await
}
@@ -1927,7 +1918,6 @@ async fn handle_dependency_job(
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
) -> error::Result<serde_json::Value> {
let content = capture_dependency_job(
&job.id,
@@ -1942,8 +1932,7 @@ async fn handle_dependency_job(
.unwrap_or_else(|| "no raw code"),
logs,
job_dir,
db,
worker_name
db
)
.await;
match content {
@@ -1978,7 +1967,6 @@ async fn handle_flow_dependency_job(
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
) -> error::Result<()> {
let path = job.script_path.clone().ok_or_else(|| {
error::Error::InternalErr(
@@ -2009,7 +1997,6 @@ async fn handle_flow_dependency_job(
logs,
job_dir,
db,
worker_name
)
.await;
match new_lock {
@@ -2124,13 +2111,12 @@ async fn capture_dependency_job(
job_raw_code: &str,
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str
db: &sqlx::Pool<sqlx::Postgres>
) -> error::Result<String> {
match job_language {
ScriptLang::Python3 => {
create_dependencies_dir(job_dir).await;
pip_compile(job_id, job_raw_code, logs, job_dir, db, worker_name).await
pip_compile(job_id, job_raw_code, logs, job_dir, db ).await
}
ScriptLang::Go => {
install_go_dependencies(
@@ -2141,7 +2127,6 @@ async fn capture_dependency_job(
db,
false,
false,
worker_name
)
.await
}
@@ -2159,7 +2144,6 @@ async fn pip_compile(
logs: &mut String,
job_dir: &str,
db: &Pool<Postgres>,
worker_name: &str
) -> error::Result<String> {
logs.push_str(&format!("\nresolving dependencies..."));
set_logs(logs, job_id, db).await;
@@ -2192,7 +2176,7 @@ async fn pip_compile(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false, worker_name)
handle_child(job_id, db, logs, child, false)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
let path_lock = format!("{job_dir}/requirements.txt");
@@ -2215,7 +2199,6 @@ async fn install_go_dependencies(
db: &sqlx::Pool<sqlx::Postgres>,
preview: bool,
skip_go_mod: bool,
worker_name: &str
) -> error::Result<String> {
if !skip_go_mod {
gen_go_mymod(code, job_dir).await?;
@@ -2226,7 +2209,7 @@ async fn install_go_dependencies(
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false, worker_name).await?;
handle_child(job_id, db, logs, child, false).await?;
}
let child = Command::new(GO_PATH.as_str())
.current_dir(job_dir)
@@ -2234,7 +2217,7 @@ async fn install_go_dependencies(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false, worker_name)
handle_child(job_id, db, logs, child, false)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
@@ -2340,7 +2323,6 @@ async fn handle_child(
logs: &mut String,
mut child: Child,
nsjail: bool,
worker_name: &str,
) -> error::Result<()> {
let update_job_interval = Duration::from_millis(500);
let write_logs_delay = Duration::from_millis(500);
@@ -2354,14 +2336,11 @@ async fn handle_child(
tracing::info!("could not get child pid");
}
let (set_too_many_logs, mut too_many_logs) = watch::channel::<bool>(false);
let (tx, mut rx) = broadcast::channel::<()>(3);
let mut rx2 = tx.subscribe();
let output = child_joined_output_stream(&mut child);
let job_id = job_id.clone();
let (tx, mut rx) = mpsc::channel::<()>(1);
/* the cancellation future is polled on by `wait_on_child` while
* waiting for the child to exit normally */
@@ -2371,22 +2350,10 @@ async fn handle_child(
let mut interval = interval(update_job_interval);
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut i = 1;
loop {
tokio::select!(
_ = rx.recv() => break,
_ = interval.tick() => {
// update the last_ping column every 5 seconds
i+=1;
if i % 10 == 0 {
sqlx::query!(
"UPDATE worker_ping SET ping_at = now() WHERE worker = $1",
&worker_name
)
.execute(&db)
.await
.expect("update worker ping");
}
let mem_peak = get_mem_peak(pid, nsjail).await;
tracing::info!("{job_id} still running. mem peak: {}kB", mem_peak);
let mem_peak = if mem_peak > 0 { Some(mem_peak) } else { None };
@@ -2420,10 +2387,10 @@ async fn handle_child(
biased;
result = child.wait() => return result.map(Ok),
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
_ = sleep(*TIMEOUT_DURATION) => KillReason::Timeout,
_ = update_job => KillReason::Cancelled,
_ = sleep(*TIMEOUT_DURATION) => KillReason::Timeout,
};
tx.send(()).expect("rx should never be dropped");
tx.send(()).await.expect("rx should never be dropped");
drop(tx);
let set_reason = async {
@@ -2446,7 +2413,7 @@ async fn handle_child(
}
}
};
/* send SIGKILL and reap child process */
let (_, kill) = future::join(set_reason, child.kill()).await;
kill.map(|()| Err(kill_reason))
@@ -2462,13 +2429,12 @@ async fn handle_child(
/* log_remaining is zero when output limit was reached */
let mut log_remaining = max_log_size.saturating_sub(logs.chars().count());
let mut result = io::Result::Ok(());
let mut output = output.take_until(rx2.recv()).boxed();
let mut output = output;
/* `do_write` resolves the task, but does not contain the Result.
* It's useful to know if the task completed. */
let (mut do_write, mut write_result) = tokio::spawn(ready(())).remote_handle();
while let Some(line) = output.by_ref().next().await {
while let Some(line) = output.by_ref().next().await {
let do_write_ = do_write.shared();
let mut read_lines = stream::once(async { line })
@@ -2483,14 +2449,11 @@ async fn handle_child(
let mut joined = String::new();
while let Some(line) = read_lines.next().await {
match line {
Ok(_) if log_remaining == 0 => (),
Ok(line) => {
if line.is_empty() {
continue;
}
append_with_limit(&mut joined, &line, &mut log_remaining);
if log_remaining == 0 {
tracing::info!(%job_id, "Too many logs lines for job {job_id}");
let _ = set_too_many_logs.send(true);
@@ -2510,7 +2473,6 @@ async fn handle_child(
logs.push_str(&joined);
/* Ensure the last flush completed before starting a new one.
*
* This shouldn't pause since `take_until()` reads lines until `do_write`
@@ -2527,7 +2489,8 @@ async fn handle_child(
panic::resume_unwind(p);
}
(do_write, write_result) = tokio::spawn(append_logs(job_id, joined, db.clone())).remote_handle();
(do_write, write_result) =
tokio::spawn(append_logs(job_id, joined, db.clone())).remote_handle();
if let Err(err) = result {
tracing::error!(%job_id, %err, "error reading output for job {job_id}: {err}");
@@ -2537,7 +2500,6 @@ async fn handle_child(
if *set_too_many_logs.borrow() {
break;
}
}
/* drop our end of the pipe */
@@ -2851,7 +2813,7 @@ async fn handle_python_reqs(
.spawn()?
};
let child = handle_child(&job.id, db, logs, child, false, worker_name).await;
let child = handle_child(&job.id, db, logs, child, false).await;
tracing::info!(
worker_name = %worker_name,
job_id = %job.id,

View File

@@ -28,18 +28,14 @@ export class AppFile implements Resource, PushDiffs {
remotePath: string,
diffs: Difference[],
): Promise<void> {
let app: AppWithLastVersion | undefined = undefined;
try {
app = await AppService.getAppByPath({ workspace, path: remotePath });
} catch (e) {}
if (app) {
if (await AppService.existsApp({ workspace, path: remotePath })) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing app... ${remotePath}`,
`Applying ${diffs.length} diffs to existing app...`,
),
);
const changeset: {
path?: string | undefined;
summary?: string | undefined;
value?: any;
policy?: Policy | undefined;
@@ -50,7 +46,7 @@ export class AppFile implements Resource, PushDiffs {
(
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
diff.path.length !== 1 ||
!["summary"].includes(
!["path", "summary"].includes(
diff.path[0] as string,
)
)
@@ -65,13 +61,6 @@ export class AppFile implements Resource, PushDiffs {
}
}
if ((!changeset?.policy || JSON.stringify(changeset?.policy) == JSON.stringify(app.policy))
&& (!changeset?.value || JSON.stringify(changeset?.value) == JSON.stringify(app.value))
&& (!changeset?.summary || changeset.summary == app.summary)) {
console.log(colors.yellow(`No changes to push for app ${remotePath}, skipping`))
return;
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
@@ -98,10 +87,19 @@ export class AppFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: AppWithLastVersion | undefined;
try {
existing = await AppService.getAppByPath({
workspace: workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false }),
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
}

View File

@@ -18,7 +18,7 @@ export type Context = {
};
async function tryResolveWorkspace(
opts: GlobalOptions
opts: GlobalOptions,
): Promise<
{ isError: false; value: Workspace } | { isError: true; error: string }
> {
@@ -49,20 +49,17 @@ async function tryResolveWorkspace(
}
export async function resolveWorkspace(
opts: GlobalOptions
opts: GlobalOptions,
): Promise<Workspace> {
const res = await tryResolveWorkspace(opts);
if (res.isError) {
console.log(colors.red.bold(res.error));
return Deno.exit(-1);
} else {
return res.value;
}
}
export async function requireLogin(
opts: GlobalOptions
): Promise<GlobalUserInfo> {
export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo> {
const workspace = await resolveWorkspace(opts);
let token = await tryGetLoginInfo(opts);
@@ -76,7 +73,7 @@ export async function requireLogin(
return await UserService.globalWhoami();
} catch {
console.log(
"! Could not reach API given existing credentials. Attempting to reauth..."
"! Could not reach API given existing credentials. Attempting to reauth...",
);
const newToken = await loginInteractive(workspace.remote);
if (!newToken) {
@@ -88,14 +85,14 @@ export async function requireLogin(
setClient(
token,
workspace.remote.substring(0, workspace.remote.length - 1)
workspace.remote.substring(0, workspace.remote.length - 1),
);
return await UserService.globalWhoami();
}
}
export async function tryResolveVersion(
opts: GlobalOptions
opts: GlobalOptions,
): Promise<number | undefined> {
if ((opts as any).__cache_version) {
return (opts as any).__cache_version;
@@ -105,12 +102,12 @@ export async function tryResolveVersion(
if (workspaceRes.isError) return undefined;
const response = await fetch(
new URL(new URL(workspaceRes.value.remote).origin + "/api/version")
new URL(new URL(workspaceRes.value.remote).origin + "/api/version"),
);
const version = await response.text();
try {
return Number.parseInt(
version.split("-", 1)[0].replaceAll(".", "").replace("v", "")
version.split("-", 1)[0].replaceAll(".", "").replace("v", ""),
);
} catch {
return undefined;
@@ -119,7 +116,7 @@ export async function tryResolveVersion(
export async function validatePath(
opts: GlobalOptions,
path: string
path: string,
): Promise<boolean> {
const backendVersion = await tryResolveVersion(opts);
if (path.startsWith("f")) {
@@ -127,16 +124,19 @@ export async function validatePath(
return true;
}
console.log(
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`,
);
return false;
}
if (!(path.startsWith("g") || path.startsWith("u"))) {
if (
!(path.startsWith("g") ||
path.startsWith("u"))
) {
console.log(
colors.red(
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/..."
)
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/...",
),
);
return false;
}

View File

@@ -94,7 +94,6 @@ export class FlowFile implements Resource, PushDiffs {
...changeset,
...base_changeset,
}
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
@@ -115,11 +114,20 @@ export class FlowFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let remote: Flow | undefined;
try {
remote = await FlowService.getFlowByPath({
workspace,
path: remotePath,
});
} catch {
remote = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false }),
microdiff(remote ?? {}, this, { cyclesFix: false }),
);
}
}

View File

@@ -1,4 +1,4 @@
import { colors, Command, FolderService, microdiff } from "./deps.ts";
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
@@ -22,9 +22,7 @@ export class FolderFile implements Resource, PushDiffs {
owners: Array<string> | undefined;
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
extra_perms: Map<string, boolean> | undefined;
@property(() => String)
display_name: string| undefined;
async push(workspace: string, remotePath: string): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
@@ -33,10 +31,16 @@ export class FolderFile implements Resource, PushDiffs {
remotePath = remotePath.substring(2);
}
let existing: Folder | undefined;
try {
existing = await FolderService.getFolder({ workspace, name: remotePath });
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false }),
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
@@ -62,24 +66,22 @@ export class FolderFile implements Resource, PushDiffs {
if (exists) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`,
`Applying ${diffs.length} diffs to existing folder...`,
),
);
const changeset: {
owners?: string[] | undefined;
extra_perms?: any;
display_name?: string | undefined;
} = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["owners", "extra_perms", "display_name"].includes(diff.path[0] as string)
!["owners", "extra_perms"].includes(diff.path[0] as string)
)
) {
console.log(diff.path)
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -95,10 +97,11 @@ export class FolderFile implements Resource, PushDiffs {
if (!hasChanges) {
return;
}
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined}
requestBody: changeset,
});
} else {
console.log(colors.bold.yellow("Creating new folder: " + remotePath));

View File

@@ -13,7 +13,7 @@ import sync from "./sync.ts";
import { tryResolveVersion } from "./context.ts";
import { GlobalOptions } from "./types.ts";
const VERSION = "v1.76.0";
const VERSION = "v1.72.0";
let command: any = new Command()
.name("wmill")

View File

@@ -13,6 +13,7 @@ import {
EditResourceType,
microdiff,
ResourceService,
ResourceType,
Table,
} from "./deps.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
@@ -25,10 +26,19 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
description?: string;
async push(workspace: string, remotePath: string): Promise<void> {
await this.pushDiffs(
let existing: ResourceType | undefined;
try {
existing = await ResourceService.getResourceType({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false }),
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
@@ -55,8 +65,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
return;
}
console.log(
colors.yellow.bold(
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
colors.yellow(
`Applying ${diffs.length} diffs to existing resource type...`,
),
);
const changeset: EditResourceType = {};

View File

@@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs {
})
) {
console.log(
colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`),
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
);
const changeset: EditResource = {
@@ -51,7 +51,11 @@ export class ResourceFile implements Resource2, PushDiffs {
};
for (const diff of diffs) {
if (diff.path[0] === "is_oauth") {
//is_oauth is not updatable
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
continue;
}
if (
@@ -60,10 +64,9 @@ export class ResourceFile implements Resource2, PushDiffs {
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
diff.path[0] !== "description"
) && diff.path[0] !== "resource_type"
)
)
) {
console.log(colors.red("Invalid variable diff with path " + diff.path));
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -107,10 +110,19 @@ export class ResourceFile implements Resource2, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: Resource | undefined;
try {
existing = await ResourceService.getResource({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false }),
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
}

View File

@@ -42,7 +42,6 @@ export class ScriptFile {
},
toPlain: (data) => data,
})
@property(() => String)
kind?: "script" | "failure" | "trigger" | "command" | "approval";
constructor(summary: string, description: string) {
@@ -106,21 +105,11 @@ export async function handleFile(path: string, content: string, workspace: strin
} catch { }
const language = inferContentTypeFromFilePath(path);
let remote = undefined
try {
remote = await ScriptService.getScriptByPath({
const remote = await ScriptService.getScriptByPath({
workspace,
path: remotePath,
});
} catch { }
if (remote) {
if (typed.description === remote.description && content === remote.content && typed.summary === remote.summary && typed.is_template === remote.is_template && typed.kind == remote.kind && remote?.lock == typed.lock?.join('\n') && JSON.stringify(typed.schema) == JSON.stringify(remote.schema)) {
console.log(colors.yellow(`No change to push for script ${remotePath}, skipping`))
return true
}
await ScriptService.createScript({
workspace,
requestBody: {
@@ -134,11 +123,10 @@ export async function handleFile(path: string, content: string, workspace: strin
lock: typed.lock,
parent_hash: remote.hash,
schema: typed.schema,
}
},
});
console.log(colors.yellow.bold(`Creating script with a parent ${remotePath}`))
} else {
} catch {
// no parent hash
await ScriptService.createScript({
workspace: workspace,
@@ -156,6 +144,7 @@ export async function handleFile(path: string, content: string, workspace: strin
},
});
console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`))
}
return true
}

View File

@@ -13,7 +13,7 @@ import {
ResourceService,
VariableService,
AppService,
FlowService,
FlowService
} from "./deps.ts";
import {
Difference,
@@ -25,13 +25,16 @@ import {
import { downloadZip } from "./pull.ts";
import { FolderFile } from "./folder.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import { handleScriptMetadata, ScriptFile } from "./script.ts";
import {
handleScriptMetadata,
ScriptFile,
} from "./script.ts";
import { ResourceFile } from "./resource.ts";
import { FlowFile } from "./flow.ts";
import { VariableFile } from "./variable.ts";
import { handleFile } from "./script.ts";
import { equal } from "https://deno.land/x/equal@v1.5.0/mod.ts";
import { diffCharacters } from "https://deno.land/x/diff@v0.3.5/mod.ts";
import { equal } from "https://deno.land/x/equal/mod.ts";
import { diffCharacters } from "https://deno.land/x/diff/mod.ts";
type DynFSElement = {
isDirectory: boolean;
path: string;
@@ -41,7 +44,10 @@ type DynFSElement = {
};
async function FSFSElement(p: string): Promise<DynFSElement> {
function _internal_element(localP: string, isDir: boolean): DynFSElement {
function _internal_element(
localP: string,
isDir: boolean,
): DynFSElement {
return {
isDirectory: isDir,
path: localP.substring(p.length + 1),
@@ -107,14 +113,16 @@ function ZipFSElement(zip: JSZip): DynFSElement {
async function* readDirRecursiveWithIgnore(
ignore: (path: string, isDirectory: boolean) => boolean,
root: DynFSElement
): AsyncGenerator<{
path: string;
ignored: boolean;
isDirectory: boolean;
getContentBytes(): Promise<Uint8Array>;
getContentText(): Promise<string>;
}> {
root: DynFSElement,
): AsyncGenerator<
{
path: string;
ignored: boolean;
isDirectory: boolean;
getContentBytes(): Promise<Uint8Array>;
getContentText(): Promise<string>;
}
> {
const stack: {
path: string;
isDirectory: boolean;
@@ -122,20 +130,18 @@ async function* readDirRecursiveWithIgnore(
c(): AsyncIterable<DynFSElement>;
getContentBytes(): Promise<Uint8Array>;
getContentText(): Promise<string>;
}[] = [
{
path: root.path,
ignored: ignore(root.path, root.isDirectory),
isDirectory: root.isDirectory,
c: root.getChildren,
getContentBytes(): Promise<Uint8Array> {
throw undefined;
},
getContentText(): Promise<string> {
throw undefined;
},
}[] = [{
path: root.path,
ignored: ignore(root.path, root.isDirectory),
isDirectory: root.isDirectory,
c: root.getChildren,
getContentBytes(): Promise<Uint8Array> {
throw undefined;
},
];
getContentText(): Promise<string> {
throw undefined;
},
}];
while (stack.length > 0) {
const e = stack.pop()!;
@@ -155,17 +161,17 @@ async function* readDirRecursiveWithIgnore(
}
type Added = { name: "added"; path: string; content: string };
type Deleted = { name: "deleted"; path: string };
type Edit = { name: "edited"; path: string; before: string; after: string };
type Deleted = { name: "deleted"; path: string; };
type Edit = { name: "edited"; path: string; before: string; after: string; };
type Change = Added | Deleted | Edit;
async function elementsToMap(
els: DynFSElement,
ignore: (path: string, isDirectory: boolean) => boolean
): Promise<{ [key: string]: string }> {
async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirectory: boolean) => boolean): Promise<{ [key: string]: string }> {
const map: { [key: string]: string } = {};
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
for await (const entry of readDirRecursiveWithIgnore(
ignore,
els,
)) {
if (entry.isDirectory || entry.ignored) continue;
const content = await entry.getContentText();
map[entry.path] = content;
@@ -173,26 +179,20 @@ async function elementsToMap(
return map;
}
async function compareDynFSElement(
els1: DynFSElement,
els2: DynFSElement | undefined,
ignore: (path: string, isDirectory: boolean) => boolean
els1: DynFSElement, els2: DynFSElement,
ignore: (path: string, isDirectory: boolean) => boolean,
raw: boolean
): Promise<Change[]> {
const [m1, m2] = els2
? await Promise.all([
elementsToMap(els1, ignore),
elementsToMap(els2, ignore),
])
: [await elementsToMap(els1, ignore), {}];
const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] :
await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]);
const changes: Change[] = [];
for (const [k, v] of Object.entries(m1)) {
if (m2[k] === undefined) {
changes.push({ name: "added", path: k, content: v });
} else if (
m2[k] != v &&
(!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))
) {
} else if (m2[k] != v && (!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))) {
// await Deno.writeTextFile("/tmp/k", m2[k])
// await Deno.writeTextFile("/tmp/v", v)
// console.log(k)
@@ -208,54 +208,54 @@ async function compareDynFSElement(
}
}
return changes;
return changes
}
const isNotWmillFile = (p: string, isDirectory: boolean) => {
if (p.endsWith("/")) {
return false;
return false
}
if (isDirectory) {
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
}
try {
const typ = getTypeStrFromPath(p);
if (typ == "resource-type") {
return p.includes("/");
const typ = getTypeStrFromPath(p)
if (typ == 'resource-type') {
return p.includes('/')
} else {
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
}
} catch {
return true;
return true
}
};
}
const isWhitelisted = (p: string) => {
return p == "./" || p == "" || p == "u" || p == "f" || p == "g";
};
return p == "./" || p == "" || p == "u" || p == "f" || p == "g"
}
async function ignoreF() {
try {
const ignore: {
accepts(file: string): boolean;
denies(file: string): boolean;
} = gitignore_parser.compile(await Deno.readTextFile(".wmillignore"));
} = gitignore_parser.compile(
await Deno.readTextFile(".wmillignore"),
);
return (p: string, isDirectory: boolean) => {
return (
!isWhitelisted(p) &&
(isNotWmillFile(p, isDirectory) || ignore.denies(p))
);
};
return !isWhitelisted(p) && (isNotWmillFile(p, isDirectory) || ignore.denies(p));
}
} catch (e) {
return (p: string, isDirectory: boolean) =>
!isWhitelisted(p) && isNotWmillFile(p, isDirectory);
return (p: string, isDirectory: boolean) => !isWhitelisted(p) && isNotWmillFile(p, isDirectory)
}
}
async function pull(
opts: GlobalOptions & { raw: boolean; yes: boolean; failConflicts: boolean }
opts: GlobalOptions & { raw: boolean; yes: boolean, failConflicts: boolean },
) {
if (!opts.raw) {
await ensureDir(path.join(Deno.cwd(), ".wmill"));
}
@@ -263,96 +263,74 @@ async function pull(
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log(
colors.gray(
"Computing the files to update locally to match remote (taking .wmillignore into account)"
)
);
const remote = ZipFSElement((await downloadZip(workspace))!);
const local = opts.raw
? undefined
: await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"));
const changes = await compareDynFSElement(remote, local, await ignoreF());
console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)"));
const remote = ZipFSElement((await downloadZip(workspace))!)
const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
console.log(`remote -> local: ${changes.length} changes to apply`);
if (changes.length > 0) {
prettyChanges(changes);
prettyChanges(changes)
if (
!opts.yes &&
!opts.raw &&
!(await Confirm.prompt({
message: `Do you want to apply these ${changes.length} changes?`,
default: true,
}))
!opts.yes && !opts.raw && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
) {
return;
return
}
const conflicts = [];
const conflicts = []
console.log(colors.gray(`Applying changes to files ...`));
for await (const change of changes) {
const target = path.join(Deno.cwd(), change.path);
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
if (change.name === "edited") {
try {
const currentLocal = await Deno.readTextFile(target);
if (currentLocal !== change.before && currentLocal !== change.after) {
console.log(
colors.red(
`Conflict detected on ${change.path}\nBoth local and remote have been modified.`
)
);
const currentLocal = await Deno.readTextFile(target)
if (currentLocal !== change.before) {
console.log(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`))
if (opts.failConflicts) {
conflicts.push({
local: currentLocal,
change,
path: change.path,
});
conflicts.push({ local: currentLocal, change, path: change.path })
continue;
} else if (opts.yes) {
console.log(
colors.red(
`Override local version with remote since --yes was passed and no --fail-conflicts.`
)
);
} else {
showConflict(change.path, currentLocal, change.after);
if (
await Confirm.prompt(
"Preserve local (push to change remote and avoid seeing this again)?"
)
) {
console.log(colors.red(`Override local version with remote since --yes was passed and no --fail-conflicts.`))
}
else {
showConflict(change.path, currentLocal, change.after)
if (await Confirm.prompt("Preserve local (push to change remote and avoid seeing this again)?")) {
continue;
}
}
}
} catch {}
} catch { }
if (change.path.endsWith(".json")) {
const diffs = microdiff(
JSON.parse(change.before),
JSON.parse(change.after),
{ cyclesFix: false }
);
const diffs =
microdiff(
JSON.parse(change.before),
JSON.parse(change.after),
{ cyclesFix: false },
)
console.log(
`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`
console.log(`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`)
await applyDiff(
diffs,
target,
);
await applyDiff(diffs, target);
} else {
console.log(`Editing script ${change.path}`);
console.log(`Editing script ${change.path}`)
await Deno.writeTextFile(target, change.after);
}
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
await ensureDir(path.dirname(stateTarget))
await Deno.copyFile(target, stateTarget);
}
} else if (change.name === "added") {
await ensureDir(path.dirname(target));
await ensureDir(path.dirname(target))
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
console.log(
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
);
await ensureDir(path.dirname(stateTarget))
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
}
await Deno.writeTextFile(target, change.content);
if (!opts.raw) {
@@ -360,10 +338,8 @@ async function pull(
}
} else if (change.name === "deleted") {
try {
console.log(
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
);
await Deno.remove(target);
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
await Deno.remove(target)
if (!opts.raw) {
await Deno.remove(stateTarget);
}
@@ -376,29 +352,24 @@ async function pull(
}
if (opts.failConflicts) {
if (conflicts.length > 0) {
console.error(colors.red(`Conflicts were found`));
console.log("Conflicts:");
console.error(colors.red(`Conflicts were found`))
console.log("Conflicts:")
for (const conflict of conflicts) {
showConflict(conflict.path, conflict.local, conflict.change.after);
showConflict(conflict.path, conflict.local, conflict.change.after)
}
console.log(
colors.red(`Please resolve theses conflicts manually by either:
console.log(colors.red(`Please resolve theses conflicts manually by either:
- reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
`)
);
Deno.exit(1);
`))
Deno.exit(1)
}
}
console.log(
colors.green.underline(
`Done! All ${changes.length} changes applied locally.`
)
);
console.log(colors.green.underline(`Done! All ${changes.length} changes applied locally.`));
}
function showConflict(path: string, local: string, remote: string) {
console.log(colors.yellow(`- ${path}`));
console.log(colors.yellow(`- ${path}`))
let finalString = "";
for (const character of diffCharacters(local, remote)) {
@@ -414,8 +385,8 @@ async function pull(
}
}
console.log(finalString);
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m");
console.log();
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m")
console.log()
}
async function applyDiff(diffs: Difference[], file: string) {
ensureDir(path.dirname(file));
@@ -444,20 +415,17 @@ async function pull(
}
}
function prettyChanges(changes: Change[]) {
for (const change of changes) {
if (change.name === "added") {
console.log(
colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path)
);
console.log(colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path));
} else if (change.name === "deleted") {
console.log(
colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path)
);
console.log(colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path));
} else if (change.name === "edited") {
console.log(
colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path)
);
console.log(colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path));
}
}
}
@@ -484,208 +452,138 @@ function prettyDiff(diffs: Difference[]) {
}
}
function removeSuffix(str: string, suffix: string) {
return str.slice(0, str.length - suffix.length);
}
async function push(
opts: GlobalOptions & {
raw: boolean;
yes: boolean;
skipPull: boolean;
failConflicts: boolean;
}
) {
async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull: boolean, failConflicts: boolean }) {
if (!opts.raw) {
if (!opts.skipPull) {
console.log(
colors.gray("You need to be up-to-date before pushing, pulling first.")
);
await pull(opts);
console.log(colors.green("Pull done, now pushing."));
console.log();
console.log(colors.gray("You need to be up-to-date before pushing, pulling first."))
await pull(opts)
console.log(colors.green("Pull done, now pushing."))
console.log()
}
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log(
colors.gray(
"Computing the files to update on the remote to match local (taking .wmillignore into account)"
)
);
const remote = opts.raw
? undefined
: ZipFSElement((await downloadZip(workspace))!);
const local = await FSFSElement(path.join(Deno.cwd(), ""));
const changes = await compareDynFSElement(local, remote, await ignoreF());
console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)"));
const remote = ZipFSElement((await downloadZip(workspace))!)
const local = await FSFSElement(path.join(Deno.cwd(), ""))
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
console.log(`remote <- local: ${changes.length} changes to apply`);
if (changes.length > 0) {
prettyChanges(changes);
prettyChanges(changes)
if (
!opts.yes &&
!(await Confirm.prompt({
message: `Do you want to apply these ${changes.length} changes?`,
default: true,
}))
!opts.yes && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
) {
return;
return
}
console.log(colors.gray(`Applying changes to files ...`));
const alreadySynced: string[] = [];
const alreadySynced: string[] = []
for await (const change of changes) {
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
if (change.name === "edited") {
if (
await handleScriptMetadata(
change.path,
workspace.workspaceId,
alreadySynced
)
) {
if (await handleScriptMetadata(change.path, workspace.workspaceId, alreadySynced)) {
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
} else if (
await handleFile(
change.path,
change.after,
workspace.workspaceId,
alreadySynced
)
) {
continue
} else if (await handleFile(change.path, change.after, workspace.workspaceId, alreadySynced)) {
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
continue
}
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
console.log(
`Editing ${getTypeStrFromPath(change.path)} ${change.path}`
);
await ensureDir(path.dirname(stateTarget))
console.log(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`)
}
const obj = inferTypeFromPath(change.path, JSON.parse(change.after));
const obj = inferTypeFromPath(change.path, JSON.parse(change.after))
const diff = microdiff(
inferTypeFromPath(change.path, JSON.parse(change.before)),
obj,
{ cyclesFix: false }
);
const diff = microdiff(inferTypeFromPath(change.path, JSON.parse(change.before)), obj, { cyclesFix: false });
await applyDiff(
workspace.workspaceId,
change.path.split(".")[0],
obj,
diff
diff,
);
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.after);
}
} else if (change.name === "added") {
if (change.path.endsWith(".script.json")) {
continue;
} else if (
await handleFile(
change.path,
change.content,
workspace.workspaceId,
alreadySynced
)
) {
continue;
continue
} else if (await handleFile(change.path, change.content, workspace.workspaceId, alreadySynced)) {
continue
}
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
console.log(
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
);
await ensureDir(path.dirname(stateTarget))
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
}
const obj = inferTypeFromPath(change.path, JSON.parse(change.content));
const obj = inferTypeFromPath(change.path, JSON.parse(change.content))
const diff = microdiff({}, obj, { cyclesFix: false });
await applyDiff(
workspace.workspaceId,
change.path.split(".")[0],
obj,
diff
diff,
);
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.content);
}
} else if (change.name === "deleted") {
if (!change.path.includes(".json")) {
continue;
continue
}
console.log(
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
);
const typ = getTypeStrFromPath(change.path);
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
const typ = getTypeStrFromPath(change.path)
const workspaceId = workspace.workspaceId;
switch (typ) {
case "script": {
const script = await ScriptService.getScriptByPath({
workspace: workspaceId,
path: removeSuffix(change.path, ".script.json"),
});
await ScriptService.deleteScriptByHash({
workspace: workspaceId,
hash: script.hash,
});
const script = await ScriptService.getScriptByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".script.json") })
await ScriptService.deleteScriptByHash({ workspace: workspaceId, hash: script.hash })
break;
}
case "folder":
await FolderService.deleteFolder({
workspace: workspaceId,
name: change.path.split("/")[1],
});
await FolderService.deleteFolder({ workspace: workspaceId, name: change.path.split('/')[1] })
break;
case "resource":
await ResourceService.deleteResource({
workspace: workspaceId,
path: removeSuffix(change.path, ".resource.json"),
});
await ResourceService.deleteResource({ workspace: workspaceId, path: removeSuffix(change.path, ".resource.json") })
break;
case "resource-type":
await ResourceService.deleteResourceType({
workspace: workspaceId,
path: removeSuffix(change.path, ".resource-type.json"),
});
break;
await ResourceService.deleteResourceType({ workspace: workspaceId, path: removeSuffix(change.path, ".resource-type.json") })
break
case "flow":
await FlowService.deleteFlowByPath({
workspace: workspaceId,
path: removeSuffix(change.path, ".flow.json"),
});
break;
await FlowService.deleteFlowByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".flow.json") })
break
case "app":
await AppService.deleteApp({
workspace: workspaceId,
path: removeSuffix(change.path, ".app.json"),
});
break;
await AppService.deleteApp({ workspace: workspaceId, path: removeSuffix(change.path, ".app.json") })
break
case "variable":
await VariableService.deleteVariable({
workspace: workspaceId,
path: removeSuffix(change.path, ".variable.json"),
});
break;
await VariableService.deleteVariable({ workspace: workspaceId, path: removeSuffix(change.path, ".variable.json") })
break
default:
break;
}
try {
await Deno.remove(stateTarget);
} catch {}
Deno.remove(stateTarget)
} catch { }
}
}
console.log(
colors.green.underline(
`Done! All ${changes.length} changes pushed to the remote workspace.`
)
);
console.log(colors.green.underline(`Done! All ${changes.length} changes pushed to the remote workspace.`));
}
async function applyDiff(
workspace: string,
remotePath: string,
@@ -696,11 +594,11 @@ async function push(
| ResourceFile
| ResourceTypeFile
| FolderFile,
diffs: Difference[]
diffs: Difference[],
) {
if (file instanceof ScriptFile) {
throw new Error(
"This code path should be unreachable - we should never generate diffs for scripts"
"This code path should be unreachable - we should never generate diffs for scripts",
);
} else if (file instanceof FolderFile) {
const parts = remotePath.split("/");
@@ -711,14 +609,14 @@ async function push(
}
}
if (diffs.length === 0) {
console.log("No diffs to apply to " + remotePath);
console.log("No diffs to apply to " + remotePath)
return;
}
try {
await file.pushDiffs(workspace, remotePath, diffs);
} catch (e) {
console.error("Failing to apply diffs to " + remotePath);
console.error(JSON.stringify(e));
console.error("Failing to apply diffs to " + remotePath)
console.error(e.body)
}
}
}
@@ -726,24 +624,18 @@ async function push(
const command = new Command()
.command("pull")
.description(
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking."
)
.option(
"--fail-conflicts",
"Error on conflicts (both remote and local have changes on the same item)"
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking.",
)
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
.option("--yes", "Pull without needing confirmation")
.option("--raw", "Pull without using state, just overwrite.")
.action(pull as any)
.command("push")
.description(
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking."
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking.",
)
.option(
"--fail-conflicts",
"Error on conflicts (both remote and local have changes on the same item)"
)
.option("--skip-pull", "Push without pulling first (you have pulled prior)")
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
.option("--skip-pull", "Push without pulling first")
.option("--yes", "Push without needing confirmation")
.option("--raw", "Push without using state, just overwrite.")
.action(push as any);

View File

@@ -11,6 +11,7 @@ import {
colors,
Command,
EditVariable,
ListableVariable,
microdiff,
Table,
VariableService,
@@ -66,7 +67,7 @@ export class VariableFile implements Resource, PushDiffs {
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing variable... ${remotePath}`,
`Applying ${diffs.length} diffs to existing variable...`,
),
);
const changeset: EditVariable = {};
@@ -75,12 +76,11 @@ export class VariableFile implements Resource, PushDiffs {
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["path", "value", "is_secret", "description", "account", "is_oauth"].includes(
!["path", "value", "is_secret", "description"].includes(
diff.path[0] as string,
)
)
) {
console.log(colors.red("Invalid variable diff with path " + diff.path));
throw new Error("Invalid variable diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -96,7 +96,6 @@ export class VariableFile implements Resource, PushDiffs {
if (!hasChanges) {
return;
}
await VariableService.updateVariable({
workspace,
path: remotePath,
@@ -104,6 +103,7 @@ export class VariableFile implements Resource, PushDiffs {
requestBody: changeset,
});
console.log(changeset);
} else {
console.log(colors.yellow.bold("Creating new variable..."));
await VariableService.createVariable({
@@ -121,10 +121,19 @@ export class VariableFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ListableVariable | undefined;
try {
existing = await VariableService.getVariable({
workspace: workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false }),
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
}

View File

@@ -139,8 +139,6 @@ async function list(opts: GlobalOptions) {
}),
)
.render();
console.log('Active: ' + colors.green.bold(activeName || 'none'))
}
async function switchC(opts: GlobalOptions, workspaceName: string) {
@@ -315,8 +313,6 @@ async function remove(_opts: GlobalOptions, name: string) {
async function whoami(_opts: GlobalOptions) {
await requireLogin(_opts)
console.log(await UserService.globalWhoami())
const activeName = await getActiveWorkspaceName(_opts);
console.log('Active: ' + colors.green.bold(activeName || 'none'))
}
const command = new Command()

View File

@@ -1,12 +1,12 @@
{
"name": "windmill",
"version": "1.76.0",
"version": "1.72.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.76.0",
"version": "1.72.0",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
@@ -48,6 +48,7 @@
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.6",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -67,7 +68,6 @@
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",
"svelte-awesome": "^3.0.0",
"svelte-awesome-color-picker": "^2.4.1",
"svelte-check": "^3.0.2",
"svelte-highlight": "^6.2.1",
"svelte-overlay": "^1.4.1",
@@ -1600,6 +1600,12 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@windmill-labs/svelte-grid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
"dev": true
},
"node_modules/@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -7122,15 +7128,6 @@
"svelte": "^3.43.1"
}
},
"node_modules/svelte-awesome-color-picker": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/svelte-awesome-color-picker/-/svelte-awesome-color-picker-2.4.1.tgz",
"integrity": "sha512-IzICGgaMRMgyK4uukm27UdTFfN7s0QN3eoFyIGiXXY8FCPumS9T+vffVG4rNhiuUUP4ga5SoGUE+KXuNj5dsrw==",
"dev": true,
"dependencies": {
"colord": "^2.9.3"
}
},
"node_modules/svelte-chartjs": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.0.tgz",
@@ -9277,6 +9274,12 @@
"eslint-visitor-keys": "^3.3.0"
}
},
"@windmill-labs/svelte-grid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
"dev": true
},
"@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -13335,15 +13338,6 @@
"dev": true,
"requires": {}
},
"svelte-awesome-color-picker": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/svelte-awesome-color-picker/-/svelte-awesome-color-picker-2.4.1.tgz",
"integrity": "sha512-IzICGgaMRMgyK4uukm27UdTFfN7s0QN3eoFyIGiXXY8FCPumS9T+vffVG4rNhiuUUP4ga5SoGUE+KXuNj5dsrw==",
"dev": true,
"requires": {
"colord": "^2.9.3"
}
},
"svelte-chartjs": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.76.0",
"version": "1.72.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -27,6 +27,7 @@
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.6",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -46,7 +47,6 @@
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",
"svelte-awesome": "^3.0.0",
"svelte-awesome-color-picker": "^2.4.1",
"svelte-check": "^3.0.2",
"svelte-highlight": "^6.2.1",
"svelte-overlay": "^1.4.1",

View File

@@ -1,2 +1,79 @@
/// <reference types="@sveltejs/kit" />
declare type Item = import('svelte-dnd-action').Item
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>
declare namespace svelte.JSX {
interface HTMLAttributes<T> {
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
}
}
declare module '@windmill-labs/svelte-grid' {
import type { SvelteComponentTyped } from 'svelte'
export interface Size {
w: number
h: number
}
export interface Positon {
x: number
y: number
}
interface ItemLayout extends Size, Positon {
fixed?: boolean
resizable?: boolean
draggable?: boolean
customDragger?: boolean
customResizer?: boolean
min?: Size
max?: Size
}
export type Item<T> = T & { [width: number]: ItemLayout; data: any }
export type FilledItem<T> = T & { [width: number]: Required<ItemLayout>; data: any }
export interface Props<T> {
fillSpace?: boolean
items: FilledItem<T>[]
rowHeight: number
cols: [number, number][]
gap?: [number, number]
fastStart?: boolean
throttleUpdate?: number
throttleResize?: number
onTopId?: string
scroller?: undefined
sensor?: number
parentWidth?: number
}
export interface Slots<T> {
default: { item: ItemLayout; dataItem: Item<T> }
}
export default class Grid<T = {}> extends SvelteComponentTyped<
Props<T>,
{
pointerup: CustomEvent<{ id: string }>
mount: CustomEvent<>
},
Slots<T>
> { }
}
declare module '@windmill-labs/svelte-grid/build/helper/index.mjs' {
import { ItemLayout } from '@windmill-labs/svelte-grid'
const x: {
normalize(items: any[], col: any): unknown[]
adjust(items: any[], col: any): unknown[]
findSpace(item: any, items: any, cols: any): unknown
item<T>(obj: ItemLayout): Required<ItemLayout>
}
export default x
}

View File

@@ -81,7 +81,7 @@
$: key =
apiTokenApps[resource_type]?.key ??
(args != undefined
? Object.keys(args).filter((x) => ['token', 'password', 'api_key', 'key'].includes(x))[0]
? Object.keys(args).filter((x) => ['token', 'password', 'api_key'].includes(x))[0]
: undefined)
let scopes: string[] = []
@@ -259,7 +259,6 @@
args['token'] == '' &&
args['password'] == '' &&
args['api_key'] == '' &&
args['key'] == '' &&
key != undefined) ||
(step == 3 && pathError != '') ||
!isValid
@@ -397,7 +396,7 @@
bind:error={pathError}
bind:path
initialPath=""
namePlaceholder={resource_type}
namePlaceholder="{resource_type}"
kind="resource"
/>
@@ -434,7 +433,7 @@
{:else}
<Path
initialPath=""
namePlaceholder={resource_type}
namePlaceholder="{resource_type}"
bind:error={pathError}
bind:path
kind="resource"

View File

@@ -2,10 +2,9 @@
import type { DropdownItem } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { MoreVertical } from 'lucide-svelte'
import { MoreHorizontal } from 'lucide-svelte'
import { Button, Menu } from './common'
import { goto } from '$app/navigation'
import { twMerge } from 'tailwind-merge'
type Alignment = 'start' | 'end'
type Side = 'top' | 'bottom'
@@ -14,24 +13,15 @@
export let dropdownItems: DropdownItem[]
export let name: string | undefined = undefined
export let placement: Placement = 'bottom-start'
export let btnClasses = ''
$: buttonClass = twMerge('!border-0 bg-transparent !p-[6px]', btnClasses)
export let btnClasses = '!text-blue-500 bg-transparent'
const dispatch = createEventDispatcher()
</script>
<Menu {placement} let:close>
<Button
nonCaptureEvent
color="dark"
variant="border"
size="xs"
btnClasses={buttonClass}
{...$$restProps}
slot="trigger"
>
<Button nonCaptureEvent color="light" size="xs" {btnClasses} {...$$restProps} slot="trigger">
{#if !$$slots.default}
<MoreVertical size={20} />
<MoreHorizontal size={20} />
{:else}
<slot />
{/if}

View File

@@ -4,17 +4,18 @@
import { initHistory, redo, undo } from '$lib/history'
import { userStore, workspaceStore } from '$lib/stores'
import { encodeState, formatCron, loadHubScripts, sendUserToast } from '$lib/utils'
import { faCalendarAlt, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { faCalendarAlt, faEye, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { Redo, Undo } from 'lucide-svelte'
import { setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Button, ButtonPopup, ButtonPopupItem, UndoRedo } from './common'
import { Button, ButtonPopup, ButtonPopupItem } from './common'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { OFFSET } from './CronInput.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import FlowEditor from './flows/FlowEditor.svelte'
import type { FlowState } from './flows/flowState'
import { dfs } from './flows/flowStore'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
@@ -192,62 +193,11 @@
$: initialPath && $workspaceStore && loadSchedule()
loadHubScripts()
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Z':
if (event.ctrlKey) {
$flowStore = redo(history)
event.preventDefault()
}
break
case 'z':
if (event.ctrlKey) {
$flowStore = undo(history, $flowStore)
$selectedIdStore = 'Input'
event.preventDefault()
}
break
case 's':
if (event.ctrlKey) {
saveFlow(false)
event.preventDefault()
}
break
case 'ArrowDown': {
let ids = generateIds()
let idx = ids.indexOf($selectedIdStore)
if (idx > -1 && idx < ids.length - 1) {
$selectedIdStore = ids[idx + 1]
event.preventDefault()
}
break
}
case 'ArrowUp': {
let ids = generateIds()
let idx = ids.indexOf($selectedIdStore)
if (idx > 0 && idx < ids.length) {
$selectedIdStore = ids[idx - 1]
event.preventDefault()
}
break
}
}
}
function generateIds() {
return [
'settings-metadata',
'constants',
...dfs($flowStore.value.modules, (module) => module.id)
]
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if !$userStore?.operator}
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
<UnsavedConfirmationModal />
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
@@ -256,17 +206,32 @@
>
<div class="flex flex-row gap-4 items-center">
<FlowImportExportMenu />
<UndoRedo
undoProps={{ disabled: $history.index === 0 }}
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
on:undo={() => {
$flowStore = undo(history, $flowStore)
$selectedIdStore = 'Input'
}}
on:redo={() => {
$flowStore = redo(history)
}}
/>
<div class="flex gap-1">
<Button
title="Undo"
disabled={$history.index == 0}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$flowStore = undo(history, $flowStore)
}}
>
<Undo size={14} />
</Button>
<Button
title="Redo"
disabled={$history.index == $history.history.length - 1}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$flowStore = redo(history)
}}
>
<Redo size={14} />
</Button>
</div>
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">

View File

@@ -3,8 +3,6 @@
import { workspaceStore } from '$lib/stores'
export let name: string
export let tabular = false
export let order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources']
$: $workspaceStore && loadUsage()
@@ -15,18 +13,10 @@
}
</script>
{#if tabular}
{#each order as key}
<td class="text-center">
{usage[key] ?? ''}
</td>
<div class="flex flex-col text-xs text-gray-600">
{#each Object.entries(usage) as [k, v]}
<div>
{k}: {v}
</div>
{/each}
{:else}
<div class="flex flex-col text-xs text-gray-600">
{#each Object.entries(usage) as [k, v]}
<div>
{k}: {v}
</div>
{/each}
</div>
{/if}
</div>

View File

@@ -234,14 +234,6 @@
name,
requestBody: { username: member_name }
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name
}
})
loadGroup()
}}>remove</button
>

View File

@@ -11,7 +11,7 @@
<span class="flex items-center space-x-2 mb-2">
<h1>{title}</h1>
{#if tooltip}
<Tooltip scale={0.9}>{tooltip}</Tooltip>
<Tooltip>{tooltip}</Tooltip>
{/if}
</span>
{:else}

View File

@@ -23,7 +23,6 @@
import FolderEditor from './FolderEditor.svelte'
import GroupEditor from './GroupEditor.svelte'
import { random_adj } from './random_positive_adjetive'
import Badge from './common/badge/Badge.svelte'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
let meta: Meta | undefined = undefined
@@ -307,7 +306,7 @@
{#if meta != undefined}
<div class="flex gap-4 shrink">
<label class="block">
<span class="text-gray-700 text-sm whitespace-nowrap">Owner</span>
<span class="text-gray-700 text-sm whitespace-nowrap">&nbsp;</span>
<ToggleButtonGroup
class="mt-0.5"
@@ -410,26 +409,8 @@
{/if}
</div>
<div class="flex flex-col w-full mt-4">
<div class="flex justify-start w-full">
<Badge
color="gray"
class="center-center !bg-gray-300 !text-gray-600 !w-[70px] !h-[24px] rounded-r-none"
>
Full path
</Badge>
<input
type="text"
readonly
value={path}
size={path?.length || 50}
class="font-mono !text-xs max-w-[calc(100%-70px)] !w-auto !h-[24px] !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
currentTarget.select()
}}
/>
<!-- <span class="font-mono text-sm break-all">{path}</span> -->
</div>
<div class="flex-row flex justify-between">
<div><span class="font-mono text-sm">{path}</span></div>
<div class="text-red-600 text-2xs">{error}</div>
</div>
</div>

View File

@@ -11,38 +11,17 @@
import { SELECT_INPUT_DEFAULT_STYLE } from '../defaults'
const dispatch = createEventDispatcher()
let resources: Resource[] = []
export let initialValue: string | undefined = undefined
export let value: string | undefined = initialValue
export let resourceType: string | undefined = undefined
let valueSelect =
initialValue || value
? {
value: value ?? initialValue,
label: value ?? initialValue
}
: undefined
let collection = [valueSelect]
async function loadResources(resourceType: string | undefined) {
const nc = (
await ResourceService.listResource({
workspace: $workspaceStore!,
resourceType
})
).map((x) => ({
value: x.path,
label: x.path
}))
if (!nc.find((x) => x.value == value) && (initialValue || value)) {
nc.push({ value: value ?? initialValue!, label: value ?? initialValue! })
}
collection = nc
const v = value
resources = await ResourceService.listResource({ workspace: $workspaceStore!, resourceType })
value = v
}
$: {
if ($workspaceStore) {
loadResources(resourceType)
@@ -50,6 +29,10 @@
}
$: dispatch('change', value)
$: collection = resources.map((x) => ({
value: x.path,
label: x.path
}))
let appConnect: AppConnect
let resourceEditor: ResourceEditor
</script>
@@ -58,7 +41,6 @@
on:refresh={async (e) => {
await loadResources(resourceType)
value = e.detail
valueSelect = { value: e.detail, label: e.detail }
}}
newPageOAuth
bind:this={appConnect}
@@ -68,27 +50,21 @@
bind:this={resourceEditor}
on:refresh={async (e) => {
await loadResources(resourceType)
console.log(e)
if (e.detail) {
value = e.detail
valueSelect = { value: e.detail, label: e.detail }
}
}}
/>
<div class="flex flex-row gap-x-1 w-full">
<Select
value={valueSelect}
on:change={(e) => {
value = e.detail.value
valueSelect = e.detail
}}
on:clear={() => {
value = undefined
valueSelect = undefined
}}
listAutoWidth={false}
value={collection.find((x) => x.value == value)}
bind:justValue={value}
items={collection}
class="text-clip grow min-w-0"
placeholder="{resourceType ?? 'any'} resource"
placeholder="{resourceType} resource"
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles}
/>

View File

@@ -45,7 +45,7 @@
label: 'Failed',
data:
failed?.map((job) => ({
x: job.started_at as any,
x: job.created_at as any,
y: job.duration_ms,
id: job.id,
path: job.script_path
@@ -57,7 +57,7 @@
label: 'Successful',
data:
success?.map((job) => ({
x: job.started_at as any,
x: job.created_at as any,
y: job.duration_ms,
id: job.id,
path: job.script_path
@@ -71,10 +71,7 @@
enabled: true,
modifierKey: 'ctrl' as 'ctrl',
onPanComplete: ({ chart }) => {
dispatch('zoom', {
min: addSeconds(new Date(chart.scales.x.min), -1),
max: addSeconds(new Date(chart.scales.x.max), 1)
})
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
}
},
zoom: {
@@ -83,10 +80,7 @@
},
mode: 'x' as 'x',
onZoom: ({ chart }) => {
dispatch('zoom', {
min: addSeconds(new Date(chart.scales.x.min), -1),
max: addSeconds(new Date(chart.scales.x.max), 1)
})
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
}
}
}
@@ -94,22 +88,6 @@
function getPath(x: any): string {
return x.path
}
$: minTime = getMinTime(jobs)
function addSeconds(date: Date, seconds: number): Date {
date.setTime(date.getTime() + seconds * 1000)
return date
}
function getMinTime(jobs: CompletedJob[] | undefined): Date {
return addSeconds(new Date(jobs?.[jobs?.length - 1]?.started_at ?? new Date().toString()), -15)
}
$: maxTime = getMaxTime(jobs)
function getMaxTime(jobs: CompletedJob[] | undefined): Date {
return addSeconds(new Date(jobs?.[0]?.started_at ?? new Date().toString()), 15)
}
</script>
<Scatter
@@ -137,8 +115,7 @@
display: false
},
type: 'time',
min: minTime,
max: maxTime
min: jobs?.[jobs?.length - 1]?.created_at ?? new Date().toString()
},
y: {
grid: {

View File

@@ -1,23 +1,24 @@
<script lang="ts">
import { Script, ScriptService } from '$lib/gen'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { inferArgs } from '$lib/infer'
import { initialCode } from '$lib/script_helpers'
import { initialCode, isInitialCode } from '$lib/script_helpers'
import { userStore, workspaceStore } from '$lib/stores'
import { emptySchema, encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import Path from './Path.svelte'
import RadioButton from './RadioButton.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import CenteredPage from './CenteredPage.svelte'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
import { faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
import Tooltip from './Tooltip.svelte'
import SettingSection from './SettingSection.svelte'
export let script: Script
export let initialPath: string = ''
@@ -31,34 +32,14 @@
['Go', Script.language.GO],
['Bash', Script.language.BASH]
]
const scriptKindOptions: { value: Script.kind; title: string; desc?: string }[] = [
{
value: Script.kind.SCRIPT,
title: 'Action'
},
{
value: Script.kind.TRIGGER,
title: 'Trigger',
desc: 'First module of flows to trigger them based on external changes. These kind of scripts are usually running on a schedule to periodically look for changes.'
},
{
value: Script.kind.APPROVAL,
title: 'Approval',
desc: 'Send notifications externally to ask for approval to continue a flow.'
},
{
value: Script.kind.FAILURE,
title: 'Error Handler',
desc: 'Handle errors in flows after all retry attempts have been exhausted.'
}
]
let viewScriptKind = script.kind !== Script.kind.SCRIPT
let pathError = ''
let summaryC: HTMLInputElement | undefined = undefined
let pathC: Path | undefined = undefined
let loadingSave = false
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
$: step = Number($page.url.searchParams.get('step')) || 1
if (script.content == '') {
@@ -73,6 +54,7 @@
script.content = initialCode(language, kind, template)
}
let loadingSave = false
async function editScript(leave: boolean): Promise<void> {
loadingSave = true
try {
@@ -139,7 +121,10 @@
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if !$userStore?.operator}
<UnsavedConfirmationModal />
<div class="flex flex-col h-screen">
<!-- Nav between steps-->
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
@@ -269,8 +254,7 @@
}}
disabled={lockedLanguage}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2">{label}</span>
<LanguageIcon {lang} /><span class="ml-2 py-4">{label}</span>
</Button>
{/each}
<Button
@@ -285,7 +269,7 @@
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-4">PostgreSQL</span>
</Button>
<!-- <Button
size="sm"
@@ -298,41 +282,65 @@
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
<LanguageIcon lang="mysql" /><span class="ml-2 py-4">MySQL</span>
</Button> -->
</div>
<SettingSection
title="Script kind"
element="h3"
tooltip="Tag this script as having a specific purpose inside flows. If it won't be used in flows,
you don't have to worry about this."
accordion
>
<div class="flex flex-wrap gap-2">
{#each scriptKindOptions as { value, title, desc }}
{@const isPicked = script.kind === value}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses="font-medium {isPicked ? '!bg-blue-50/75' : ''}"
on:click={() => {
<div class="mt-16 mb-4">
<Button
color="light"
size="sm"
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
on:click={() => (viewScriptKind = !viewScriptKind)}
>
Tag this script as having a specific purpose inside flows
</Button>
</div>
{#if viewScriptKind}
<div class="max-w-lg">
<RadioButton
label="Script Type"
options={[
['Action', Script.kind.SCRIPT],
[
{
title: 'Trigger',
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
},
Script.kind.TRIGGER
],
[
{
title: 'Approval',
desc: `Send notification externally to ask for approval to continue a flow`
},
Script.kind.APPROVAL
],
[
{
title: 'Error Handler',
desc: `Handle errors for flows after all retries attempts have been exhausted`
},
Script.kind.FAILURE
]
// ['Command Handler', Script.kind.COMMAND]
]}
on:change={(e) => {
if (isInitialCode(script.content)) {
template = 'script'
script.kind = value
initContent(script.language, value, template)
}}
disabled={lockedLanguage}
>
{title}
{#if desc}
<Tooltip class="mb-0.5 ml-1">
{desc}
</Tooltip>
{/if}
</Button>
{/each}
initContent(script.language, e.detail, template)
}
}}
bind:value={script.kind}
/>
</div>
</SettingSection>
{/if}
<!-- <div class="ml-3">
<Toggle
bind:checked={script.is_template}
options={{ right: 'Save as a workspace template' }}
/>
</div> -->
</CenteredPage>
{:else if step === 2}
<ScriptEditor
@@ -352,5 +360,3 @@
{:else}
Script Builder not available to operators
{/if}
<svelte:window on:keydown={onKeyDown} />

View File

@@ -1,43 +0,0 @@
<script lang="ts">
import { ChevronDown } from 'lucide-svelte'
import { slide } from 'svelte/transition'
import type { IntRange } from '../common'
import Tooltip from './Tooltip.svelte'
export let title: string
export let tooltip: string
export let element: `h${IntRange<1, 6>}` = 'h2'
export let accordion = false
let showContent = !accordion
</script>
<div class="border-b [&:has(button:hover)]:border-gray-400 duration-200 pb-1 mt-8 mb-2">
{#if accordion}
<button
class="flex w-full justify-between items-center"
on:click={() => (showContent = !showContent)}
>
<svelte:element this={element}>
{title}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
</svelte:element>
<span class="rounded-full hover:bg-gray-100 focus:bg-gray-100 p-1">
<ChevronDown size={22} class="rotate-0 duration-300 {showContent ? '!rotate-180' : ''}" />
</span>
</button>
{:else}
<svelte:element this={element}>
{title}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
</svelte:element>
{/if}
</div>
{#if showContent}
<div transition:slide={{ duration: 300 }}>
<slot />
</div>
{/if}

View File

@@ -2,18 +2,13 @@
import { ChevronDown, ChevronUp } from 'lucide-svelte'
import { slide } from 'svelte/transition'
import Button from './common/button/Button.svelte'
import Tooltip from './Tooltip.svelte'
export let text: string
export let tooltip: string | undefined = undefined
export let view = false
</script>
<Button color="light" on:click={() => (view = !view)} variant="border"
>{text}
{#if tooltip}
<Tooltip wrapperClass="mx-1">{tooltip}</Tooltip>
{/if}
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
>
{#if view}

View File

@@ -186,9 +186,9 @@
syncIteration++
await loadTestJob(id)
let nextIteration = 50
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
if (syncIteration == ITERATIONS_BEFORE_SLOW_REFRESH) {
nextIteration = 500
} else if (syncIteration > ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
} else if (syncIteration == ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
nextIteration = 2000
}
setTimeout(() => syncer(id), nextIteration)

View File

@@ -18,7 +18,5 @@
data={faInfoCircle}
{scale}
/>
<svelte:fragment slot="text">
<slot />
</svelte:fragment>
<span slot="text"> <slot /> </span>
</Popover>

View File

@@ -9,7 +9,9 @@
...new Set(
workers
.filter((worker) => {
return worker.ip != 'unretrievable IP' && worker.last_ping && worker.last_ping < 300
const date = new Date().getTime() - 300 * 60
const ping_at = new Date(worker.ping_at).getTime()
return worker.ip != 'unretrievable IP' && ping_at > date
})
.map((worker) => worker.ip)
)

View File

@@ -3,7 +3,7 @@
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
@@ -22,11 +22,10 @@
export let noWFull = false
export let preclickAction: (() => Promise<void>) | undefined = undefined
export let customCss: ComponentCustomCSS<'button'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { worldStore, app } = getContext<AppViewerContext>('AppViewerContext')
const { runnableComponents, worldStore, app } = getContext<AppEditorContext>('AppEditorContext')
let labelValue: string
let color: ButtonType.Color
@@ -109,6 +108,10 @@
} else {
await runnableComponent?.runComponent()
}
if (recomputeIds) {
await Promise.all(recomputeIds.map((id) => $runnableComponents?.[id]?.()))
}
}
</script>
@@ -123,7 +126,6 @@
<InputValue
{id}
input={configuration.disabled}
extraContext={extraQueryParams}
bind:value={disabled}
bind:error={errors.disabled}
/>
@@ -132,7 +134,6 @@
<RunnableWrapper
flexWrap
{recomputeIds}
bind:runnableComponent
{componentInput}
{id}
@@ -140,7 +141,6 @@
autoRefresh={false}
goto={gotoUrl}
{gotoNewTab}
{render}
>
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
{#if errorsMessage}
@@ -164,11 +164,11 @@
{loading}
>
<span class="truncate inline-flex gap-2 items-center">
{#if beforeIcon && beforeIconComponent}
{#if beforeIconComponent}
<svelte:component this={beforeIconComponent} size={14} />
{/if}
<div>{labelValue}</div>
{#if afterIcon && afterIconComponent}
{#if afterIconComponent}
<svelte:component this={afterIconComponent} size={14} />
{/if}
</span>

View File

@@ -5,7 +5,7 @@
import { Icon } from 'svelte-awesome'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
@@ -19,11 +19,11 @@
export let extraQueryParams: Record<string, any> = {}
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let customCss: ComponentCustomCSS<'container' | 'button'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app, worldStore, stateId } = getContext<AppViewerContext>('AppViewerContext')
const { app, runnableComponents, worldStore, stateId } =
getContext<AppEditorContext>('AppEditorContext')
let labelValue: string = 'Default label'
let color: ButtonType.Color
@@ -61,8 +61,6 @@
<InputValue {id} input={configuration.size} bind:value={size} />
<RunnableWrapper
{recomputeIds}
{render}
bind:runnableComponent
{componentInput}
{id}
@@ -103,6 +101,12 @@
}}
on:click={() => {
runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
}}
{size}
{color}

View File

@@ -5,7 +5,7 @@
import { Icon } from 'svelte-awesome'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
@@ -22,11 +22,10 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let customCss: ComponentCustomCSS<'button' | 'popup'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app, runnableComponents, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let labelValue: string = 'Default label'
let color: ButtonType.Color
@@ -94,8 +93,6 @@
}}
>
<RunnableWrapper
{recomputeIds}
{render}
bind:runnableComponent
{componentInput}
{id}
@@ -129,6 +126,12 @@
on:click={async () => {
await runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
open = false
}}
size="xs"

View File

@@ -17,17 +17,16 @@
import InputValue from '../helpers/InputValue.svelte'
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
ChartJS.register(
Title,
@@ -90,7 +89,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.line} bind:value={lineChart} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
{#if lineChart}

View File

@@ -5,7 +5,7 @@
import type { AppInput } from '../../inputType'
import {
IS_APP_PUBLIC_CONTEXT_KEY,
type AppViewerContext,
type AppEditorContext,
type ComponentCustomCSS
} from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
@@ -14,16 +14,15 @@
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'header' | 'container'> | undefined = undefined
export let render: boolean
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let result: any = undefined
export const staticOutputs: string[] = ['result', 'loading']
</script>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap {componentInput} {id} bind:initializing bind:result>
<div
class={twMerge(
'w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm',

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
@@ -9,10 +9,9 @@
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let result: string | undefined = undefined
let h: number | undefined = undefined
@@ -29,15 +28,7 @@
bind:clientHeight={h}
bind:clientWidth={w}
>
<RunnableWrapper
{render}
autoRefresh
flexWrap
{componentInput}
{id}
bind:initializing
bind:result
>
<RunnableWrapper autoRefresh flexWrap {componentInput} {id} bind:initializing bind:result>
{#key result}
<iframe
frameborder="0"

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { AlignWrapper, InputValue } from '../helpers'
import { loadIcon } from '../icon'
@@ -12,9 +12,8 @@
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = []
export let customCss: ComponentCustomCSS<'container' | 'icon'> | undefined = undefined
export let render: boolean
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let icon: string | undefined = undefined
let size: number
@@ -22,10 +21,12 @@
let strokeWidth: number
let iconComponent: any
$: handleIcon(icon)
$: icon && handleIcon()
async function handleIcon(i?: string) {
iconComponent = i ? await loadIcon(i) : undefined
async function handleIcon() {
if (icon) {
iconComponent = await loadIcon(icon)
}
}
$: css = concatCustomCss($app.css?.iconcomponent, customCss)
@@ -37,13 +38,12 @@
<InputValue {id} input={configuration.strokeWidth} bind:value={strokeWidth} />
<AlignWrapper
{render}
{horizontalAlignment}
{verticalAlignment}
class={css?.container?.class ?? ''}
style={css?.container?.style ?? ''}
>
{#if icon && iconComponent}
{#if iconComponent}
<svelte:component
this={iconComponent}
size={size || 24}

View File

@@ -3,7 +3,7 @@
import { twMerge } from 'tailwind-merge'
import type { staticValues } from '../../editor/componentsPanel/componentStaticValues'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
@@ -13,9 +13,8 @@
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['loading']
export let customCss: ComponentCustomCSS<'image'> | undefined = undefined
export let render: boolean
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
const fit: Record<FitOption, string> = {
cover: 'object-cover',
contain: 'object-contain',
@@ -33,12 +32,10 @@
<InputValue {id} input={configuration.imageFit} bind:value={imageFit} />
<InputValue {id} input={configuration.altText} bind:value={altText} />
{#if render}
<img
on:pointerdown|preventDefault
src={source}
alt={altText}
style={css?.image?.style ?? ''}
class={twMerge(`w-full h-full ${fit[imageFit || 'cover']}`, css?.image?.class ?? '')}
/>
{/if}
<img
on:pointerdown|preventDefault
src={source}
alt={altText}
style={css?.image?.style ?? ''}
class={twMerge(`w-full h-full ${fit[imageFit || 'cover']}`, css?.image?.class ?? '')}
/>

View File

@@ -2,7 +2,7 @@
import { getContext, onMount } from 'svelte'
import { concatCustomCss } from '../../utils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { InputValue } from '../helpers'
import { twMerge } from 'tailwind-merge'
import { Map, View, Feature } from 'ol'
@@ -32,10 +32,9 @@
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['mapRegion']
export let customCss: ComponentCustomCSS<'map'> | undefined = undefined
export let render: boolean
const { app, worldStore, selectedComponent, connectingInput, focusedGrid, mode } =
getContext<AppViewerContext>('AppViewerContext')
getContext<AppEditorContext>('AppEditorContext')
$: outputs = $worldStore?.outputsById[id] as {
mapRegion: Output<{
@@ -193,25 +192,23 @@
<InputValue {id} input={configuration.zoom} bind:value={zoom} />
<InputValue {id} input={configuration.markers} bind:value={markers} />
{#if render}
<div class="relative h-full w-full">
<div
on:pointerdown|stopPropagation={selectComponent}
bind:this={mapElement}
class={twMerge(`w-full h-full`, css?.map?.class ?? '')}
style={css?.map?.style ?? ''}
/>
<div class="relative h-full w-full">
<div
on:pointerdown|stopPropagation={selectComponent}
bind:this={mapElement}
class={twMerge(`w-full h-full`, css?.map?.class ?? '')}
style={css?.map?.style ?? ''}
/>
{#if $mode !== 'preview'}
<div
class="absolute bottom-0 left-0 px-1 py-0.5 bg-indigo-500 text-white text-2xs"
on:pointerdown={handleSyncRegion}
>
Set region
</div>
{/if}
</div>
{/if}
{#if $mode !== 'preview'}
<div
class="absolute bottom-0 left-0 px-1 py-0.5 bg-indigo-500 text-white text-2xs"
on:pointerdown={handleSyncRegion}
>
Set region
</div>
{/if}
</div>
<style global lang="postcss">
.ol-overlaycontainer-stopevent {

View File

@@ -4,7 +4,7 @@
import { getDocument, type PDFDocumentProxy, type PDFPageProxy } from 'pdfjs-dist'
import 'pdfjs-dist/build/pdf.worker.entry'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
import { throttle } from '../../../../utils'
@@ -17,9 +17,8 @@
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['loading']
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
const { app, mode, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, mode, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
let source: string | ArrayBuffer | undefined = undefined
let wrapper: HTMLDivElement | undefined = undefined
@@ -185,137 +184,135 @@
<InputValue {id} input={configuration.source} bind:value={source} />
<InputValue {id} input={configuration.zoom} bind:value={zoom} />
{#if render}
<div class="relative w-full h-full bg-gray-100">
{#if source && zoom}
{#if pages?.length}
<div
bind:clientWidth={controlsWidth}
bind:clientHeight={controlsHeight}
class="fixed flex {$mode !== 'preview'
? 'w-[calc(100%-2px)] top-[1px]'
: 'w-full top-0'} {wideView
? 'justify-center gap-14'
: '!justify-between'} overflow-x-auto bg-white border mx-auto py-1"
>
<div class="flex justify-start items-center px-2 text-gray-600 text-sm">
<div class="relative w-full h-full bg-gray-100">
{#if source && zoom}
{#if pages?.length}
<div
bind:clientWidth={controlsWidth}
bind:clientHeight={controlsHeight}
class="fixed flex {$mode !== 'preview'
? 'w-[calc(100%-2px)] top-[1px]'
: 'w-full top-0'} {wideView
? 'justify-center gap-14'
: '!justify-between'} overflow-x-auto bg-white border mx-auto py-1"
>
<div class="flex justify-start items-center px-2 text-gray-600 text-sm">
<Button
on:click={() => zoom && (zoom -= 10)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Zoom out"
aria-label="Zoom out"
btnClasses="!rounded-r-none !px-2"
>
<ZoomOut size={16} />
</Button>
{#if wideView}
<Button
on:click={() => zoom && (zoom -= 10)}
on:click={() => (zoom = 100)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Zoom out"
aria-label="Zoom out"
btnClasses="!rounded-r-none !px-2"
title="Reset zoom"
aria-label="Reset zoom"
btnClasses="!w-[50px] !font-medium !rounded-none !border-l-0 !px-1"
>
<ZoomOut size={16} />
{zoom.toFixed(0)}%
</Button>
{/if}
<Button
on:click={() => renderPdf(true, true)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Scale to viewport"
aria-label="Scale to viewport"
btnClasses="!rounded-none !border-l-0 !px-2"
>
<MoveHorizontal size={16} />
</Button>
<Button
on:click={() => zoom && (zoom += 10)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Zoom in"
aria-label="Zoom in"
btnClasses="!rounded-l-none !px-2 !border-l-0"
>
<ZoomIn size={16} />
</Button>
</div>
<div class="center-center px-2 text-gray-600 text-sm">
<input
on:input={({ currentTarget }) => {
scrollToPage(currentTarget.valueAsNumber)
}}
min="1"
max={pages.length}
value={pageNumber}
disabled={!doc}
type="number"
class="!w-[45px] !px-1 !py-0"
/>
<span class="whitespace-nowrap pl-1">
/ {pages.length}
</span>
</div>
<div class="flex justify-end items-center px-2 text-gray-600 text-sm">
<Button
on:click={downloadPdf}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Download PDF"
aria-label="Download PDF"
btnClasses="!font-medium !px-2"
>
{#if wideView}
<Button
on:click={() => (zoom = 100)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Reset zoom"
aria-label="Reset zoom"
btnClasses="!w-[50px] !font-medium !rounded-none !border-l-0 !px-1"
>
{zoom.toFixed(0)}%
</Button>
<span class="mr-1"> Download </span>
{/if}
<Button
on:click={() => renderPdf(true, true)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Scale to viewport"
aria-label="Scale to viewport"
btnClasses="!rounded-none !border-l-0 !px-2"
>
<MoveHorizontal size={16} />
</Button>
<Button
on:click={() => zoom && (zoom += 10)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Zoom in"
aria-label="Zoom in"
btnClasses="!rounded-l-none !px-2 !border-l-0"
>
<ZoomIn size={16} />
</Button>
</div>
<div class="center-center px-2 text-gray-600 text-sm">
<input
on:input={({ currentTarget }) => {
scrollToPage(currentTarget.valueAsNumber)
}}
min="1"
max={pages.length}
value={pageNumber}
disabled={!doc}
type="number"
class="!w-[45px] !px-1 !py-0"
/>
<span class="whitespace-nowrap pl-1">
/ {pages.length}
</span>
</div>
<div class="flex justify-end items-center px-2 text-gray-600 text-sm">
<Button
on:click={downloadPdf}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Download PDF"
aria-label="Download PDF"
btnClasses="!font-medium !px-2"
>
{#if wideView}
<span class="mr-1"> Download </span>
{/if}
<Download size={16} />
</Button>
</div>
<Download size={16} />
</Button>
</div>
{:else}
<div
out:fade={{ duration: 200 }}
class="absolute inset-0 center-center flex-col text-center text-sm bg-white text-gray-600"
>
<Loader2 class="animate-spin mb-2" />
Loading PDF
</div>
{/if}
</div>
{:else}
<div
bind:this={wrapper}
on:scroll={throttledScroll}
class={twMerge('w-full h-full overflow-auto', css?.container?.class ?? '', 'bg-gray-100')}
style="padding-top: {controlsHeight ?? 0}px; {css?.container?.style ?? ''}"
/>
{/if}
{#if $mode !== 'preview' && $selectedComponent === id}
<button
class="fixed z-10 bottom-0 left-0 px-2 py-0.5 bg-indigo-500/90
hover:bg-indigo-500 focus:bg-indigo-500 duration-200 text-white text-2xs"
on:click={() => syncZoomValue()}
out:fade={{ duration: 200 }}
class="absolute inset-0 center-center flex-col text-center text-sm bg-white text-gray-600"
>
Sync zoom value
</button>
{/if}
{#if error}
<div
class="absolute inset-0 z-20 center-center
bg-gray-100 text-center text-gray-600 text-sm"
>
{error}
<Loader2 class="animate-spin mb-2" />
Loading PDF
</div>
{/if}
</div>
{/if}
<div
bind:this={wrapper}
on:scroll={throttledScroll}
class={twMerge('w-full h-full overflow-auto', css?.container?.class ?? '', 'bg-gray-100')}
style="padding-top: {controlsHeight ?? 0}px; {css?.container?.style ?? ''}"
/>
{/if}
{#if $mode !== 'preview' && $selectedComponent === id}
<button
class="fixed z-10 bottom-0 left-0 px-2 py-0.5 bg-indigo-500/90
hover:bg-indigo-500 focus:bg-indigo-500 duration-200 text-white text-2xs"
on:click={() => syncZoomValue()}
>
Sync zoom value
</button>
{/if}
{#if error}
<div
class="absolute inset-0 z-20 center-center
bg-gray-100 text-center text-gray-600 text-sm"
>
{error}
</div>
{/if}
</div>

View File

@@ -14,7 +14,7 @@
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import InputValue from '../helpers/InputValue.svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
@@ -23,10 +23,9 @@
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
ChartJS.register(
Title,
@@ -73,7 +72,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.doughnutStyle} bind:value={doughnut} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
{#if doughnut}

View File

@@ -19,20 +19,19 @@
import type { ChartOptions, ChartData } from 'chart.js'
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
let zoomable = false
let pannable = false
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
ChartJS.register(
Title,
@@ -79,7 +78,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
<Scatter {data} {options} />

View File

@@ -8,7 +8,7 @@
import InputValue from '../helpers/InputValue.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { twMerge } from 'tailwind-merge'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { getContext } from 'svelte'
import ResizeWrapper from '../helpers/ResizeWrapper.svelte'
@@ -19,11 +19,10 @@
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'text'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let result: string | undefined = undefined
let style: 'Title' | 'Subtitle' | 'Body' | 'Caption' | 'Label' | undefined = undefined
@@ -68,7 +67,7 @@
<InputValue {id} input={configuration.copyButton} bind:value={copyButton} />
<InputValue {id} input={configuration.fitContent} bind:value={fitContent} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap {componentInput} {id} bind:initializing bind:result>
<ResizeWrapper {id} shouldWrap={fitContent}>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if !result || result === ''}

View File

@@ -20,7 +20,7 @@
import { Scatter } from 'svelte-chartjs'
import InputValue from '../helpers/InputValue.svelte'
import type { ChartOptions, ChartData } from 'chart.js'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { getContext } from 'svelte'
import { concatCustomCss } from '../../utils'
@@ -29,10 +29,9 @@
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let logarithmicScale = false
let zoomable = false
@@ -94,7 +93,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
<Scatter {data} {options} />

View File

@@ -7,7 +7,6 @@
export let componentInput: AppInput | undefined
// export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
@@ -16,17 +15,18 @@
let Plotly
onMount(async () => {
//@ts-ignore
await import('https://cdn.plot.ly/plotly-2.18.0.min.js')
if (divEl) {
//@ts-ignore
await import('https://cdn.plot.ly/plotly-2.18.0.min.js')
Plotly = window['Plotly']
Plotly = window['Plotly']
}
})
let h: number | undefined = undefined
let w: number | undefined = undefined
$: Plotly &&
render &&
result &&
divEl &&
h &&
@@ -40,7 +40,7 @@
</script>
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>

View File

@@ -8,7 +8,6 @@
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
@@ -53,7 +52,7 @@
<InputValue {id} input={configuration.canvas} bind:value={canvas} />
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>

View File

@@ -5,7 +5,7 @@
import { getContext, onMount } from 'svelte'
import type { Output } from '../../../rx'
import type { AppViewerContext } from '../../../types'
import type { AppEditorContext } from '../../../types'
import InputValue from '../../helpers/InputValue.svelte'
import type { AppInput } from '../../../inputType'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
@@ -17,13 +17,16 @@
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['selectedRow', 'loading', 'result', 'selectedRowIndex']
let result: Record<string, any>[] | undefined = undefined
const { worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const {
worldStore,
staticOutputs: staticOutputsStore,
selectedComponent
} = getContext<AppEditorContext>('AppEditorContext')
let selectedRowIndex = -1
@@ -81,7 +84,7 @@
<InputValue {id} input={configuration.pagination} bind:value={pagination} />
<InputValue {id} input={configuration.pageSize} bind:value={pageSize} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap {componentInput} {id} bind:initializing bind:result>
{#if Array.isArray(result) && result.every(isObject)}
<div
class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full"

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import type { Output } from '../../../rx'
import type { AppViewerContext, BaseAppComponent, ComponentCustomCSS } from '../../../types'
import type { AppEditorContext, BaseAppComponent, ComponentCustomCSS } from '../../../types'
import InputValue from '../../helpers/InputValue.svelte'
import type { AppInput } from '../../../inputType'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
@@ -25,7 +25,6 @@
export let customCss:
| ComponentCustomCSS<'container' | 'tableHeader' | 'tableBody' | 'tableFooter'>
| undefined = undefined
export let render: boolean
export const staticOutputs: string[] = [
'selectedRow',
@@ -61,7 +60,7 @@
app,
worldStore,
staticOutputs: staticOutputsStore
} = getContext<AppViewerContext>('AppViewerContext')
} = getContext<AppEditorContext>('AppEditorContext')
let selectedRowIndex = -1
@@ -151,7 +150,7 @@
<InputValue {id} input={configuration.search} bind:value={search} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper flexWrap {componentInput} {id} bind:initializing bind:result>
{#if Array.isArray(result) && result.every(isObject)}
<div
class={twMerge(
@@ -248,7 +247,6 @@
{#each actionButtons as actionButton, actionIndex (actionIndex)}
{#if rowIndex == 0}
<AppButton
{render}
noWFull
{...actionButton}
preclickAction={async () => {
@@ -260,7 +258,6 @@
/>
{:else}
<AppButton
{render}
noWFull
{...actionButton}
preclickAction={async () => {

View File

@@ -8,7 +8,6 @@
let c = ''
export { c as class }
export let style = ''
export let render: boolean = true
function tailwindHorizontalAlignment(alignment?: HorizontalAlignment) {
if (!alignment) return ''
@@ -40,8 +39,6 @@
)
</script>
{#if render}
<div class={classes} {style}>
<slot />
</div>
{/if}
<div class={classes} {style}>
<slot />
</div>

View File

@@ -12,7 +12,6 @@
export let name: string
export let inlineScript: InlineScript | undefined
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let autoRefresh: boolean = false
let result: any = undefined
@@ -20,10 +19,8 @@
</script>
<RunnableComponent
render={false}
{id}
{fields}
{autoRefresh}
bind:result
runnable={{
name,
@@ -31,7 +28,6 @@
type: 'runnableByName'
}}
wrapperClass="hidden"
recomputable
>
<slot />
</RunnableComponent>

View File

@@ -3,9 +3,8 @@
import { deepEqual } from 'fast-equals'
import { getContext } from 'svelte'
import type { AppInput, EvalAppInput, UploadAppInput } from '../../inputType'
import type { AppViewerContext } from '../../types'
import type { AppEditorContext } from '../../types'
import { accessPropertyByPath } from '../../utils'
import { computeGlobalContext, eval_like } from './eval'
type T = string | number | boolean | Record<string | number, any> | undefined
@@ -13,7 +12,6 @@
export let value: T
export let id: string | undefined = undefined
export let error: string = ''
export let extraContext: Record<string, any> = {}
let lastInput = input ? JSON.parse(JSON.stringify(input)) : undefined
@@ -25,13 +23,13 @@
}
}
const { worldStore, state, mode } = getContext<AppViewerContext>('AppViewerContext')
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: stateId = $worldStore?.state
$: state = $worldStore?.state
let timeout: NodeJS.Timeout | undefined = undefined
const debounce_ms = 50
function debounce(cb: () => Promise<void>) {
function debounce(cb: () => void) {
if (timeout) {
clearTimeout(timeout)
}
@@ -41,22 +39,20 @@
$: lastInput && $worldStore && debounce(handleConnection)
$: lastInput &&
lastInput.type == 'template' &&
$stateId &&
$state &&
debounce(async () => (value = await getValue(lastInput)))
debounce(() => (value = getValue(lastInput)))
$: lastInput &&
lastInput.type == 'eval' &&
$stateId &&
state &&
debounce(async () => (value = await evalExpr(lastInput)))
$state &&
debounce(() => (value = evalExpr(lastInput)))
async function handleConnection() {
function handleConnection() {
if (lastInput.type === 'connected') {
$worldStore?.connect<any>(lastInput, onValueChange)
} else if (lastInput.type === 'static' || lastInput.type == 'template') {
value = await getValue(lastInput)
value = getValue(lastInput)
} else if (lastInput.type == 'eval') {
value = await evalExpr(lastInput as EvalAppInput)
value = evalExpr(lastInput as EvalAppInput)
} else if (lastInput.type == 'upload') {
value = (lastInput as UploadAppInput).value
} else {
@@ -64,15 +60,9 @@
}
}
async function evalExpr(input: EvalAppInput) {
function evalExpr(input: EvalAppInput) {
try {
const r = await eval_like(
input.expr,
computeGlobalContext($worldStore, id, extraContext),
true,
$state,
$mode == 'dnd'
)
const r = eval_like(input.expr, computeGlobalContext())
error = ''
return r
} catch (e) {
@@ -81,16 +71,23 @@
}
}
async function getValue(input: AppInput) {
function computeGlobalContext() {
return Object.fromEntries(
Object.entries($worldStore?.outputsById ?? {})
.filter(([k, _]) => k != id)
.map(([key, value]) => {
return [
key,
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
]
})
)
}
export function getValue(input: AppInput) {
if (input.type === 'template' && isCodeInjection(input.eval)) {
try {
const r = await eval_like(
'`' + input.eval + '`',
computeGlobalContext($worldStore, id, extraContext),
true,
$state,
$mode == 'dnd'
)
const r = eval_like('`' + input.eval + '`', computeGlobalContext())
error = ''
return r
} catch (e) {
@@ -103,6 +100,31 @@
}
}
function create_context_function_template(eval_string, context) {
return `
return function (context) {
"use strict";
${
Object.keys(context).length > 0
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
: ``
}
return ${eval_string};
}
`
}
function make_context_evaluator(eval_string, context) {
let template = create_context_function_template(eval_string, context)
let functor = Function(template)
return functor()
}
function eval_like(text, context = {}) {
let evaluator = make_context_evaluator(text, context)
return evaluator(context)
}
function onValueChange(newValue: any): void {
if (lastInput.type === 'connected' && newValue !== undefined && newValue !== null) {
const { connection } = lastInput

View File

@@ -1,19 +1,16 @@
<script lang="ts">
import { Loader2 } from 'lucide-svelte'
import { getContext } from 'svelte'
import { fade } from 'svelte/transition'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext } from '../../types'
import type { AppEditorContext } from '../../types'
import InputValue from './InputValue.svelte'
export let componentInput: AppInput
export let id: string
export let result: any
export let render: boolean
// Sync the result to the output
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: outputs = $worldStore?.outputsById[id] as {
loading: Output<boolean>
@@ -35,16 +32,4 @@
<InputValue {id} input={componentInput} bind:value={result} />
{/if}
{#if render}
<slot />
{:else}
<div class="w-full h-full">
<div
out:fade|local={{ duration: 50 }}
class="absolute inset-0 center-center flex-col bg-white text-gray-600 border"
>
<Loader2 class="animate-spin" size={16} />
<span class="text-xs mt-1">Loading</span>
</div>
</div>
{/if}
<slot />

View File

@@ -1,11 +1,13 @@
<script lang="ts">
import { RefreshCw } from 'lucide-svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
import { RefreshCcw, RefreshCw } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../types'
import type { AppEditorContext } from '../../types'
export let componentId: string
const { runnableComponents, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
async function refresh() {
window.dispatchEvent(new Event('pointerup'))

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { getContext } from 'svelte'
import { findGridItem } from '../../editor/appUtils'
import type { AppViewerContext } from '../../types'
import type { AppEditorContext } from '../../types'
export let id: string
export let shouldWrap: boolean = false
const { app, breakpoint } = getContext<AppViewerContext>('AppViewerContext')
const { app, breakpoint } = getContext<AppEditorContext>('AppEditorContext')
$: gridItem = findGridItem($app, id)

View File

@@ -8,11 +8,10 @@
import { AppService, type CompletedJob } from '$lib/gen'
import { classNames, defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { Bug } from 'lucide-svelte'
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import { getContext, onMount } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { AppViewerContext } from '../../types'
import { computeGlobalContext, eval_like } from './eval'
import type { Output } from '../../rx'
import type { AppEditorContext } from '../../types'
import InputValue from './InputValue.svelte'
import RefreshButton from './RefreshButton.svelte'
@@ -30,9 +29,6 @@
export let initializing: boolean | undefined = undefined
export let gotoUrl: string | undefined = undefined
export let gotoNewTab: boolean | undefined = undefined
export let render: boolean
export let recomputable: boolean = false
export let recomputeIds: string[] = []
const {
worldStore,
@@ -44,27 +40,17 @@
noBackend,
errorByComponent,
mode,
stateId,
state
} = getContext<AppViewerContext>('AppViewerContext')
stateId
} = getContext<AppEditorContext>('AppEditorContext')
$: autoRefresh && handleAutorefresh()
if (recomputable) {
$runnableComponents[id] = async () => {
await executeComponent(true)
}
$runnableComponents = $runnableComponents
}
function handleAutorefresh() {
if (autoRefresh && $worldStore) {
onMount(() => {
if (autoRefresh) {
$runnableComponents[id] = async () => {
await executeComponent(true)
}
executeComponent(true)
}
}
})
let args: Record<string, any> | undefined = undefined
let testIsLoading = false
@@ -72,6 +58,7 @@
let executeTimeout: NodeJS.Timeout | undefined = undefined
function setDebouncedExecute() {
console.log('EXECUTE')
executeTimeout && clearTimeout(executeTimeout)
executeTimeout = setTimeout(() => {
executeComponent(true)
@@ -92,23 +79,31 @@
$: fields && (currentStaticValues = computeStaticValues())
$: if (JSON.stringify(currentStaticValues) != JSON.stringify(lazyStaticValues)) {
lazyStaticValues = currentStaticValues
refreshIfAutoRefresh()
}
function refreshIfAutoRefresh() {
if (autoRefresh) {
setDebouncedExecute()
}
}
$: fields && (lazyStaticValues = computeStaticValues())
$: (runnableInputValues || extraQueryParams || args) && testJobLoader && refreshIfAutoRefresh()
$: console.log(runnableInputValues, extraQueryParams, args, autoRefresh, testJobLoader)
$: (runnableInputValues || extraQueryParams || args) &&
autoRefresh &&
testJobLoader &&
setDebouncedExecute()
// Test job internal state
let testJob: CompletedJob | undefined = undefined
let testJobLoader: TestJobLoader | undefined = undefined
$: outputs = initOutput($worldStore, id, { result: undefined, loading: false })
$: outputs = $worldStore?.outputsById[id] as {
result: Output<Array<any>>
loading: Output<boolean>
}
$: if (outputs?.loading != undefined) {
outputs.loading.set(false, true)
}
$: outputs?.loading?.set(testIsLoading)
$: schemaStripped = stripSchema(fields, $stateId)
@@ -149,24 +144,6 @@
)
async function executeComponent(noToast = false) {
if (runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend') {
outputs?.loading?.set(true)
try {
const r = await eval_like(
runnable.inlineScript?.content,
computeGlobalContext($worldStore, id, {}),
false,
$state,
$mode == 'dnd'
)
setResult(r)
$state = $state
} catch (e) {
sendUserToast('Error running frontend script: ' + e.message, true)
}
outputs?.loading?.set(false)
return
}
if (noBackend) {
if (!noToast) {
sendUserToast('This app is not connected to a windmill backend, it is a static preview')
@@ -239,31 +216,6 @@
}
}
function setResult(res: any) {
outputs.result?.set(res)
result = res
const previousJobId = Object.keys($errorByComponent).find(
(key) => $errorByComponent[key].componentId === id
)
if (previousJobId && !result?.error) {
delete $errorByComponent[previousJobId]
$errorByComponent = $errorByComponent
}
if (gotoUrl && gotoUrl != '' && result?.error == undefined) {
if (gotoNewTab) {
window.open(gotoUrl, '_blank')
} else {
goto(gotoUrl)
}
}
if (recomputeIds) {
recomputeIds.map((id) => $runnableComponents?.[id]?.())
}
}
$: result?.error && recordError(result.error)
</script>
@@ -280,7 +232,24 @@
const startedAt = new Date(testJob.started_at).getTime()
if (startedAt > lastStartedAt) {
lastStartedAt = startedAt
setResult(e.detail.result)
outputs.result?.set(testJob?.result)
result = testJob.result
const previousJobId = Object.keys($errorByComponent).find(
(key) => $errorByComponent[key].componentId === id
)
if (previousJobId && !result?.error) {
delete $errorByComponent[previousJobId]
$errorByComponent = $errorByComponent
}
if (gotoUrl && gotoUrl != '' && result?.error == undefined) {
if (gotoNewTab) {
window.open(gotoUrl, '_blank')
} else {
goto(gotoUrl)
}
}
}
}
}}
@@ -289,61 +258,57 @@
bind:this={testJobLoader}
/>
{#if render}
<div class="h-full flex relative flex-row flex-wrap {wrapperClass}" style={wrapperStyle}>
{#if schemaStripped && Object.keys(schemaStripped?.properties ?? {}).length > 0 && (autoRefresh || forceSchemaDisplay)}
<div class="px-2 h-fit min-h-0">
<SchemaForm
{flexWrap}
schema={schemaStripped}
bind:args
{disabledArgs}
shouldHideNoInputs
noVariablePicker
/>
</div>
{/if}
<div class="h-full flex relative flex-row flex-wrap {wrapperClass}" style={wrapperStyle}>
{#if schemaStripped && Object.keys(schemaStripped?.properties ?? {}).length > 0 && (autoRefresh || forceSchemaDisplay)}
<div class="px-2 h-fit min-h-0">
<SchemaForm
{flexWrap}
schema={schemaStripped}
bind:args
{disabledArgs}
shouldHideNoInputs
noVariablePicker
/>
</div>
{/if}
{#if !runnable && autoRefresh}
<Alert type="warning" size="xs" class="mt-2 px-1" title="Missing runnable">
Please select a runnable
</Alert>
{:else if result?.error && $mode === 'preview'}
<div
title="Error"
class={classNames(
'text-red-500 px-1 text-2xs py-0.5 font-bold w-fit absolute border border-red-500 -bottom-2 shadow left-1/2 transform -translate-x-1/2 z-50 cursor-pointer',
'bg-red-100/80'
)}
>
<Popover notClickable placement="bottom" popupClass="!bg-white border w-96">
<Bug size={14} />
<span slot="text">
<div class="bg-white">
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
An error occured, please contact the app author.
<span class="font-semibold">Job id: {testJob?.id}</span>
</div>
</Alert>
</div>
</span>
</Popover>
</div>
<div class="block grow w-full max-h-full border border-red-300 bg-red-50 relative">
<slot />
</div>
{:else}
<div class="block grow w-full max-h-full">
<slot />
</div>
{/if}
{#if !initializing && autoRefresh === true}
<div class="flex absolute top-1 right-1 z-50">
<RefreshButton componentId={id} />
</div>
{/if}
</div>
{:else}
<div class="w-full h-full" />
{/if}
{#if !runnable && autoRefresh}
<Alert type="warning" size="xs" class="mt-2 px-1" title="Missing runnable">
Please select a runnable
</Alert>
{:else if result?.error && $mode === 'preview'}
<div
title="Error"
class={classNames(
'text-red-500 px-1 text-2xs py-0.5 font-bold w-fit absolute border border-red-500 -bottom-2 shadow left-1/2 transform -translate-x-1/2 z-50 cursor-pointer',
'bg-red-100/80'
)}
>
<Popover notClickable placement="bottom" popupClass="!bg-white border w-96">
<Bug size={14} />
<span slot="text">
<div class="bg-white">
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
An error occured, please contact the app author.
<span class="font-semibold">Job id: {testJob?.id}</span>
</div>
</Alert>
</div>
</span>
</Popover>
</div>
<div class="block grow w-full max-h-full border border-red-300 bg-red-50 relative">
<slot />
</div>
{:else}
<div class="block grow w-full max-h-full">
<slot />
</div>
{/if}
{#if !initializing && autoRefresh === true}
<div class="flex absolute top-1 right-1 z-50">
<RefreshButton componentId={id} />
</div>
{/if}
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import type { AppInput } from '../../inputType'
import type { AppViewerContext } from '../../types'
import type { AppEditorContext } from '../../types'
import { isScriptByNameDefined, isScriptByPathDefined } from '../../utils'
import NonRunnableComponent from './NonRunnableComponent.svelte'
import RunnableComponent from './RunnableComponent.svelte'
@@ -20,10 +20,8 @@
export let runnableStyle = ''
export let goto: string | undefined = undefined
export let gotoNewTab: boolean | undefined = undefined
export let render: boolean
export let recomputeIds: string[] = []
const { staticExporter, noBackend } = getContext<AppViewerContext>('AppViewerContext')
const { staticExporter, noBackend } = getContext<AppEditorContext>('AppEditorContext')
$: if (initializing && result) {
initializing = false
@@ -45,7 +43,6 @@
<slot />
{:else if componentInput.type === 'runnable' && isRunnableDefined()}
<RunnableComponent
{recomputeIds}
gotoUrl={goto}
{gotoNewTab}
{flexWrap}
@@ -60,12 +57,11 @@
{initializing}
wrapperClass={runnableClass}
wrapperStyle={runnableStyle}
{render}
>
<slot />
</RunnableComponent>
{:else}
<NonRunnableComponent {render} bind:result {id} {componentInput}>
<NonRunnableComponent bind:result {id} {componentInput}>
<slot />
</NonRunnableComponent>
{/if}

View File

@@ -1,69 +0,0 @@
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/utils'
import type { World } from '../../rx'
export function computeGlobalContext(
world: World | undefined,
id: string | undefined,
extraContext: any = {}
) {
return {
...Object.fromEntries(
Object.entries(world?.outputsById ?? {})
.filter(([k, _]) => k != id)
.map(([key, value]) => {
return [
key,
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
]
})
),
...extraContext
}
}
function create_context_function_template(eval_string, context, noReturn: boolean) {
return `
return async function (context, state, goto) {
"use strict";
${
Object.keys(context).length > 0
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
: ``
}
${noReturn ? `return ${eval_string}` : eval_string}
}
`
}
function make_context_evaluator(
eval_string,
context,
noReturn: boolean
): (context, state, goto) => Promise<any> {
let template = create_context_function_template(eval_string, context, noReturn)
let functor = Function(template)
return functor()
}
export async function eval_like(
text,
context = {},
noReturn: boolean,
state: any,
editor: boolean
) {
let evaluator = make_context_evaluator(text, context, noReturn)
return await evaluator(context, state, async (x, newTab) => {
if (newTab || editor) {
if (!newTab) {
sendUserToast(
'In editor mode, `goto` opens a new tab to prevent losing your work. To test the redirection , use the preview mode.'
)
}
window.open(x, '_blank')
} else {
await goto(x)
}
})
}

View File

@@ -3,7 +3,7 @@
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
@@ -13,9 +13,8 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let customCss: ComponentCustomCSS<'text'> | undefined = undefined
export let render: boolean
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
export const staticOutputs: string[] = ['result']
@@ -37,7 +36,7 @@
<InputValue {id} input={configuration.label} bind:value={labelValue} />
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()

View File

@@ -3,38 +3,36 @@
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputDefaultValue from '../helpers/InputDefaultValue.svelte'
import InputValue from '../helpers/InputValue.svelte'
export let id: string
export let configuration: Record<string, AppInput>
export let inputType: 'date'
export let inputType: 'date' | 'time' | 'datetime-local'
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let input: HTMLInputElement
let labelValue: string = 'Title'
let minValue: string = ''
let maxValue: string = ''
let defaultValue: string | undefined = undefined
let value: string | undefined = undefined
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string | undefined>
result: Output<string>
}
$: handleDefault(defaultValue)
$: outputs?.result.set(value)
function handleDefault(defaultValue: string | undefined) {
value = defaultValue
function handleInput() {
outputs?.result.set(input.value)
}
$: input && handleInput()
$: css = concatCustomCss($app.css?.dateinputcomponent, customCss)
</script>
@@ -43,18 +41,17 @@
<InputValue {id} input={configuration.maxDate} bind:value={maxValue} />
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
<AlignWrapper {render} {verticalAlignment}>
{#if inputType === 'date'}
<input
on:focus={() => ($selectedComponent = id)}
on:pointerdown|stopPropagation
type="date"
bind:value
min={minValue}
max={maxValue}
placeholder="Type..."
class={twMerge('mx-0.5', css?.input?.class ?? '')}
style={css?.input?.style ?? ''}
/>
{/if}
<InputDefaultValue bind:input {defaultValue} />
<AlignWrapper {verticalAlignment}>
<input
type={inputType}
bind:this={input}
on:input={handleInput}
min={minValue}
max={maxValue}
placeholder="Type..."
class={twMerge('mx-0.5', css?.input?.class ?? '')}
style={css?.input?.style ?? ''}
/>
</AlignWrapper>

View File

@@ -4,7 +4,7 @@
import { FileInput } from '../../../common'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
@@ -12,9 +12,8 @@
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let acceptedFileTypes: string[] | undefined = undefined
let allowMultiple: boolean | undefined = undefined
@@ -36,19 +35,17 @@
<InputValue {id} input={configuration.allowMultiple} bind:value={allowMultiple} />
<InputValue {id} input={configuration.text} bind:value={text} />
{#if render}
<div class="w-full h-full p-1">
<FileInput
accept={acceptedFileTypes?.length ? acceptedFileTypes?.join(', ') : undefined}
multiple={allowMultiple}
convertTo="base64"
on:change={({ detail }) => {
handleChange(detail)
}}
class={twMerge('w-full h-full', css?.container?.class)}
style={css?.container?.style}
>
{text}
</FileInput>
</div>
{/if}
<div class="w-full h-full p-1">
<FileInput
accept={acceptedFileTypes?.length ? acceptedFileTypes?.join(', ') : undefined}
multiple={allowMultiple}
convertTo="base64"
on:change={({ detail }) => {
handleChange(detail)
}}
class={twMerge('w-full h-full', css?.container?.class)}
style={css?.container?.style}
>
{text}
</FileInput>
</div>

View File

@@ -3,11 +3,12 @@
import Select from 'svelte-select'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
import { stopPropagation } from 'ol/events/Event'
export const staticOutputs: string[] = ['result']
export let id: string
@@ -15,10 +16,9 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
const { app, worldStore, connectingInput, selectedComponent } =
getContext<AppViewerContext>('AppViewerContext')
getContext<AppEditorContext>('AppEditorContext')
let items: { label: string; value: string }[]
let placeholder: string = 'Select an item'
@@ -57,7 +57,7 @@
<InputValue {id} input={configuration.items} bind:value={labels} />
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<div class="app-select w-full mx-0.5" style="height: 34px" on:pointerdown|stopPropagation>
{#if !value || Array.isArray(value)}
<Select

View File

@@ -3,9 +3,10 @@
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputDefaultValue from '../helpers/InputDefaultValue.svelte'
import InputValue from '../helpers/InputValue.svelte'
export let id: string
@@ -13,30 +14,29 @@
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let input: HTMLInputElement
let defaultValue: number | undefined = undefined
let placeholder: string | undefined = undefined
let value: number | undefined = undefined
let min: number | undefined = undefined
let max: number | undefined = undefined
let step = 1
$: outputs = $worldStore?.outputsById[id] as {
result: Output<number | undefined>
result: Output<number | null>
}
$: handleDefault(defaultValue)
$: outputs?.result.set(value)
function handleDefault(defaultValue: number | undefined) {
value = defaultValue
function handleInput() {
const value = input?.value
const num = isNaN(+value) ? null : +value
outputs?.result.set(num)
}
$: input && handleInput()
$: css = concatCustomCss($app.css?.numberinputcomponent, customCss)
</script>
@@ -45,17 +45,21 @@
<InputValue {id} input={configuration.max} bind:value={max} />
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
<InputDefaultValue bind:input {defaultValue} />
<AlignWrapper {render} {verticalAlignment}>
<AlignWrapper {verticalAlignment}>
<input
on:pointerdown|stopPropagation
on:focus={() => ($selectedComponent = id)}
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 mx-0.5',
css?.input?.class ?? ''
)}
style={css?.input?.style ?? ''}
bind:value
bind:this={input}
on:input={handleInput}
on:focus={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
{min}
{max}
{step}

View File

@@ -3,7 +3,7 @@
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import { concatCustomCss } from '../../utils'
@@ -15,9 +15,8 @@
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'handles' | 'bar' | 'limits' | 'values'> | undefined =
undefined
export let render: boolean
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let min = 0
let max = 42
let step = 1
@@ -49,7 +48,7 @@
<InputValue {id} input={configuration.defaultLow} bind:value={values[0]} />
<InputValue {id} input={configuration.defaultHigh} bind:value={values[1]} />
<AlignWrapper {render} {verticalAlignment}>
<AlignWrapper {verticalAlignment}>
<div class="flex flex-col w-full">
<div class="flex items-center w-full gap-1 px-1">
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>

View File

@@ -3,11 +3,13 @@
import Select from 'svelte-select'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
import { twMerge } from 'tailwind-merge'
import Portal from 'svelte-portal'
export const staticOutputs: string[] = ['result']
export let id: string
@@ -15,10 +17,9 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
const { app, worldStore, connectingInput, selectedComponent } =
getContext<AppViewerContext>('AppViewerContext')
getContext<AppEditorContext>('AppEditorContext')
let items: { label: string; value: any; created?: boolean }[]
let placeholder: string = 'Select an item'
@@ -43,8 +44,6 @@
: []
if (defaultValue) {
value = JSON.stringify(defaultValue)
} else if (listItems.length > 0) {
value = listItems[0]?.value
}
}
@@ -99,7 +98,7 @@
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
<InputValue {id} input={configuration.create} bind:value={create} />
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<div class="app-select w-full mx-0.5" style="height: 34px;" on:pointerdown|stopPropagation>
<Select
--border-radius="0"

View File

@@ -3,7 +3,7 @@
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import { twMerge } from 'tailwind-merge'
@@ -15,9 +15,8 @@
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'handle' | 'limits' | 'value' | 'bar'> | undefined =
undefined
export let render: boolean
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
let min = 0
let max = 42
let step = 1
@@ -57,7 +56,7 @@
<InputValue {id} input={configuration.max} bind:value={max} />
<InputValue {id} input={configuration.defaultValue} bind:value={values[0]} />
<AlignWrapper {render} {verticalAlignment}>
<AlignWrapper {verticalAlignment}>
<div class="flex items-center w-full gap-1 px-1">
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
{+min}

View File

@@ -3,9 +3,10 @@
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputDefaultValue from '../helpers/InputDefaultValue.svelte'
import InputValue from '../helpers/InputValue.svelte'
export let id: string
@@ -14,73 +15,45 @@
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let appCssKey: 'textinputcomponent' | 'passwordinputcomponent' | 'emailinputcomponent' =
'textinputcomponent'
export let render: boolean
export let appCssKey: 'textinputcomponent' | 'passwordinputcomponent' = 'textinputcomponent'
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let input: HTMLInputElement
let placeholder: string | undefined = undefined
let defaultValue: string | undefined = undefined
let value: string | undefined = undefined
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string | undefined>
result: Output<string>
}
$: handleDefault(defaultValue)
$: outputs?.result.set(value)
function handleDefault(defaultValue: string | undefined) {
value = defaultValue
function handleInput() {
outputs?.result.set(input.value)
}
$: input && handleInput()
$: css = concatCustomCss($app.css?.[appCssKey], customCss)
</script>
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
<InputDefaultValue bind:input {defaultValue} />
<AlignWrapper {render} {verticalAlignment}>
{#if inputType === 'password'}
<input
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 mx-0.5',
css?.input?.class ?? ''
)}
style={css?.input?.style ?? ''}
on:pointerdown|stopPropagation
on:focus={() => ($selectedComponent = id)}
type="password"
bind:value
{placeholder}
/>
{:else if inputType === 'text'}
<input
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 mx-0.5',
css?.input?.class ?? ''
)}
style={css?.input?.style ?? ''}
on:pointerdown|stopPropagation
on:focus={() => ($selectedComponent = id)}
type="text"
bind:value
{placeholder}
/>
{:else if inputType === 'email'}
<input
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 mx-0.5',
css?.input?.class ?? ''
)}
style={css?.input?.style ?? ''}
on:pointerdown|stopPropagation
on:focus={() => ($selectedComponent = id)}
type="email"
bind:value
{placeholder}
/>
{/if}
<AlignWrapper {verticalAlignment}>
<input
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 mx-0.5',
css?.input?.class ?? ''
)}
style={css?.input?.style ?? ''}
on:focus={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
type={inputType}
bind:this={input}
on:input={handleInput}
{placeholder}
/>
</AlignWrapper>

View File

@@ -3,7 +3,7 @@
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../../inputType'
import type { Output } from '../../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../../types'
import { concatCustomCss } from '../../../utils'
import AlignWrapper from '../../helpers/AlignWrapper.svelte'
import InputValue from '../../helpers/InputValue.svelte'
@@ -14,9 +14,8 @@
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let defaultValue: number | undefined = undefined
@@ -50,7 +49,7 @@
<InputValue {id} input={configuration.currency} bind:value={currency} />
<InputValue {id} input={configuration.locale} bind:value={locale} />
<AlignWrapper {render} {verticalAlignment}>
<AlignWrapper {verticalAlignment}>
{#key isNegativeAllowed}
{#key locale}
{#key currency}

View File

@@ -2,7 +2,7 @@
import { getContext } from 'svelte'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, GridItem } from '../../types'
import type { AppEditorContext, ComponentCustomCSS, GridItem } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
@@ -10,12 +10,11 @@
export let configuration: Record<string, AppInput>
export let componentContainerHeight: number
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
let noPadding: boolean | undefined = undefined
export const staticOutputs: string[] = []
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, focusedGrid, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
function onFocus() {
$focusedGrid = {
@@ -31,10 +30,9 @@
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
<div>
<div class="border">
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={render}
{noPadding}
{id}
class={css?.container.class}

View File

@@ -3,7 +3,7 @@
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../inputType'
import type {
AppViewerContext,
AppEditorContext,
ComponentCustomCSS,
HorizontalAlignment,
VerticalAlignment
@@ -18,9 +18,8 @@
export let verticalAlignment: VerticalAlignment | undefined = undefined
export let customCss: ComponentCustomCSS<'container' | 'divider'> | undefined = undefined
export let position: 'horizontal' | 'vertical'
export let render: boolean
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let size = 2
let color = '#00000060'
@@ -35,7 +34,6 @@
{verticalAlignment}
class={twMerge(css?.container?.class, 'h-full')}
style={css?.container?.style}
{render}
>
<div
class={twMerge(

View File

@@ -2,7 +2,7 @@
import { getContext } from 'svelte'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import InputValue from '../helpers/InputValue.svelte'
import Portal from 'svelte-portal'
import { concatCustomCss } from '../../utils'
@@ -16,9 +16,8 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let noWFull = false
export let render: boolean
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, focusedGrid, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
let gridContent: string[] | undefined = undefined
let noPadding: boolean | undefined = undefined
@@ -70,7 +69,7 @@
<InputValue {id} input={configuration.fillContainer} bind:value={fillContainer} />
<Portal target="#app-editor-top-level-drawer">
<Drawer let:open bind:this={appDrawer} size="800px" alwaysOpen positionClass="!absolute">
<Drawer bind:this={appDrawer} size="800px" alwaysOpen positionClass="!absolute">
<DrawerContent
title={drawerTitle}
on:close={() => {
@@ -80,7 +79,6 @@
>
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{noPadding}
{id}
class={css?.container.class}

View File

@@ -2,7 +2,7 @@
import { getContext } from 'svelte'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppInput } from '../../inputType'
import type { AppEditorContext, AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
@@ -14,11 +14,10 @@
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let horizontal: boolean = false
export let panes: number[]
export let render: boolean
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
export const staticOutputs: string[] = []
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
const { app, focusedGrid, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
let noPadding: boolean | undefined = undefined
@@ -32,31 +31,6 @@
$: $selectedComponent === id && onFocus()
$: css = concatCustomCss($app.css?.containercomponent, customCss)
$componentControl[id] = {
left: () => {
if ($focusedGrid?.subGridIndex) {
const index = $focusedGrid?.subGridIndex ?? 0
if (index > 0) {
$focusedGrid.subGridIndex = index - 1
return true
}
}
return false
},
right: () => {
// subGridIndex can be 0
if ($focusedGrid?.subGridIndex !== undefined) {
const index = $focusedGrid?.subGridIndex ?? 0
if (index < panes.length - 1) {
$focusedGrid.subGridIndex = index + 1
return true
}
}
return false
}
}
let sumedup = [50, 50]
$: {
let ns = panes.map((x) => (x / panes.reduce((a, b) => a + b, 0)) * 100)
@@ -74,7 +48,6 @@
<Pane size={paneSize} minSize={20}>
{#if $app.subgrids?.[`${id}-${index}`]}
<SubGridEditor
visible={render}
noYPadding
{noPadding}
{id}

View File

@@ -1,11 +1,10 @@
<script lang="ts">
import { Tab, Tabs } from '$lib/components/common'
import { getContext, onMount } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import { getContext } from 'svelte'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext, AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
@@ -16,82 +15,63 @@
export let customCss:
| ComponentCustomCSS<'tabRow' | 'tabs' | 'selectedTab' | 'container'>
| undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['selectedTabIndex']
const { app, worldStore, focusedGrid, selectedComponent } =
getContext<AppViewerContext>('AppViewerContext')
getContext<AppEditorContext>('AppEditorContext')
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
let selected: string = tabs[0]
let selected: string = ''
let noPadding: boolean | undefined = undefined
let tabHeight: number = 0
$: outputs = $worldStore
? initOutput($worldStore, id, {
selectedTabIndex: 0
})
: undefined
function handleTabSelection() {
const selectedIndex = tabs?.indexOf(selected)
outputs?.selectedTabIndex.set(selectedIndex, true)
if ($selectedComponent != id) {
$selectedComponent = id
}
if ($focusedGrid?.parentComponentId != id || $focusedGrid?.subGridIndex != selectedIndex) {
$focusedGrid = {
parentComponentId: id,
subGridIndex: selectedIndex
}
}
}
$: $selectedComponent === id && focusGrid()
function focusGrid() {
const selectedIndex = tabs?.indexOf(selected)
if ($focusedGrid?.parentComponentId != id || $focusedGrid?.subGridIndex != selectedIndex) {
$focusedGrid = {
parentComponentId: id,
subGridIndex: selectedIndex
}
}
}
$componentControl[id] = {
left: () => {
const index = tabs.indexOf(selected)
if (index > 0) {
selected = tabs[index - 1]
return true
}
return false
},
right: () => {
const index = tabs.indexOf(selected)
if (index < tabs.length - 1) {
selected = tabs[index + 1]
return true
}
return false
}
}
$: selected && handleTabSelection()
$: selectedIndex = tabs?.indexOf(selected) ?? -1
$: if ((tabs && selected === '') || !tabs.includes(selected)) {
selected = tabs[0]
}
$: outputs = $worldStore?.outputsById[id] as {
selectedTabIndex: Output<number | null>
}
$: outputs?.selectedTabIndex && handleOutputs()
function handleTabSelection() {
outputs?.selectedTabIndex.set(selectedIndex)
if ($selectedComponent != id) {
$selectedComponent = id
}
if ($focusedGrid?.parentComponentId != id || $focusedGrid?.subGridIndex != selectedIndex) {
$focusedGrid = {
parentComponentId: id,
subGridIndex: selectedIndex
}
}
}
$: selectedIndex >= 0 && handleTabSelection()
let tabHeight: number = 0
function onFocus() {
$focusedGrid = {
parentComponentId: id,
subGridIndex: selectedIndex ?? 0
}
}
$: $selectedComponent === id && selectedIndex >= 0 && onFocus()
$: css = concatCustomCss($app.css?.tabscomponent, customCss)
function handleOutputs() {
outputs?.selectedTabIndex.set(outputs.selectedTabIndex.peak())
}
</script>
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
<div>
<div bind:clientHeight={tabHeight}>
<div bind:clientHeight={tabHeight} on:pointerdown|stopPropagation>
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
{#each tabs ?? [] as res}
<Tab
@@ -110,7 +90,7 @@
{#each tabs ?? [] as res, i}
<SubGridEditor
{id}
visible={render && i === selectedIndex}
visible={i === selectedIndex}
bind:subGrid={$app.subgrids[`${id}-${i}`]}
{noPadding}
class={css?.container?.class}

View File

@@ -4,12 +4,11 @@
import { twMerge } from 'tailwind-merge'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { writable, type Writable } from 'svelte/store'
import { writable } from 'svelte/store'
import { buildWorld, type World } from '../rx'
import type {
App,
AppEditorContext,
AppViewerContext,
ConnectingInput,
EditorBreakpoint,
EditorMode,
@@ -33,13 +32,11 @@
import SettingsPanel from './SettingsPanel.svelte'
import { fly } from 'svelte/transition'
import { VariableService, type Policy } from '$lib/gen'
import type { Policy } from '$lib/gen'
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import { page } from '$app/stores'
import CssSettings from './componentsPanel/CssSettings.svelte'
import { initHistory } from '$lib/history'
import ComponentNavigation from './component/ComponentNavigation.svelte'
import ItemPicker from '$lib/components/ItemPicker.svelte'
import VariableEditor from '$lib/components/VariableEditor.svelte'
export let app: App
export let path: string
@@ -65,9 +62,8 @@
const runnableComponents = writable<Record<string, () => Promise<void>>>({})
const errorByComponent = writable<Record<string, { error: string; componentId: string }>>({})
const focusedGrid = writable<FocusedGrid | undefined>(undefined)
const pickVariableCallback: Writable<((path: string) => void) | undefined> = writable(undefined)
setContext<AppViewerContext>('AppViewerContext', {
setContext<AppEditorContext>('AppEditorContext', {
worldStore,
staticOutputs,
app: appStore,
@@ -89,13 +85,7 @@
focusedGrid,
stateId: writable(0),
parentWidth: writable(0),
state: writable({})
})
setContext<AppEditorContext>('AppEditorContext', {
history,
componentControl: writable({}),
pickVariableCallback
history
})
let timeout: NodeJS.Timeout | undefined = undefined
@@ -119,16 +109,10 @@
mounted = true
})
let context = {
$: context = {
email: $userStore?.email,
username: $userStore?.username,
query: Object.fromEntries($page.url.searchParams.entries()),
hash: $page.url.hash
}
function hashchange(e: HashChangeEvent) {
context.hash = e.newURL.split('#')[1]
context = context
query: Object.fromEntries($page.url.searchParams.entries())
}
$: mounted && ($worldStore = buildWorld($staticOutputs, $worldStore, context))
@@ -141,200 +125,152 @@
} else {
selectedTab = 'insert'
}
let itemPicker: ItemPicker | undefined = undefined
$: if ($pickVariableCallback) {
itemPicker?.openDrawer()
}
let variableEditor: VariableEditor | undefined = undefined
</script>
<svelte:window on:hashchange={hashchange} />
{#if $connectingInput.opened}
<div
class="absolute w-full h-screen bg-black border-2 bg-opacity-25 z-20 flex justify-center items-center"
/>
{/if}
{#if !$userStore?.operator}
{#if $appStore}
{#if initialMode !== 'preview'}
<AppEditorHeader {policy} {fromHub} />
{/if}
<UnsavedConfirmationModal />
{#if initialMode !== 'preview'}
<AppEditorHeader {policy} {fromHub} />
{/if}
{#if previewing}
<SplitPanesWrapper>
<div
class={twMerge('h-full w-full', $appStore.css?.['app']?.['viewer']?.class)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<AppPreview
workspace={$workspaceStore ?? ''}
summary={$summaryStore}
app={$appStore}
appPath={path}
{breakpoint}
{policy}
isEditor
{context}
noBackend={false}
/>
</div>
</SplitPanesWrapper>
{:else}
<SplitPanesWrapper>
<Splitpanes class="max-w-full overflow-hidden">
<Pane size={$connectingInput?.opened ? 40 : 15} minSize={5} maxSize={33}>
<ContextPanel />
</Pane>
<Pane size={64}>
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={$connectingInput?.opened ? 100 : 70}>
<div
on:pointerdown={(e) => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
class={twMerge(
'bg-gray-100 h-full w-full',
$appStore.css?.['app']?.['viewer']?.class
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<div
class={classNames(
'relative mx-auto w-full h-full overflow-auto',
app.fullscreen ? '' : 'max-w-6xl'
)}
>
{#if $appStore.grid}
<ComponentNavigation />
<div on:pointerdown|stopPropagation class={width}>
<GridEditor {policy} />
</div>
<div id="app-editor-top-level-drawer" />
{/if}
</div>
</div>
</Pane>
<Pane size={$connectingInput?.opened ? 0 : 30}>
<div class="relative h-full w-full">
<InlineScriptsPanel />
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
</Pane>
<Pane size={21} minSize={5} maxSize={33}>
<div class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="!h-[40px]" class="!h-full">
<Tab value="insert" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<Tab value="css" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faCode} />
<span>CSS</span>
</div>
</Tab>
<div slot="content" class="h-full overflow-y-auto pb-4">
<TabContent class="overflow-auto h-full" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
{:else}
<div class="min-w-[150px] text-sm text-gray-500 text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
<TabContent value="css">
<CssSettings />
</TabContent>
</div>
</Tabs>
{#if $connectingInput.opened}
<div
class="fixed top-32 p-2 z-50 flex justify-center items-center"
transition:fly|local={{ duration: 100, y: -100 }}
>
<Alert title="Connecting" type="info">
<div class="flex gap-2 flex-col">
Click on the output of the component you want to connect to on the left panel.
<div>
<Button
color="blue"
variant="border"
size="xs"
on:click={() => {
$connectingInput.opened = false
$connectingInput.input = undefined
}}
>
Stop connecting
</Button>
</div>
</div>
</Alert>
</div>
{/if}
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
{/if}
{#if previewing}
<SplitPanesWrapper>
<div
class={twMerge('h-full w-full', $appStore.css?.['app']?.['viewer']?.class)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<AppPreview
workspace={$workspaceStore ?? ''}
summary={$summaryStore}
app={$appStore}
appPath={path}
{breakpoint}
{policy}
isEditor
{context}
noBackend={false}
/>
</div>
</SplitPanesWrapper>
{:else}
App is null
<SplitPanesWrapper>
<Splitpanes class="max-w-full overflow-hidden">
<Pane size={$connectingInput?.opened ? 40 : 15} minSize={5} maxSize={33}>
<ContextPanel />
</Pane>
<Pane size={64}>
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={$connectingInput?.opened ? 100 : 70}>
<div
on:pointerdown={(e) => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
class={twMerge(
'bg-gray-100 h-full w-full',
$appStore.css?.['app']?.['viewer']?.class
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<div
class={classNames(
'relative mx-auto w-full h-full overflow-auto',
app.fullscreen ? '' : 'max-w-6xl'
)}
>
{#if $appStore.grid}
<div on:pointerdown|stopPropagation class={width}>
<GridEditor {policy} />
</div>
<div id="app-editor-top-level-drawer" />
{/if}
</div>
</div>
</Pane>
<Pane size={$connectingInput?.opened ? 0 : 30}>
<div class="relative h-full w-full">
<InlineScriptsPanel />
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
</Pane>
<Pane size={21} minSize={5} maxSize={33}>
<div class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab} class="!border-b-2 !border-gray-200 !h-full">
<Tab value="insert" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<Tab value="css" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faCode} />
<span>CSS</span>
</div>
</Tab>
<div slot="content" class="h-full overflow-y-auto pb-4">
<TabContent class="overflow-auto" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
{:else}
<div class="min-w-[150px] text-sm text-gray-500 text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
<TabContent value="css">
<CssSettings />
</TabContent>
</div>
</Tabs>
{#if $connectingInput.opened}
<div
class="fixed top-32 p-2 z-50 flex justify-center items-center"
transition:fly|local={{ duration: 100, y: -100 }}
>
<Alert title="Connecting" type="info">
<div class="flex gap-2 flex-col">
Click on the output of the component you want to connect to on the left panel.
<div>
<Button
color="blue"
variant="border"
size="xs"
on:click={() => {
$connectingInput.opened = false
$connectingInput.input = undefined
}}
>
Stop connecting
</Button>
</div>
</div>
</Alert>
</div>
{/if}
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
{/if}
{:else}
App editor not available to operators
{/if}
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
$pickVariableCallback?.(path)
}}
itemName="Variable"
extraField="path"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
name: x.path,
...x
}))}
>
<div
slot="submission"
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?.()
}}
>
New variable
</Button>
</div>
</ItemPicker>
<VariableEditor bind:this={variableEditor} />

View File

@@ -7,8 +7,7 @@
ButtonPopup,
ButtonPopupItem,
Drawer,
DrawerContent,
UndoRedo
DrawerContent
} from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
@@ -18,7 +17,6 @@
import DisplayResult from '$lib/components/DisplayResult.svelte'
import Dropdown from '$lib/components/Dropdown.svelte'
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
import { idMutex } from '$lib/components/flows/flowStateUtils'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import JobArgs from '$lib/components/JobArgs.svelte'
import LogViewer from '$lib/components/LogViewer.svelte'
@@ -28,7 +26,7 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import { AppService, Job, Policy } from '$lib/gen'
import { redo, undo } from '$lib/history'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import {
faBug,
faClipboard,
@@ -62,7 +60,7 @@
StaticAppInput,
UserAppInput
} from '../inputType'
import type { AppEditorContext, AppViewerContext } from '../types'
import type { AppEditorContext } from '../types'
import { allItems, toStatic } from '../utils'
import AppExportButton from './AppExportButton.svelte'
import AppInputs from './AppInputs.svelte'
@@ -91,10 +89,9 @@
errorByComponent,
openDebugRun,
focusedGrid,
selectedComponent
} = getContext<AppViewerContext>('AppViewerContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
selectedComponent,
history
} = getContext<AppEditorContext>('AppEditorContext')
const loading = {
publish: false,
@@ -162,7 +159,7 @@
return []
})
.concat(
Object.values($app.hiddenInlineScripts ?? {}).map(async (v) => {
Object.values($app.hiddenInlineScripts).map(async (v) => {
let hex = await hash(v.inlineScript?.content)
const staticInputs = collectStaticFields(v.fields)
return [`rawscript/${hex}`, staticInputs]
@@ -170,6 +167,9 @@
)
)
policy.triggerables = Object.fromEntries(allTriggers.filter((x) => x.length > 0))
policy.on_behalf_of = `u/${$userStore?.username}`
policy.on_behalf_of_email = $userStore?.email
}
async function createApp(path: string) {
await computeTriggerables()
@@ -221,23 +221,18 @@
return
}
loading.save = true
try {
await computeTriggerables()
await AppService.updateApp({
workspace: $workspaceStore!,
path: $page.params.path,
requestBody: {
value: $app!,
summary: $summary,
policy
}
})
sendUserToast('App saved')
loading.save = false
} catch (e) {
loading.save = false
throw e
}
await computeTriggerables()
await AppService.updateApp({
workspace: $workspaceStore!,
path: $page.params.path,
requestBody: {
value: $app!,
summary: $summary,
policy
}
})
loading.save = false
sendUserToast('App saved')
}
let selectedJobId: string | undefined = undefined
@@ -247,56 +242,8 @@
$: selectedJobId && testJobLoader?.watchJob(selectedJobId)
$: hasErrors = Object.keys($errorByComponent).length > 0
let lock = false
function onKeyDown(event: KeyboardEvent) {
if (lock) return
lock = true
switch (event.key) {
case 'Z':
if (event.ctrlKey) {
$app = redo(history)
event.preventDefault()
}
break
case 'z':
if (event.ctrlKey) {
$app = undo(history, $app)
event.preventDefault()
}
break
case 's':
if (event.ctrlKey) {
save()
event.preventDefault()
}
break
// case 'ArrowDown': {
// let ids = generateIds()
// let idx = ids.indexOf($selectedIdStore)
// if (idx > -1 && idx < ids.length - 1) {
// $selectedIdStore = ids[idx + 1]
// event.preventDefault()
// }
// break
// }
// case 'ArrowUp': {
// let ids = generateIds()
// let idx = ids.indexOf($selectedIdStore)
// if (idx > 0 && idx < ids.length) {
// $selectedIdStore = ids[idx - 1]
// event.preventDefault()
// }
// break
// }
}
lock = false
}
</script>
<svelte:window on:keydown={onKeyDown} />
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job />
<Drawer bind:open={saveDrawerOpen} size="800px">
@@ -481,28 +428,44 @@
<div class="min-w-64 w-64">
<input type="text" placeholder="App summary" class="text-sm w-full" bind:value={$summary} />
</div>
<UndoRedo
undoProps={{ disabled: $history?.index === 0 }}
redoProps={{ disabled: $history && $history?.index === $history.history.length - 1 }}
on:undo={() => {
$app = undo(history, $app)
}}
on:redo={() => {
$app = redo(history)
}}
/>
<div class="flex gap-1">
<Button
title="Undo"
disabled={$history.index == 0}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$app = undo(history, $app)
}}
>
<Undo size={14} />
</Button>
<Button
title="Redo"
disabled={$history.index == $history.history.length - 1}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$app = redo(history)
}}
>
<Redo size={14} />
</Button>
</div>
<div class="flex gap-4 items-center grow justify-center">
<div>
<ToggleButtonGroup bind:selected={$mode}>
<ToggleButton position="left" value="dnd" size="xs">
<div class="inline-flex gap-1 items-center">
<Pencil size={14} />
<span class="hidden lg:inline">Editor</span>
<span class="hidden md:inline">Editor</span>
</div>
</ToggleButton>
<ToggleButton position="right" value="preview" size="xs">
<div class="inline-flex gap-1 items-center">
<Eye size={14} /> <span class="hidden lg:inline">Preview</span>
<Eye size={14} /> <span class="hidden md:inline">Preview</span>
</div>
</ToggleButton>
</ToggleButtonGroup>
@@ -519,46 +482,42 @@
</div>
<div class="hidden lg:block">
{#if $app}
<ToggleButtonGroup bind:selected={$app.fullscreen}>
<ToggleButton position="left" value={false} size="xs">
<div class="flex gap-1 justify-start items-center">
<AlignHorizontalSpaceAround size={14} />
<Tooltip light class="mb-0.5">
The max width is 1168px and the content stay centered instead of taking the full
page width
</Tooltip>
</div>
</ToggleButton>
<ToggleButton position="right" value={true} size="xs">
<Expand size={14} />
</ToggleButton>
</ToggleButtonGroup>
{/if}
<ToggleButtonGroup bind:selected={$app.fullscreen}>
<ToggleButton position="left" value={false} size="xs">
<div class="flex gap-1 justify-start items-center">
<AlignHorizontalSpaceAround size={14} />
<Tooltip light>
The max width is 1168px and the content stay centered instead of taking the full page
width
</Tooltip>
</div>
</ToggleButton>
<ToggleButton position="right" value={true} size="xs">
<Expand size={14} />
</ToggleButton>
</ToggleButtonGroup>
</div>
</div>
{#if $focusedGrid !== undefined}
<div class="hidden lg:block">
<Badge color="indigo">
<div class="flex flex-row gap-2 justify-center items-center">
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
<button
on:click={() => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
>
<X size={14} />
</button>
</div>
</Badge>
</div>
<Badge color="indigo">
<div class="flex flex-row gap-2 justify-center items-center">
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
<button
on:click={() => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
>
<X size={14} />
</button>
</div>
</Badge>
{/if}
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
<Dropdown
placement="bottom-end"
btnClasses="hidden lg:block"
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px] hidden lg:block"
dropdownItems={[
{
displayName: 'JSON',
@@ -583,7 +542,9 @@
}
}
]}
/>
>
<MoreVertical size={20} />
</Dropdown>
<span class="hidden md:inline">
<Button on:click={() => (inputsDrawerOpen = true)} color="light" size="xs" variant="border">
<span class="flex gap-2">
@@ -600,7 +561,7 @@
variant="border"
startIcon={{ icon: faBug }}
>
<span class="hidden xl:inline">Debug Runs</span>
<span class="hidden md:inline">Debug Runs</span>
</Button>
</span>
<AppExportButton bind:this={appExport} />
@@ -611,7 +572,7 @@
variant="border"
startIcon={{ icon: faExternalLink }}
>
<span class="hidden xl:inline">Publish</span>
<span class="hidden md:inline">Publish</span>
</Button>
{#if appPath == ''}
<Button

View File

@@ -2,11 +2,11 @@
import { Alert } from '$lib/components/common'
import Toggle from '$lib/components/Toggle.svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../types'
import type { AppEditorContext } from '../types'
import AppComponentInput from './AppComponentInput.svelte'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let resourceOnly: boolean = true
</script>

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import { onMount, setContext } from 'svelte'
import { fade } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { writable, type Writable } from 'svelte/store'
import { buildWorld, type World } from '../rx'
import type {
App,
AppEditorContext,
AppViewerContext,
ConnectingInput,
EditorBreakpoint,
EditorMode
@@ -41,7 +42,7 @@
const runnableComponents = writable<Record<string, () => Promise<void>>>({})
setContext<AppViewerContext>('AppViewerContext', {
setContext<AppEditorContext>('AppEditorContext', {
worldStore,
staticOutputs,
app: appStore,
@@ -63,13 +64,7 @@
focusedGrid: writable(undefined),
stateId: writable(0),
parentWidth: writable(0),
state: writable({})
})
setContext<AppEditorContext>('AppEditorContext', {
history: undefined,
componentControl: writable({}),
pickVariableCallback: writable(undefined)
history: writable<any>(undefined)
})
let mounted = false
@@ -78,20 +73,11 @@
mounted = true
})
let ncontext = context
function hashchange(e: HashChangeEvent) {
ncontext.hash = e.newURL.split('#')[1]
ncontext = ncontext
}
$: mounted && ($worldStore = buildWorld($staticOutputs, $worldStore, ncontext))
$: mounted && ($worldStore = buildWorld($staticOutputs, $worldStore, context))
$: width = $breakpoint === 'sm' ? 'max-w-[640px]' : 'w-full '
$: lockedClasses = isLocked ? '!max-h-[400px] overflow-hidden pointer-events-none' : ''
</script>
<svelte:window on:hashchange={hashchange} />
<div id="app-editor-top-level-drawer" />
<div class="relative">

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import type { AppViewerContext } from '../types'
import { Anchor, Bug, Expand, Move } from 'lucide-svelte'
import type { AppEditorContext } from '../types'
import { Anchor, Bug, Move } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import Popover from '$lib/components/Popover.svelte'
import { Alert, Button } from '$lib/components/common'
@@ -15,7 +15,7 @@
const dispatch = createEventDispatcher()
const { errorByComponent, openDebugRun } = getContext<AppViewerContext>('AppViewerContext')
const { errorByComponent, openDebugRun } = getContext<AppEditorContext>('AppEditorContext')
$: error = Object.values($errorByComponent).find((e) => e.componentId === component.id)
@@ -41,14 +41,13 @@
{#if pointerdown || selected || hover}
<button
title="Lock Position"
title="Position locking"
class={classNames(
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[2.5rem] z-50 cursor-pointer',
' hover:bg-gray-300',
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
)}
on:click={() => dispatch('lock')}
on:pointerdown|stopPropagation
>
{#if locked}
<Anchor aria-label="Unlock position" size={14} class="text-orange-500" />
@@ -56,18 +55,6 @@
<Anchor aria-label="Lock position" size={14} />
{/if}
</button>
<button
title="Expand"
class={classNames(
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[4.5rem] z-50 cursor-pointer',
' hover:bg-gray-300',
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
)}
on:click={() => dispatch('expand')}
on:pointerdown|stopPropagation
>
<Expand aria-label="Expand position" size={14} />
</button>
{/if}
{#if selected || hover}
@@ -75,7 +62,7 @@
title="Move"
on:mousedown|stopPropagation|capture
class={classNames(
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[0.5rem] z-50 cursor-move',
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[4.5rem] z-50 cursor-move',
'bg-gray-200/80'
)}
>

Some files were not shown because too many files have changed in this diff Show More