Compare commits

..

151 Commits

Author SHA1 Message Date
Ruben Fiszel
646c0f23da chore(main): release 1.77.0 (#1286)
* chore(main): release 1.77.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-14 22:27:08 +01:00
Ádám Kovács
dea12e8870 fix(frontend): Update setting accordion (#1285)
* fix(frontend): Update setting accordion

* remove empty tooltip

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-03-14 22:24:23 +01:00
Ryan Rich
944795f6ee feat(deno): add support for custom npm repo (#1291)
* feat(deno): add support for custom npm repo

* feat(deno): add support for custom npm repo

* feat(deno): fix comment being on wrong line

* feat(deno): review feedback
2023-03-14 22:23:44 +01:00
Ruben Fiszel
65d4bc519c add env build configuration 2023-03-14 22:20:45 +01:00
Ruben Fiszel
4d3507aec2 add .env.production to .gitignore 2023-03-14 22:19:08 +01:00
Ruben Fiszel
1d395ccc17 add env build configuration 2023-03-14 22:16:22 +01:00
hcourdent
3a7129de4b Added tooltips to App editor (#1289) 2023-03-14 22:05:34 +01:00
Ruben Fiszel
2f0acb9ffa feat(apps): state can be used as input in apps 2023-03-14 20:44:14 +01:00
Ruben Fiszel
81f989837b improve reactivity of apps 2023-03-14 18:46:31 +01:00
Faton Ramadani
c71a577fea fix(frontend): fix app tabs (#1288) 2023-03-14 16:11:07 +01:00
Faton Ramadani
bc870bd03e fix(frontend): fix container deletion (#1287)
* fix(frontend): fix container deletion

* fix(frontend): fix container deletion
2023-03-14 14:57:44 +01:00
Faton Ramadani
751edcf9b8 feat(frontend): app components output panel (#1283)
* feat(frontend): hierarchical output panel WIP

* feat(frontend): wip

* feat(frontend): working animations

* feat(frontend): working animations

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): improving connection

* feat(frontend): fix search

* feat(frontend): output panel v2

* feat(frontend): support table actions

* feat(frontend): support table actions

* feat(frontend): support background script

* feat(frontend): fix background scripts

* feat(frontend): simplify code

* feat(frontend): fix animation

* feat(frontend): fix wording

* feat(frontend): fix bg script click

* feat(frontend): fix bg script click

* feat(frontend): fix bg script click
2023-03-14 14:42:40 +01:00
Ruben Fiszel
c2a97c53cf feat(frontend): add setTab to frontend scripts 2023-03-14 14:42:26 +01:00
Ruben Fiszel
eb73f2a687 fix(backend): do not cache reference to workspace scripts 2023-03-14 14:16:07 +01:00
Ruben Fiszel
cd645d0935 feat(apps): tabs can be made pages or invisible + better frontend scripts reactivity 2023-03-14 12:34:08 +01:00
Ruben Fiszel
ac9bd7ef8c fix flow viewer select event 2023-03-14 00:11:37 +01:00
Ruben Fiszel
5dae6577b8 chore(main): release 1.76.0 (#1280)
* chore(main): release 1.76.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-13 23:41:53 +01:00
Ruben Fiszel
372f53b7fe improve refresh and background script behavior 2023-03-13 23:38:14 +01:00
Ruben Fiszel
5662fa0d09 fix compile 2023-03-13 20:59:27 +01:00
Ruben Fiszel
c958480ce8 fix(backend): improve worker ping api 2023-03-13 20:28:11 +01:00
Ruben Fiszel
f0b1b1f752 feat(frontend): add frontend (JS) scripts to apps 2023-03-13 19:29:35 +01:00
Ruben Fiszel
b8e6767cca improve error messages for no workspace 2023-03-13 16:44:38 +01:00
Ruben Fiszel
75f87e7e11 feat(frontend): disabled for action buttons can now depend on row 2023-03-13 14:36:42 +01:00
Ádám Kovács
3e5a179eb8 fix(frontend): General fixes and updates (#1281)
* fix(frontend): App icon select double click issue

* fix(frontend): Update script metadata page

* fix(frontend): Set dropdown default icon to vertical dots

* fix(frontend): Clean up

* fix(frontend): Update table styles

* fix(frontend): Add spacing to secondary menu items

* fix(frontend): Scale down full path

* fix(frontend): Table loading state

* fix(frontend): Hide script kind setting by default
2023-03-13 14:17:57 +01:00
Ruben Fiszel
c082c6350e fix runs reload 2023-03-13 13:00:23 +01:00
Ruben Fiszel
cfd489a550 feat(frontend): improve drag-n-drop behavior 2023-03-13 12:44:39 +01:00
Ruben Fiszel
1f4ae53fb4 integrate svelte-grid in codebase 2023-03-13 10:01:02 +01:00
Ruben Fiszel
0dcbf270da small app fixes 2023-03-12 21:32:41 +01:00
Faton Ramadani
82c139ed09 feat(frontend): Copy, Cut and Paste (#1279)
* feat(frontend): add copy, paste and cut

* feat(frontend): simplify code

* feat(frontend): add apple modifiers

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-12 20:58:06 +01:00
Ruben Fiszel
0789bef120 chore(main): release 1.75.0 (#1278)
* chore(main): release 1.75.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-11 20:54:54 +01:00
Ruben Fiszel
1a7dc0a3bd more cli fixes 2023-03-11 20:53:34 +01:00
Ruben Fiszel
ce323709a9 fix(cli): many small fixes 2023-03-11 20:16:56 +01:00
Ruben Fiszel
61a5e1f1ac feat(frontend): make runs filters synced with query args 2023-03-11 17:13:49 +01:00
Ruben Fiszel
3b44f9a72c feat: add filter jobs by args or result 2023-03-11 15:30:46 +01:00
Ruben Fiszel
b349308ff7 handle better linked variables renaming 2023-03-11 11:25:47 +01:00
Ruben Fiszel
f87b722a21 apps improvements 2023-03-10 23:27:55 +01:00
Ruben Fiszel
0e9be7f300 fix for app viewer 2023-03-10 20:26:49 +01:00
Ruben Fiszel
8681e83b57 feat(apps): add resource picker 2023-03-10 20:01:00 +01:00
Ruben Fiszel
bc440f8d41 feat(frontend-apps): add variable picker for static string input on apps 2023-03-10 18:41:27 +01:00
Ruben Fiszel
1d5c194f09 feat(bash): add default argument handling for bash 2023-03-10 15:04:34 +01:00
Ruben Fiszel
7a9d230459 disable playwright for now 2023-03-10 12:54:00 +01:00
Ruben Fiszel
4d5e2499cf cleanup .workflows 2023-03-10 12:48:02 +01:00
Ruben Fiszel
686275fd46 trim tailwindcss 2023-03-10 12:44:08 +01:00
Ruben Fiszel
99399f4f77 fix serde test 2023-03-10 12:19:56 +01:00
Ruben Fiszel
6e09194313 fix compile 2023-03-10 12:12:10 +01:00
Ruben Fiszel
7c825c212d fix(backend): add killpill for lines reading 2023-03-10 12:04:05 +01:00
Ruben Fiszel
480fd781b6 worker ping at least every 5s even when running long jobs 2023-03-10 01:38:12 +01:00
Ruben Fiszel
4f2079f624 trim tailwind safelist 2023-03-10 01:06:55 +01:00
Ruben Fiszel
43c45d930c chore(main): release 1.74.2 (#1277)
* chore(main): release 1.74.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-10 00:55:13 +01:00
Faton Ramadani
8d5c5b88a3 fix(frontend): fix splitpanes navigation (#1276) 2023-03-10 00:32:33 +01:00
Ruben Fiszel
cc8bedd0c7 make frontend configurable through consts.ts 2023-03-09 22:54:25 +01:00
Ruben Fiszel
74c3d6443c chore(main): release 1.74.1 (#1275)
* chore(main): release 1.74.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-09 22:45:53 +01:00
Ruben Fiszel
c7be313210 fix importjson 2023-03-09 22:42:47 +01:00
Ruben Fiszel
ae53bafaf6 fix(apps): proper reactivity for non rendered static components 2023-03-09 22:29:19 +01:00
Ruben Fiszel
2ea15d5035 fix(ci): make windmill compile again by pinning swc deps 2023-03-09 22:20:31 +01:00
Ruben Fiszel
0f187d66dd show backtrace for cook 2023-03-09 21:22:36 +01:00
Ruben Fiszel
6691b19b24 chore(main): release 1.74.0 (#1269)
* chore(main): release 1.74.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-09 19:58:21 +01:00
Ruben Fiszel
2f9ccff65c app nits 2023-03-09 19:56:47 +01:00
Ruben Fiszel
09db6fd867 fix key navigation 2023-03-09 19:41:12 +01:00
Ruben Fiszel
fd52740d5d improve reactivity check for big objects on app 2023-03-09 18:34:36 +01:00
Faton Ramadani
6b0fb75d23 feat(frontend): Add key navigation in app editor (#1273)
* feat(frontend): add expand

* feat(frontend): fix container height

* feat(frontend): remove code duplication

* feat(frontend): add historic

* feat(frontend): add key navigation

* feat(frontend): simplfiy

* feat(frontend): add support for subgrids

* feat(frontend): update key navigation

* feat(frontend): update key navigation

* feat(frontend): fix nested component

* feat(frontend): fix build

* feat(frontend): remove code duplication

* feat(frontend): support tabs

* feat(frontend): support tabs

* feat(frontend): Fix AppTabs + handle tab navigation

* feat(frontend): support splitpanes
2023-03-09 18:23:12 +01:00
Ruben Fiszel
b1a45b1e70 feat(frontend): add hash to ctx in apps 2023-03-09 14:57:43 +01:00
Ruben Fiszel
b2de531a46 fix(frontend): simplify input bindings 2023-03-09 14:25:08 +01:00
Ruben Fiszel
a4adcb5192 fix(frontend): add confirmation modal to delete script/flow/app 2023-03-09 13:19:03 +01:00
Ruben Fiszel
0c2cf92dd3 feat: add delete by path for scripts 2023-03-09 12:44:49 +01:00
Ruben Fiszel
e6344dac6d fix(cli): improve visibility of the active workspace 2023-03-09 11:21:16 +01:00
Ruben Fiszel
8fb2454e83 enforce on_behalf_of by the backend, not frontend 2023-03-09 11:12:28 +01:00
Ádám Kovács
3b6ae0cc49 fix(frontend): Minor changes (#1272)
* fix(frontend): Output seach fixed on top

* fix(frontend): Use undo-redo component in flows
2023-03-09 09:42:51 +01:00
Ruben Fiszel
96ff2eebc1 fix publishing app as a superadmin 2023-03-09 02:08:52 +01:00
Ruben Fiszel
ed29d51c36 fix app json import 2023-03-09 01:16:57 +01:00
Ádám Kovács
88e537ad1f feat(frontend): Add color picker input to app (#1270)
* feat(frontend): Add color picker input to app

* fix(frontend): Add color input to dividers
2023-03-08 22:57:34 +01:00
Faton Ramadani
b854ee3439 feat(frontend): add expand (#1268)
* feat(frontend): add expand

* feat(frontend): fix container height

* feat(frontend): remove code duplication

* feat(frontend): add historic
2023-03-08 22:56:30 +01:00
Ádám Kovács
0a5e181a3a fix(frontend): Clean up app editor (#1267)
* fix(frontend): Clean up app editor

* fix(frontend): Add outputs search empty state

* fix(frontend): Add remove button to icon input

* label

* fix(frontend): Iconed app button
2023-03-08 19:02:19 +01:00
Ruben Fiszel
8cc59225d8 improve resource picker 2023-03-08 19:00:26 +01:00
Ruben Fiszel
9c41346dde fix subtle plotly import bug 2023-03-08 18:27:19 +01:00
Ruben Fiszel
41a398f50e fix frontend build error 2023-03-08 16:59:14 +01:00
Ruben Fiszel
3436061ad4 make windmill compatible with arm64 2023-03-08 16:55:00 +01:00
Ruben Fiszel
569b5d2516 improve rendering performances for non visible elements 2023-03-08 16:39:16 +01:00
Ruben Fiszel
a08cdd7b86 chore(main): release 1.73.1 (#1266)
* chore(main): release 1.73.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-08 00:41:42 +01:00
Ruben Fiszel
719d475262 fix(frontend): load flow is not initialized 2023-03-08 00:37:58 +01:00
Ruben Fiszel
5b3e1183e5 revert import on tailwind colors for faster builds 2023-03-08 00:20:12 +01:00
Ruben Fiszel
7ed301b186 chore(main): release 1.73.0 (#1257)
* chore(main): release 1.73.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-07 22:35:56 +01:00
Ruben Fiszel
46b6e4371b improve undo/redo + keybindings 2023-03-07 21:16:15 +01:00
Ruben Fiszel
e0d3465b07 fix z-stacking on chrome for flow builder 2023-03-07 19:27:35 +01:00
Ruben Fiszel
7f8fe8dc17 fix z-stacking on chrome for flow builder 2023-03-07 19:23:41 +01:00
Faton Ramadani
24f58efd99 feat(frontend): add a way to automatically resize (#1259)
* feat(frontend): add a way to automatically resize (wip) + add automatic resizable component

* feat(frontend): fix text resize

* feat(frontend): remvove useless softWrap

* feat(frontend): remove useless softWrap

* feat(frontend): Fix recomputeIds + app table

* feat(frontend): Fix app preview error display
2023-03-07 16:53:29 +01:00
Ruben Fiszel
67d8009dcf fix multiple app nits 2023-03-07 15:51:02 +01:00
Ruben Fiszel
95ccc9edf8 fix z-index for flowbuilder 2023-03-07 14:57:51 +01:00
Ruben Fiszel
9e4d90ad37 feat(frontend): add history to flows and apps 2023-03-07 14:47:17 +01:00
Ádám Kovács
c638897fdc fix(frontend): Side menu z-index issue (#1265) 2023-03-07 09:53:19 +01:00
Ruben Fiszel
71305e5154 show archived + fix graphs 2023-03-07 01:59:35 +01:00
Ruben Fiszel
9e9f8efb8e feat(frontend): add fork/save buttons + consistent styling for slider/range 2023-03-06 22:35:25 +01:00
Ádám Kovács
3e5d09ef0b feat(frontend): Add app PDF viewer (#1254)
* feat(frontend): Add app PDF viewer (wip)

* fix(frontend): Handle file upload

* fix(frontend): Handle multi page pdf

* feat(frontend): Add pdf page numbering

* feat(frontend): Add more pdf viewer controls

* save

* fix(frontend): Pdf loading

* fix(frontend): Resize PDF in small window

* fix(frontend): Minor fixes

* feat(frontend): Add pdf zoom configuration

* fix wip

* save

* bg color

* save progress

* pdf scaling

* feat(frontend): fix zoom synchro

* fix(frontend): Pdf scroll tracking

* fix(frontend): Double scrollbar

* nits

* fixes

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-06 20:17:36 +01:00
Ruben Fiszel
614fb5022a feat(frontend): add ability to move nodes 2023-03-06 18:41:20 +01:00
Ruben Fiszel
0beadfd1ac fix z-index of inputransformform 2023-03-06 16:04:32 +01:00
Ruben Fiszel
25580c1272 add trigger button 2023-03-06 14:11:17 +01:00
Faton Ramadani
2557e136bd fix(frontend): fix app map reactivity (#1260) 2023-03-06 11:26:00 +01:00
Ruben Fiszel
200cb69d82 make default branch non removable for branchone 2023-03-06 11:19:56 +01:00
Ruben Fiszel
9ee261fe1a Update docker-compose.yml 2023-03-06 10:39:00 +01:00
Ruben Fiszel
8e563a42f5 Update docker-compose.yml with oauth example 2023-03-06 10:38:01 +01:00
Faton Ramadani
a999eb2112 fix(frontend): fix branch deletion (#1261)
* fix(frontend): fix branch deletion

* fix(frontend): fix branch deletion

* fix(frontend): fix branch deletion
2023-03-06 09:01:19 +01:00
Ruben Fiszel
e5dbe7076c handle larger sized graphs 2023-03-06 08:33:53 +01:00
Ruben Fiszel
2ac51b0af0 feat(frontend): refactor entire flow builder UX 2023-03-05 23:00:43 +01:00
Ruben Fiszel
f3232062c3 make tailwind inputs class configurable 2023-03-03 22:32:35 +01:00
Ruben Fiszel
b11a5a2df6 only bind the staticoutputs of the first row 2023-03-03 18:00:09 +01:00
Ruben Fiszel
e2c4545240 fix(frontend): arginput + apppreview fixes 2023-03-03 17:34:08 +01:00
Faton Ramadani
70dd6f759c App small fixes (#1258)
* fix(frontend): Fix runnable editor

* fix(frontend): remove isopenstore

* fix(frontend): add output searchbar

* fix(frontend): fix build

* fix(frontend): add missing clear button
2023-03-03 15:15:29 +01:00
Ruben Fiszel
dcfb29fb80 fix sqlx offline 2023-03-03 13:04:24 +01:00
Faton Ramadani
94f1aadef2 feat(frontend): Fix object viewer style (#1255)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-03 12:47:55 +01:00
Ruben Fiszel
58300eb6ac introduce root_job and leaf_jobs for efficient result_by_id 2023-03-03 12:44:44 +01:00
Ashutosh Narang
304dea4b74 update build instructions (#1256) 2023-03-03 11:19:12 +01:00
Ruben Fiszel
f4fe71e074 chore(main): release 1.72.0 (#1250)
* chore(main): release 1.72.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-03 11:12:18 +01:00
Ruben Fiszel
fd4e18f62f fix minSize for app splitpanes to 20 2023-03-03 01:08:20 +01:00
Ruben Fiszel
e428662481 feat(frontend): add creatable select 2023-03-03 01:06:36 +01:00
Ruben Fiszel
b796aeef7a calculate all previous ids inside flows 2023-03-02 20:54:44 +01:00
Ruben Fiszel
55eb48c553 fix(frontend): background script not showing inputs 2023-03-02 17:54:05 +01:00
Ruben Fiszel
a43139fe53 flow improvements 2023-03-02 17:37:51 +01:00
Ruben Fiszel
c4463bb029 fix(backend): improve result retrieval 2023-03-02 16:33:24 +01:00
Ruben Fiszel
cc6eaaf473 fix tailwind JIT for devmode + graph fixes 2023-03-02 14:49:36 +01:00
Ádám Kovács
ed25d9f186 feat(frontend): Add app map component (#1251)
* feat(frontend): Add app map component (wip)

* fix(frontend): Revert

* feat(frontend): sync map configuration (#1252)

* fix(frontend): Map markers

* fix(frontend): Switching between input types

* fix(frontend): Customize map controls

* feat(frontend): Fix output + add set region button

* feat(frontend): Fix output + add set region button

* feat(frontend): Fix output + add set region button

* feat(frontend): Only display set region button on edit mode

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-03-02 14:25:30 +01:00
Ruben Fiszel
35ea2b27b1 fix(cli): fix workspace option + run script/flow + whoami 2023-03-02 13:21:50 +01:00
Ruben Fiszel
2c1e3b3372 UX nits 2023-03-02 12:08:20 +01:00
Ruben Fiszel
4101d587de remove slide causing issues 2023-03-02 11:59:10 +01:00
Ruben Fiszel
e6ff3ab6cc remove slide causing issues 2023-03-02 11:49:57 +01:00
Ruben Fiszel
8fc6c39129 remove bg-gray-50 from viewed apps 2023-03-02 11:06:11 +01:00
Ruben Fiszel
fcb5cf4d41 revert caddyfileremote target change 2023-03-02 10:39:01 +01:00
Ruben Fiszel
2679386bf8 fix(frontend): fix table bindings 2023-03-02 09:54:30 +01:00
Ryan Rich
580388ce19 Add support for binding server listener to a specific IP address (#1253) 2023-03-02 08:01:50 +01:00
Ruben Fiszel
4e6e66d7b1 fix splitpanes 2023-03-02 02:31:39 +01:00
Faton Ramadani
f4d79ee263 feat(frontend): app splitpanes (#1248)
* feat(frontend): app splitpanes

* feat(frontend): app splitpanes vertical

* feat(frontend): support both splitpanes

* done

* done

* default select value

* container height

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-02 01:30:07 +01:00
Ruben Fiszel
38fb3450c8 fix apps tabs + make inputvalue more resilient 2023-03-01 22:15:25 +01:00
Ruben Fiszel
94b20d2f5e fix(frontend): rework app reactivity 2023-03-01 21:33:23 +01:00
Ruben Fiszel
1753cb7da6 fix(frontend): rework app reactivity 2023-03-01 20:01:59 +01:00
Ruben Fiszel
2a75cd250e fix(backend): incorrect get_result_by_id for list_result job 2023-03-01 12:43:00 +01:00
Ruben Fiszel
29f3fe2663 update sqlx-data.json 2023-03-01 12:01:22 +01:00
Ruben Fiszel
4c913dc4b6 feat(backend): get_result_by_id do a downward pass to find node at any depth (#1249)
* downwardRec

* downwardRec

* any node

* any node

* any node
2023-03-01 11:33:48 +01:00
Ruben Fiszel
5c40ff4290 Update LICENSE 2023-03-01 09:53:26 +01:00
Ruben Fiszel
2bbe112444 handle more undefined cases in app 2023-03-01 08:46:20 +01:00
Ruben Fiszel
90a12f6131 drawer focus 2023-03-01 01:21:32 +01:00
Ruben Fiszel
f3f95fa865 active grid border-dashed for apps 2023-03-01 01:18:10 +01:00
Ruben Fiszel
26784464a4 revert pips change 2023-02-28 22:53:44 +01:00
Ruben Fiszel
c96e2351d9 chore(main): release 1.71.0 (#1242)
* chore(main): release 1.71.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-28 22:51:57 +01:00
Ruben Fiszel
ddb4916a2e fix app fields 2023-02-28 22:41:21 +01:00
Ádám Kovács
1bb5ed9ae0 fix(frontend): Add more app custom css (#1247)
* fix(frontend): Add number input custom css

* fix(frontend): Add currency input custom css

* fix(frontend): Add slider custom css

* fix(frontend): Add range custom css

* fix(frontend): Add password input custom css

* fix(frontend): Add date input custom css

* fix(frontend): Add tabs custom css

* fix(frontend): Minor stylings

* fix(frontend): Add icon custom css

* fix(frontend): Add dividers custom css

* fix(frontend): Add file input custom css

* fix(frontend): Add image custom css
2023-02-28 21:05:58 +01:00
Ruben Fiszel
b5b32f00b3 fix overflow-y on debug runs 2023-02-28 19:54:20 +01:00
Ruben Fiszel
c06311faf8 add workspace_add events 2023-02-28 19:41:01 +01:00
Ruben Fiszel
8a639b6e7d select input fix 2023-02-28 19:28:36 +01:00
Ruben Fiszel
05f568fb8c display startup info in all cases 2023-02-28 17:52:47 +01:00
Ruben Fiszel
e515c70e71 fix incorrect user sttings redirect 2023-02-28 16:57:00 +01:00
Ruben Fiszel
6adc875610 feat(frontend): drawer for editing workspace scripts in flows 2023-02-28 15:51:56 +01:00
Faton Ramadani
8a0d1158c4 feat(frontend): App drawer (#1246)
* feat(frontend): app drawer

* feat(frontend): app drawer

* feat(frontend): app drawer

* feat(frontend): app drawer wip

* feat(frontend): drawer wip

* feat(frontend): drawer wip

* feat(frontend): app missing prop

* feat(frontend): revert drawer changes

* feat(frontend): highlight subgrid
2023-02-28 15:49:57 +01:00
342 changed files with 16780 additions and 5083 deletions

View File

@@ -1,20 +0,0 @@
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
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()
# 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()
publish_privately_heavy:

View File

@@ -1,6 +1,160 @@
# Changelog
## [1.77.0](https://github.com/windmill-labs/windmill/compare/v1.76.0...v1.77.0) (2023-03-14)
### Features
* **apps:** state can be used as input in apps ([2f0acb9](https://github.com/windmill-labs/windmill/commit/2f0acb9ffa8dace4a886527dcee49809d019b271))
* **apps:** tabs can be made pages or invisible + better frontend scripts reactivity ([cd645d0](https://github.com/windmill-labs/windmill/commit/cd645d0935f2d06e0ff71f14d2cf63accd378ff3))
* **deno:** add support for custom npm repo ([#1291](https://github.com/windmill-labs/windmill/issues/1291)) ([944795f](https://github.com/windmill-labs/windmill/commit/944795f6eeaa7d01ab1a35a80570a55c363723e6))
* **frontend:** add setTab to frontend scripts ([c2a97c5](https://github.com/windmill-labs/windmill/commit/c2a97c53cfff0fdb35dd8bc249490566eebdc1a9))
* **frontend:** app components output panel ([#1283](https://github.com/windmill-labs/windmill/issues/1283)) ([751edcf](https://github.com/windmill-labs/windmill/commit/751edcf9b8e0976a1d073603c9eff5dc6e714490))
### Bug Fixes
* **backend:** do not cache reference to workspace scripts ([eb73f2a](https://github.com/windmill-labs/windmill/commit/eb73f2a687f6faad301b9038ab8585450bec7481))
* **frontend:** fix app tabs ([#1288](https://github.com/windmill-labs/windmill/issues/1288)) ([c71a577](https://github.com/windmill-labs/windmill/commit/c71a577fead90c9cd01a736b54d859ec4f0b7807))
* **frontend:** fix container deletion ([#1287](https://github.com/windmill-labs/windmill/issues/1287)) ([bc870bd](https://github.com/windmill-labs/windmill/commit/bc870bd03eb76cb8bc0e0c861f6cd8a9c661186b))
* **frontend:** Update setting accordion ([#1285](https://github.com/windmill-labs/windmill/issues/1285)) ([dea12e8](https://github.com/windmill-labs/windmill/commit/dea12e8870ece998bb6607723cbaab9b9a958f22))
## [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)
### Features
* **backend:** get_result_by_id do a downward pass to find node at any depth ([#1249](https://github.com/windmill-labs/windmill/issues/1249)) ([4c913dc](https://github.com/windmill-labs/windmill/commit/4c913dc4b6be03571a015c97a13829adffb61479))
* **frontend:** Add app map component ([#1251](https://github.com/windmill-labs/windmill/issues/1251)) ([ed25d9f](https://github.com/windmill-labs/windmill/commit/ed25d9f186d9925f75404cb193a025d8a41c4540))
* **frontend:** app splitpanes ([#1248](https://github.com/windmill-labs/windmill/issues/1248)) ([f4d79ee](https://github.com/windmill-labs/windmill/commit/f4d79ee2633e6cdab0fa2410108b31cfa77e10da))
### Bug Fixes
* **backend:** improve result retrieval ([c4463bb](https://github.com/windmill-labs/windmill/commit/c4463bb029907f3c8d77abb194f872aae7876bf6))
* **backend:** incorrect get_result_by_id for list_result job ([2a75cd2](https://github.com/windmill-labs/windmill/commit/2a75cd250ea5e01849fc8bbb69bf44f147d0acb8))
* **cli:** fix workspace option + run script/flow + whoami ([35ea2b2](https://github.com/windmill-labs/windmill/commit/35ea2b27b12159c68c8507ec1f8686028c975387))
* **frontend:** background script not showing inputs ([55eb48c](https://github.com/windmill-labs/windmill/commit/55eb48c55332431304cedbf3bcbbbcff61ec3645))
* **frontend:** fix table bindings ([2679386](https://github.com/windmill-labs/windmill/commit/2679386bf87a56352269911bd89e52df5ee9f314))
* **frontend:** rework app reactivity ([94b20d2](https://github.com/windmill-labs/windmill/commit/94b20d2f5e3b551974c57ea82b6e3dc16e97b9b8))
* **frontend:** rework app reactivity ([1753cb7](https://github.com/windmill-labs/windmill/commit/1753cb7da658f47be974c15da82c71a8e19309a6))
## [1.71.0](https://github.com/windmill-labs/windmill/compare/v1.70.1...v1.71.0) (2023-02-28)
### Features
* **backend:** use counter for sleep/execution/pull durations ([e568690](https://github.com/windmill-labs/windmill/commit/e56869092a03fec4703ddd9ef65c89edb8122962))
* **cli:** add autocompletions ([287b2db](https://github.com/windmill-labs/windmill/commit/287b2db22f7b56e90bcd0c4727c00096695c2e0d))
* **frontend:** App drawer ([#1246](https://github.com/windmill-labs/windmill/issues/1246)) ([8a0d115](https://github.com/windmill-labs/windmill/commit/8a0d1158c4d7e970cb91e1adf4838e5efdbb39ff))
* **frontend:** drawer for editing workspace scripts in flows ([6adc875](https://github.com/windmill-labs/windmill/commit/6adc87561070d8aceaba1838008cd7e6be2e2660))
### Bug Fixes
* **frontend:** Add more app custom css ([#1229](https://github.com/windmill-labs/windmill/issues/1229)) ([a4e4d18](https://github.com/windmill-labs/windmill/commit/a4e4d188ad10443dd0b7f104389594efc768dc59))
* **frontend:** Add more app custom css ([#1247](https://github.com/windmill-labs/windmill/issues/1247)) ([1bb5ed9](https://github.com/windmill-labs/windmill/commit/1bb5ed9ae01fd7998b06833b6222e5dd5d774d35))
* **frontend:** display currently selected filter even if not in list ([42d1cd6](https://github.com/windmill-labs/windmill/commit/42d1cd6456620ba917c560c87d736dc93634adff))
* **frontend:** Fix deeply nested move ([#1245](https://github.com/windmill-labs/windmill/issues/1245)) ([a67f10e](https://github.com/windmill-labs/windmill/commit/a67f10eeb6fdb44bbb3a510badcc5ad0ae187a2b))
* **frontend:** invisible subgrids have h-0 + app policies fix ([2244e83](https://github.com/windmill-labs/windmill/commit/2244e83b9da803a4cf46ab0825d7cb6cb0e24872))
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)

View File

@@ -73,7 +73,7 @@ ARG features=""
COPY --from=planner /windmill/recipe.json recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef cook --release --features "$features" --recipe-path recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
@@ -86,6 +86,7 @@ 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
@@ -129,6 +130,10 @@ 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}

View File

@@ -8,5 +8,9 @@ or belonging to one of the below cases:
The files under backend/ are AGPL Licensed.
The files under frontend/ are AGPL Licensed.
The files under python-client/ are Apache 2.0 Licensed.
The files under community/ are Apache 2.0 Licensed.
The files under python-client/ deno-client/ go-client/ are Apache 2.0 Licensed.
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.
All third party components incorporated into the Windmill Software are licensed under the
original license provided by the owner of the applicable component.

View File

@@ -284,6 +284,7 @@ you to have it being synced automatically everyday.
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
| PORT | 8000 | Exposed port | Server | |
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker, set to 0 for an API only instance) | Worker |
| DISABLE_SERVER | false | Binary would operate as a worker only instance | Worker |
@@ -315,6 +316,7 @@ you to have it being synced automatically everyday.
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |

465
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.70.1"
version = "1.77.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.70.1"
version = "1.77.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"
swc_ecma_ast = "^0"
swc_ecma_parser = "0.128.2"
swc_ecma_ast = "0.98.1"
base64 = "0.21.0"
unicode-general-category = "^0"
hmac = "0.12.1"

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN root_job uuid;
ALTER TABLE queue ADD COLUMN leaf_jobs jsonb;

View File

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

View File

@@ -1,6 +1,8 @@
#![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};
@@ -17,19 +19,32 @@ 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::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 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 args = vec![];
for i in 1..20 {
if hm.contains_key(&i) {
let (name, default) = hm.get(&i).unwrap();
args.push(Arg {
name: hm[&i].clone(),
name: name.clone(),
typ: Typ::Str(None),
default: None,
default: default.clone().map(|x| json!(x)),
otyp: None,
has_default: false,
});
@@ -43,6 +58,8 @@ fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
@@ -50,8 +67,7 @@ mod tests {
let code = r#"
token="$1"
image="$2"
digest="${3:-latest}"
foo="$4"
digest="${3:-latest with spaces}"
"#;
//println!("{}", serde_json::to_string()?);
@@ -74,6 +90,13 @@ foo="$4"
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,6 +468,27 @@
},
"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": [],
@@ -550,21 +571,6 @@
},
"query": "SELECT * FROM workspace LIMIT $1 OFFSET $2"
},
"15de975d9be141c9ed9647935a508492aabbbddbf986d5c5c0f0c415293c432d": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
}
},
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'), \n ($3, 'g/all/not_secret', $4, false, 'This item is not secret')"
},
"163f00eb8b1a489d5f382cdba22a5744e88a8e6f1532d7cb02af560f5f5d49f7": {
"describe": {
"columns": [
@@ -820,75 +826,6 @@
},
"query": "\n SELECT id, flow_status, suspend, script_path\n FROM queue\n WHERE id = $1\n "
},
"1e35c39bc786d638252e5483ca4efae9a041f7e845341f8bfd715ddd9e899499": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Bool",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Text",
"Jsonb",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Bool",
"Text",
"Varchar",
"Bool"
]
}
},
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) RETURNING id"
},
"1eaf8d677d520c7f2f303a731de6b6d939918e41ad0d1c748d80db3fd33cb9d3": {
"describe": {
"columns": [],
@@ -1170,6 +1107,20 @@
},
"query": "SELECT set_config('session.folders_read', $1, true)"
},
"2a3ebe1b0eae5b2164894321e138cc4dc0293788aeb98d05d95d18dfc708d6a6": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Jsonb",
"Uuid"
]
}
},
"query": "\n UPDATE queue\n SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)\n WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id\n "
},
"2a4be8334db7d39f3d954193a8b0169cc4a4a07e081d2fa61d8764879d6a8ff5": {
"describe": {
"columns": [],
@@ -1183,33 +1134,6 @@
},
"query": "UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2"
},
"2be0cfd075df9624ccbcbe5fd645e0a5c25460c2d01493f86dcdd9b2b71f6181": {
"describe": {
"columns": [
{
"name": "flow_status",
"ordinal": 0,
"type_info": "Jsonb"
},
{
"name": "parent_job",
"ordinal": 1,
"type_info": "Uuid"
}
],
"nullable": [
null,
null
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 "
},
"2e4115bb2e6c8c85ad1492ad135d6b0454b342126cb5fa17e58caf71b32ee755": {
"describe": {
"columns": [],
@@ -2577,28 +2501,7 @@
},
"query": "SELECT set_config('session.user', $1, true)"
},
"6dfd1ac7168f098a9e7c92d530a470c0a156041d5081053d43d9efb00d81c464": {
"describe": {
"columns": [
{
"name": "policy",
"ordinal": 0,
"type_info": "Jsonb"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT policy from app WHERE path = $1 AND workspace_id = $2"
},
"6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": {
"6d50a8dc9cfc040b6f37b58053daa0709671e008a10ab4189114ceff66efe603": {
"describe": {
"columns": [
{
@@ -2612,9 +2515,9 @@
"type_info": "Varchar"
},
{
"name": "ping_at",
"name": "last_ping",
"ordinal": 2,
"type_info": "Timestamptz"
"type_info": "Int4"
},
{
"name": "started_at",
@@ -2635,7 +2538,7 @@
"nullable": [
false,
false,
false,
null,
false,
false,
false
@@ -2647,7 +2550,28 @@
]
}
},
"query": "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2"
"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": [
{
"name": "policy",
"ordinal": 0,
"type_info": "Jsonb"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT policy from app WHERE path = $1 AND workspace_id = $2"
},
"701f215eb14ba67a79afea15d7effc0dd394ba6c4a72c95d4560c5a377015d4e": {
"describe": {
@@ -3060,20 +2984,6 @@
},
"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": [
@@ -3244,6 +3154,21 @@
},
"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": [
@@ -4003,6 +3928,76 @@
},
"query": "SELECT content FROM script WHERE path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND workspace_id = $2)"
},
"a1c41bbeb2d64fa1e7dfd2ed053191a1de5d786ae8c22e225e450865ecac94e9": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Bool",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Text",
"Jsonb",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Bool",
"Text",
"Varchar",
"Bool",
"Uuid"
]
}
},
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) RETURNING id"
},
"a227548b6604c56bfc15eb780bd8ee72a89dc6701a50f5048e928bd87baa7b9a": {
"describe": {
"columns": [
@@ -4113,6 +4108,28 @@
},
"query": "UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3"
},
"a5f9fb82791103e2bbaf9cb6d87e8c50495d12d87f8ed83382068203a8dd7a67": {
"describe": {
"columns": [
{
"name": "?column?",
"ordinal": 0,
"type_info": "Jsonb"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
}
},
"query": "SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3"
},
"a6145b0482c9e5da245059a80b1563cad20318fd2dd8aef33f9ca97de1826b8b": {
"describe": {
"columns": [],
@@ -4681,6 +4698,27 @@
},
"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": [],
@@ -4869,27 +4907,6 @@
},
"query": "SELECT result FROM completed_job WHERE id = $1"
},
"c2d0e44faab6981a21ca28dfd6f4eef9dfcafb471852e701c0bbc8ae11344325": {
"describe": {
"columns": [
{
"name": "parent_job",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2"
},
"c2d6cb56c1dea4498e2aab9ea9301dbbaa127602a38f57f5add4108fdc209b1a": {
"describe": {
"columns": [
@@ -5045,6 +5062,18 @@
},
"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

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use git_version::git_version;
use sqlx::{Pool, Postgres};
@@ -15,6 +15,7 @@ use windmill_common::utils::rd_string;
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 3;
const DEFAULT_PORT: u16 = 8000;
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
mod ee;
@@ -39,6 +40,11 @@ async fn main() -> anyhow::Result<()> {
.transpose()?
.flatten();
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_ADDR")
.ok()
.and_then(|x| x.parse().ok() )
.unwrap_or(IpAddr::from(DEFAULT_SERVER_BIND_ADDR));
let port: u16 = std::env::var("PORT")
.ok()
.and_then(|x| x.parse::<u16>().ok())
@@ -60,8 +66,64 @@ async fn main() -> anyhow::Result<()> {
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx);
#[cfg(feature = "enterprise")]
tracing::info!(
"
##############################
Windmill Enterprise Edition {GIT_VERSION}
##############################"
);
#[cfg(not(feature = "enterprise"))]
tracing::info!(
"
##############################
Windmill Community Edition {GIT_VERSION}
##############################"
);
display_config(vec![
"DISABLE_NSJAIL",
"DISABLE_SERVER",
"NUM_WORKERS",
"METRICS_ADDR",
"JSON_FMT",
"BASE_URL",
"BASE_INTERNAL_URL",
"TIMEOUT",
"SLEEP_QUEUE",
"MAX_LOG_SIZE",
"SERVER_BIND_ADDR",
"PORT",
"KEEP_JOB_DIR",
"S3_CACHE_BUCKET",
"TAR_CACHE_RATE",
"COOKIE_DOMAIN",
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
"PATH",
"HOME",
"DATABASE_CONNECTIONS",
"TIMEOUT_WAIT_RESULT",
"QUEUE_LIMIT_WAIT_RESULT",
"DENO_AUTH_TOKENS",
"DENO_FLAGS",
"NPM_CONFIG_REGISTRY",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",
"WHITELIST_WORKSPACES",
"BLACKLIST_WORKSPACES",
"NEW_USER_WEBHOOK",
"CLOUD_HOSTED",
]);
if server_mode || num_workers > 0 {
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let addr = SocketAddr::from((server_bind_address, port));
let server_f = async {
if server_mode {
@@ -72,60 +134,6 @@ async fn main() -> anyhow::Result<()> {
let workers_f = async {
if num_workers > 0 {
#[cfg(feature = "enterprise")]
tracing::info!(
"
##############################
Windmill Enterprise Edition {GIT_VERSION}
##############################"
);
#[cfg(not(feature = "enterprise"))]
tracing::info!(
"
##############################
Windmill Community Edition {GIT_VERSION}
##############################"
);
display_config(vec![
"DISABLE_NSJAIL",
"DISABLE_SERVER",
"NUM_WORKERS",
"METRICS_ADDR",
"JSON_FMT",
"BASE_URL",
"BASE_INTERNAL_URL",
"TIMEOUT",
"SLEEP_QUEUE",
"MAX_LOG_SIZE",
"PORT",
"KEEP_JOB_DIR",
"S3_CACHE_BUCKET",
"TAR_CACHE_RATE",
"COOKIE_DOMAIN",
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
"PATH",
"HOME",
"DATABASE_CONNECTIONS",
"TIMEOUT_WAIT_RESULT",
"QUEUE_LIMIT_WAIT_RESULT",
"DENO_AUTH_TOKENS",
"DENO_FLAGS",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",
"WHITELIST_WORKSPACES",
"BLACKLIST_WORKSPACES",
"NEW_USER_WEBHOOK",
"CLOUD_HOSTED",
]);
run_workers(
db.clone(),
rx.resubscribe(),

View File

@@ -836,6 +836,7 @@ impl RunJob {
/* scheduled_for_o */ None,
/* schedule_path */ None,
/* parent_job */ None,
/* root job */ None,
/* is_flow_step */ false,
/* running */ false,
None,

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.70.1
version: 1.77.0
title: Windmill API
contact:
@@ -204,7 +204,6 @@ paths:
schema:
type: string
/w/{workspace}/users/is_owner/{path}:
get:
summary: is owner of path
@@ -1549,7 +1548,7 @@ paths:
required: true
content:
application/json:
schema:
schema:
type: object
properties:
value: {}
@@ -2103,7 +2102,6 @@ paths:
items:
type: string
/w/{workspace}/scripts/create:
post:
summary: create script
@@ -2278,7 +2276,7 @@ paths:
/w/{workspace}/scripts/delete/h/{hash}:
post:
summary: delete script by hash (erase content but keep hash)
summary: delete script by hash (erase content but keep hash, require admin)
operationId: deleteScriptByHash
tags:
- script
@@ -2293,6 +2291,23 @@ 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
@@ -2538,11 +2553,6 @@ paths:
required: true
schema:
type: string
- name: skip_direct
description: Skip checking that the node is part of the given flow.
in: query
schema:
type: boolean
responses:
"200":
description: job result
@@ -2823,7 +2833,6 @@ paths:
schema:
$ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/apps/secret_of/{path}:
get:
summary: get public secret of app
@@ -3156,12 +3165,14 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $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
@@ -3186,10 +3197,12 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $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
@@ -3223,9 +3236,11 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped
description: is the job skipped
in: query
@@ -3337,6 +3352,22 @@ paths:
schema:
$ref: "#/components/schemas/CompletedJob"
/w/{workspace}/jobs/completed/get_result/{id}:
get:
summary: get completed job result
operationId: getCompletedJobResult
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
responses:
"200":
description: result
content:
application/json:
schema: {}
/w/{workspace}/jobs/completed/delete/{id}:
post:
summary: delete completed job (erase content but keep run id)
@@ -3410,7 +3441,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
@@ -3436,7 +3467,7 @@ paths:
properties:
approvalPage:
type: string
resume:
resume:
type: string
cancel:
type: string
@@ -4081,7 +4112,7 @@ paths:
type: string
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4115,7 +4146,7 @@ paths:
properties:
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4286,7 +4317,8 @@ 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
@@ -4311,7 +4343,8 @@ 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
@@ -4347,7 +4380,8 @@ 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
@@ -4367,19 +4401,18 @@ 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:
@@ -4586,15 +4619,15 @@ components:
in: query
schema:
type: string
CreatedBefore:
name: created_before
StartedBefore:
name: started_before
description: filter on created before (inclusive) timestamp
in: query
schema:
type: string
format: date-time
CreatedAfter:
name: created_after
StartedAfter:
name: started_after
description: filter on created after (exclusive) timestamp
in: query
schema:
@@ -4618,6 +4651,19 @@ 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
@@ -5522,9 +5568,8 @@ components:
type: string
worker_instance:
type: string
ping_at:
type: string
format: date-time
last_ping:
type: number
started_at:
type: string
format: date-time

View File

@@ -313,10 +313,13 @@ async fn create_app(
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
Json(mut 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)
@@ -463,7 +466,9 @@ async fn update_app(
sqlb.set_str("summary", nsummary);
}
if let Some(npolicy) = ns.policy {
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);
sqlb.set(
"policy",
&format!(
@@ -670,6 +675,7 @@ async fn execute_component(
None,
None,
None,
None,
false,
false,
None,
@@ -727,6 +733,9 @@ 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)
@@ -747,3 +756,20 @@ 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

@@ -7,11 +7,12 @@
*/
use axum::{
extract::{Extension, Path},
extract::{Extension, Path, Query},
routing::{get, post, put},
Json, Router,
};
use hyper::StatusCode;
use hyper::{HeaderMap, StatusCode};
use serde::Deserialize;
use windmill_common::{
error::{JsonResult, Result},
utils::{not_found_if_none, StripPath},
@@ -19,6 +20,7 @@ use windmill_common::{
use crate::{
db::{UserDB, DB},
jobs::add_include_headers,
users::Authed,
};
@@ -83,13 +85,21 @@ pub async fn new_payload(
Ok(StatusCode::CREATED)
}
#[derive(Deserialize, Clone)]
pub struct IncludeHeaderQuery {
include_header: Option<String>,
}
pub async fn update_payload(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<serde_json::Value>,
Query(run_query): Query<IncludeHeaderQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> Result<StatusCode> {
let mut tx = db.begin().await?;
let args = add_include_headers(&run_query.include_header, headers, args.unwrap_or_default());
sqlx::query!(
"
UPDATE capture
@@ -99,7 +109,7 @@ pub async fn update_payload(
",
&w_id,
&path.to_path(),
&payload,
serde_json::json!(args),
)
.execute(&mut tx)
.await?;

View File

@@ -237,6 +237,7 @@ async fn create_flow(
None,
None,
None,
None,
false,
false,
None,
@@ -393,6 +394,7 @@ async fn update_flow(
None,
None,
None,
None,
false,
false,
None,
@@ -638,8 +640,6 @@ mod tests {
"type": "script",
"path": "test"
},
"stop_after_if": null,
"summary": null
},
{
"id": "b",
@@ -648,15 +648,12 @@ 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",
@@ -678,8 +675,7 @@ mod tests {
"stop_after_if": {
"expr": "previous.isEmpty()",
"skip_if_stopped": false,
},
"summary": null
}
}
],
"failure_module": {
@@ -693,8 +689,7 @@ 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,10 +16,9 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use itertools::Itertools;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, Error, JsonResult, Result},
error::{self, to_anyhow, Error, JsonResult, Result},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
@@ -262,13 +261,26 @@ 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", display_name);
sqlb.set("display_name", "?".bind(&display_name));
}
if let Some(owners) = ng.owners {
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
sqlb.set(
"owners",
"?".bind(&format!(
"{{{}}}",
owners
.iter()
.map(|x| format!("\"{x}\""))
.collect::<Vec<_>>()
.join(","),
)),
);
}
if let Some(extra_perms) = ng.extra_perms {
sqlb.set_str("extra_perms", extra_perms.to_string());
sqlb.set(
"extra_perms",
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
);
}
sqlb.returning("*");

View File

@@ -102,10 +102,9 @@ pub fn global_service() -> Router {
async fn get_result_by_id(
Extension(db): Extension<DB>,
Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
) -> windmill_common::error::JsonResult<serde_json::Value> {
let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id).await?;
Ok(Json(res))
}
@@ -177,11 +176,6 @@ async fn get_job(
Ok(Json(job))
}
#[derive(Deserialize)]
pub struct ResultByIdQuery {
pub skip_direct: bool,
}
pub async fn get_job_by_id<'c>(
mut tx: Transaction<'c, Postgres>,
w_id: &str,
@@ -295,41 +289,53 @@ impl RunJobQuery {
fn add_include_headers(
&self,
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let whitelist = self
.include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
return add_include_headers(&self.include_header, headers, args);
}
}
pub fn add_include_headers(
include_header: &Option<String>,
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
if include_header.is_none() {
return args;
}
let whitelist = include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
}
#[derive(Deserialize)]
pub struct ListQueueQuery {
pub script_path_start: Option<String>,
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_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 {
@@ -358,11 +364,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.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("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(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(s) = &lq.suspended {
@@ -372,6 +378,7 @@ 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",
@@ -379,6 +386,10 @@ 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
}
@@ -451,13 +462,14 @@ async fn list_jobs(
script_path_exact: lq.script_path_exact,
script_hash: lq.script_hash,
created_by: lq.created_by,
created_before: lq.created_before,
created_after: lq.created_after,
started_before: lq.started_before,
started_after: lq.started_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",
@@ -1068,6 +1080,8 @@ impl From<UnifiedJob> for Job {
visible_to_owner: uj.visible_to_owner,
suspend: uj.suspend,
mem_peak: uj.mem_peak,
root_job: None,
leaf_jobs: None,
}),
t => panic!("job type {} not valid", t),
}
@@ -1130,15 +1144,15 @@ 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,
Extension(user_db): Extension<UserDB>,
@@ -1163,6 +1177,7 @@ pub async fn run_flow_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1198,6 +1213,7 @@ pub async fn run_job_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1350,6 +1366,7 @@ pub async fn run_wait_result_job_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1396,6 +1413,7 @@ pub async fn run_wait_result_job_by_hash(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1441,6 +1459,7 @@ pub async fn run_wait_result_flow_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1503,6 +1522,7 @@ async fn run_preview_job(
scheduled_for,
None,
None,
None,
false,
false,
None,
@@ -1536,6 +1556,7 @@ async fn run_preview_flow_job(
scheduled_for,
None,
None,
None,
false,
false,
None,
@@ -1571,6 +1592,7 @@ pub async fn run_job_by_hash(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1673,11 +1695,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.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("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(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(sk) = &lq.is_skipped {
sqlb.and_where_eq("is_skipped", sk);
@@ -1692,6 +1714,14 @@ 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)]
@@ -1700,8 +1730,8 @@ pub struct ListCompletedQuery {
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub success: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
@@ -1709,6 +1739,10 @@ 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(
@@ -1775,6 +1809,7 @@ async fn get_completed_job(
.fetch_optional(&db)
.await?;
tracing::info!("job_o: {:?}", job_o);
let job = not_found_if_none(job_o, "Completed Job", id.to_string())?;
Ok(Json(job))
}

View File

@@ -734,14 +734,10 @@ async fn slack_command(
.map_err(|_| error::Error::BadRequest("invalid payload".to_string()))?;
let body = String::from_utf8_lossy(&body);
if SLACK_SIGNING_SECRET
.as_ref()
.as_ref()
.map(|sv| sv.verify(&ts, &body, &sig).ok())
.flatten()
.is_none()
{
return Err(error::Error::BadRequest("verification failed".to_owned()));
if let Some(sv) = SLACK_SIGNING_SECRET.as_ref() {
if sv.verify(&ts, &body, &sig).ok().is_none() {
return Err(error::Error::BadRequest("verification failed".to_owned()));
}
}
let mut tx = db.begin().await?;
@@ -785,6 +781,7 @@ async fn slack_command(
None,
None,
None,
None,
false,
false,
None,

View File

@@ -70,6 +70,7 @@ 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))
@@ -123,6 +124,7 @@ async fn list_scripts(
AND workspace_id = ?)"
.bind(&w_id),
);
sqlb.and_where_eq("archived", true);
} else {
sqlb.and_where_eq("archived", false);
}
@@ -377,6 +379,7 @@ async fn create_script(
None,
None,
None,
None,
false,
false,
None,
@@ -718,6 +721,46 @@ 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

@@ -1311,7 +1311,9 @@ async fn create_user(
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &nu.email, "name": &nu.name, "event": "new_user"}))
.json(
&serde_json::json!({"email" : &nu.email, "name": &nu.name, "event": "global_add"}),
)
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));

View File

@@ -26,6 +26,7 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde_json::Value;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -378,9 +379,26 @@ 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 WHERE path = $2 AND workspace_id = $3",
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4",
npath,
v,
path,
w_id
)
@@ -419,6 +437,23 @@ 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,6 +37,7 @@ 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,
ping_at: chrono::DateTime<chrono::Utc>,
last_ping: Option<i32>,
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 * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
"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",
per_page as i64,
offset as i64
)

View File

@@ -965,7 +965,7 @@ async fn invite_user(
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = &HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &nu.email, "event": "new_invite"}))
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_invite"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
@@ -1002,6 +1002,15 @@ async fn add_user(
tx.commit().await?;
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_add"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
Ok((
StatusCode::CREATED,
format!("user with email {} added", nu.email),
@@ -1064,6 +1073,7 @@ struct ScriptMetadata {
schema: Option<Schema>,
is_template: bool,
lock: Vec<String>,
kind: String,
}
enum ArchiveImpl {
@@ -1226,6 +1236,7 @@ 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

@@ -125,7 +125,7 @@ pub enum FlowStatusModule {
},
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobResult {
SingleJob(Uuid),
ListJob(Vec<Uuid>),

View File

@@ -7,7 +7,7 @@
*/
use std::{
fmt::Display,
fmt::{self, Display},
hash::{Hash, Hasher},
};
@@ -103,6 +103,18 @@ 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

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::{collections::HashMap, str::FromStr};
use std::collections::HashMap;
use anyhow::Context;
use reqwest::Client;
@@ -152,55 +152,24 @@ pub async fn pull(
pub async fn get_result_by_id(
db: Pool<Postgres>,
mut skip_direct: bool,
w_id: String,
flow_id: String,
flow_id: Uuid,
node_id: String,
) -> error::Result<serde_json::Value> {
let mut result_id: Option<JobResult> = None;
let mut parent_id = Uuid::from_str(&flow_id).ok();
while result_id.is_none() && parent_id.is_some() {
if !skip_direct {
let r = sqlx::query!(
"SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 ",
parent_id.unwrap(),
w_id,
)
.fetch_optional(&db)
.await?;
if let Some(r) = r {
let value = r
.flow_status
.as_ref()
.ok_or_else(|| Error::InternalErr(format!("requiring a flow status value")))?
.to_owned();
parent_id = r.parent_job;
let status_o = serde_json::from_value::<FlowStatus>(value).ok();
result_id = status_o.and_then(|status| {
status
.modules
.iter()
.find(|m| m.id() == node_id)
.and_then(|m| m.job_result())
});
} else {
parent_id = None;
}
} else {
let q_parent = sqlx::query_scalar!(
"SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2",
parent_id.unwrap(),
w_id,
)
.fetch_optional(&db)
.await?
.flatten();
parent_id = q_parent;
skip_direct = false
}
}
let job_result: Option<JobResult> = sqlx::query_scalar!(
"SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
node_id,
flow_id,
w_id,
)
.fetch_optional(&db)
.await?
.flatten()
.map(|x| serde_json::from_value(x).ok())
.flatten();
let result_id = windmill_common::utils::not_found_if_none(
result_id,
job_result,
"Flow result by id",
format!("{}, {}", flow_id, node_id),
)?;
@@ -282,6 +251,7 @@ pub async fn push<'c>(
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
schedule_path: Option<String>,
parent_job: Option<Uuid>,
root_job: Option<Uuid>,
is_flow_step: bool,
mut same_worker: bool,
pre_run_error: Option<&windmill_common::error::Error>,
@@ -534,12 +504,13 @@ pub async fn push<'c>(
.unwrap_or_else(|| (None, None));
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
let uuid = sqlx::query_scalar!(
"INSERT INTO queue
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) \
RETURNING id",
workspace_id,
job_id,
@@ -562,7 +533,8 @@ pub async fn push<'c>(
same_worker,
pre_run_error.map(|e| e.to_string()),
email,
visible_to_owner
visible_to_owner,
root_job
)
.fetch_one(&mut tx)
.await
@@ -675,6 +647,10 @@ pub struct QueuedJob {
pub suspend: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root_job: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_jobs: Option<serde_json::Value>,
}
impl QueuedJob {

View File

@@ -85,6 +85,7 @@ pub async fn push_scheduled_job<'c>(
Some(next),
Some(schedule.path.clone()),
None,
None,
false,
false,
None,

View File

@@ -40,7 +40,7 @@ pub async fn eval_timeout(
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
let base_internal_url: String = base_internal_url.to_string();
timeout(
std::time::Duration::from_millis(2000),
std::time::Duration::from_millis(3000),
tokio::task::spawn_blocking(move || {
let mut ops = vec![];
@@ -104,7 +104,7 @@ pub async fn eval_timeout(
isolate.terminate_execution();
};
Error::ExecutionErr(format!(
"The expression of evaluation `{expr2}` took too long to execute (>2000ms)"
"The expression of evaluation `{expr2}` took too long to execute (>3000ms)"
))
})??
}
@@ -145,7 +145,7 @@ fn add_closing_bracket(s: &str) -> String {
s
}
const SPLIT_PAT: &str = ";\n";
const SPLIT_PAT: &str = ";";
async fn eval(
context: &mut JsRuntime,
expr: &str,
@@ -154,14 +154,21 @@ async fn eval(
by_id: Option<IdContext>,
base_internal_url: &str,
) -> anyhow::Result<serde_json::Value> {
let expr = expr.trim();
let expr = format!(
"{}\nreturn {};",
expr.split(SPLIT_PAT)
.take(expr.split(SPLIT_PAT).count() - 1)
.join("\n"),
expr.split(SPLIT_PAT).last().unwrap_or_else(|| "")
);
let exprs = expr
.trim()
.split(SPLIT_PAT)
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.collect::<Vec<&str>>();
let expr = if exprs.is_empty() {
"return undefined;".to_string()
} else {
format!(
"{};\n return {};",
exprs.iter().take(exprs.len() - 1).join(";\n"),
exprs.last().unwrap()
)
};
let (api_code, by_id_code) = if let Some(EvalCreds { workspace, token }) = creds {
let by_id_code = if let Some(by_id) = by_id {
format!(
@@ -198,12 +205,12 @@ const results = new Proxy({{}}, {{
.into_iter()
.map(|(k, v)| {
let v_str = match v {
JobResult::SingleJob(x) => x.to_string(),
JobResult::SingleJob(x) => format!("\"{x}\""),
JobResult::ListJob(x) => {
format!("[{}]", x.iter().map(|x| x.to_string()).join(","))
format!("[{}]", x.iter().map(|x| format!("\"{x}\"")).join(","))
}
};
format!("\"{k}\": \"{v_str}\"")
format!("\"{k}\": {v_str}")
})
.join(","),
by_id.previous_id,
@@ -292,9 +299,8 @@ async fn op_get_result(args: Vec<String>) -> Result<serde_json::Value, anyhow::E
let base_url = &args[3];
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client
.get_completed_job(workspace, &id.parse()?)
.get_completed_job_result(workspace, &id.parse()?)
.await?
.result
.clone();
Ok(serde_json::json!(result))
}
@@ -309,7 +315,7 @@ async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyho
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client
.result_by_id(workspace, flow_job_id, node_id, Some(true))
.result_by_id(workspace, flow_job_id, node_id)
.await
.map_or(None, |e| Some(e.into_inner()));

View File

@@ -34,7 +34,7 @@ use tokio::{
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
process::{Child, Command},
sync::{
mpsc::{self, Sender}, watch,
mpsc::{self, Sender}, watch, broadcast,
},
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_ENV_CHECK: u64 = 15;
const NUM_SECS_PING: u64 = 5;
const NUM_SECS_SYNC: u64 = 60 * 10;
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
@@ -330,7 +330,7 @@ lazy_static::lazy_static! {
.ok()
.map(|x| format!(";{x}"))
.unwrap_or_else(|| String::new());
static ref NPM_CONFIG_REGISTRY: Option<String> = std::env::var("NPM_CONFIG_REGISTRY").ok();
@@ -553,7 +553,7 @@ pub async fn run_worker(
let do_break = async {
if last_ping.elapsed().as_secs() > NUM_SECS_ENV_CHECK {
if last_ping.elapsed().as_secs() > NUM_SECS_PING {
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).await
handle_dependency_job(&job, &mut logs, job_dir, db, worker_name).await
}
JobKind::FlowDependencies => {
handle_flow_dependency_job(&job, &mut logs, job_dir, db)
handle_flow_dependency_job(&job, &mut logs, job_dir, db, worker_name)
.await
.map(|()| Value::Null)
}
@@ -912,7 +912,8 @@ async fn handle_queued_job(
job_dir,
worker_dir,
&mut logs,
base_internal_url
base_internal_url,
worker_name
)
.await
}
@@ -1064,7 +1065,9 @@ async fn handle_code_execution_job(
job_dir: &str,
worker_dir: &str,
logs: &mut String,
base_internal_url: &str
base_internal_url: &str,
worker_name: &str
) -> error::Result<serde_json::Value> {
let (inner_content, requirements_o, language) = match job.job_kind {
JobKind::Preview | JobKind::Script_Hub => (
@@ -1086,7 +1089,6 @@ 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()
@@ -1136,7 +1138,7 @@ mount {{
token,
&inner_content,
&shared_mount,
base_internal_url
base_internal_url,
)
.await
}
@@ -1151,7 +1153,8 @@ mount {{
&inner_content,
&shared_mount,
requirements_o,
base_internal_url
base_internal_url,
worker_name
)
.await
}
@@ -1166,7 +1169,8 @@ mount {{
job_dir,
requirements_o,
&shared_mount,
base_internal_url
base_internal_url,
worker_name
)
.await
}
@@ -1179,7 +1183,8 @@ mount {{
&inner_content,
job_dir,
&shared_mount,
base_internal_url
base_internal_url,
worker_name
)
.await
}
@@ -1208,6 +1213,7 @@ 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");
@@ -1236,6 +1242,7 @@ async fn handle_go_job(
db,
true,
skip_go_mod,
worker_name
)
.await?;
@@ -1353,7 +1360,7 @@ func Run(req Req) (interface{{}}, error){{
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(&job.id, db, logs, build_go, false).await?;
handle_child(&job.id, db, logs, build_go, false, worker_name).await?;
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
@@ -1379,7 +1386,7 @@ func Run(req Req) (interface{{}}, error){{
.stderr(Stdio::piped())
.spawn()?
};
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
read_result(job_dir).await
}
@@ -1393,6 +1400,7 @@ 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;
@@ -1456,7 +1464,7 @@ async fn handle_bash_job(
.stderr(Stdio::piped())
.spawn()?
};
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
//for now bash jobs have an empty result object
Ok(serde_json::json!(logs
.lines()
@@ -1473,6 +1481,26 @@ fn capitalize(s: &str) -> String {
}
}
fn get_common_deno_proc_envs(token: &str, base_internal_url: &str) -> HashMap<String, String> {
let hostname_base = BASE_URL.split("://").last().unwrap_or("localhost");
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
let deno_auth_tokens_base = DENO_AUTH_TOKENS.as_str();
let deno_auth_tokens =
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens_base}",);
let mut deno_envs: HashMap<String, String> = HashMap::from([
(String::from("PATH"), PATH_ENV.clone()),
(String::from("DENO_AUTH_TOKENS"), deno_auth_tokens),
(String::from("BASE_INTERNAL_URL"), base_internal_url.to_string()),
(String::from("NO_COLOR"), String::from("true")),
]);
if let Some(ref s) = *NPM_CONFIG_REGISTRY {
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
}
return deno_envs;
}
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_deno_job(
logs: &mut String,
@@ -1484,7 +1512,8 @@ async fn handle_deno_job(
inner_content: &String,
shared_mount: &str,
lockfile: Option<String>,
base_internal_url: &str
base_internal_url: &str,
worker_name: &str
) -> error::Result<serde_json::Value> {
logs.push_str("\n\n--- DENO CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
@@ -1537,11 +1566,9 @@ run().catch(async (e) => {{
let mut reserved_variables = get_reserved_variables(job, &token, db).await?;
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
let hostname_base = BASE_URL.split("://").last().unwrap_or("localhost");
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
let deno_auth_tokens_base = DENO_AUTH_TOKENS.as_str();
let deno_auth_tokens =
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens_base}",);
let common_deno_proc_envs = get_common_deno_proc_envs(&token, base_internal_url);
//do not cache local dependencies
let reload = format!("--reload={base_internal_url}");
let child = async {
Ok(if !*DISABLE_NSJAIL {
let _ = write_file(
@@ -1565,6 +1592,7 @@ run().catch(async (e) => {{
}
args.push("--import-map");
args.push("/tmp/import_map.json");
args.push(&reload);
args.push("--unstable");
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
for flag in deno_flags {
@@ -1579,9 +1607,7 @@ run().catch(async (e) => {{
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("DENO_AUTH_TOKENS", deno_auth_tokens)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(common_deno_proc_envs)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -1593,6 +1619,7 @@ run().catch(async (e) => {{
args.push("run");
args.push("--import-map");
args.push(&import_map_path);
args.push(&reload);
args.push("--unstable");
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
for flag in deno_flags {
@@ -1606,11 +1633,8 @@ run().catch(async (e) => {{
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.envs(common_deno_proc_envs)
.env("DENO_DIR", DENO_CACHE_DIR)
.env("DENO_AUTH_TOKENS", deno_auth_tokens)
.env("BASE_INTERNAL_URL", base_internal_url)
.env("NO_COLOR", "true")
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -1619,7 +1643,7 @@ run().catch(async (e) => {{
}
.instrument(trace_span!("create_deno_jail"))
.await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
read_result(job_dir).await
}
@@ -1657,7 +1681,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;
@@ -1671,7 +1695,7 @@ async fn handle_python_job(
if requirements.is_empty() {
"".to_string()
} else {
pip_compile(&job.id, &requirements, logs, job_dir, db)
pip_compile(&job.id, &requirements, logs, job_dir, db, worker_name)
.await
.map_err(|e| {
Error::ExecutionErr(format!("pip compile failed: {}", e.to_string()))
@@ -1888,7 +1912,7 @@ mount {{
.spawn()?
};
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
read_result(job_dir).await
}
@@ -1918,6 +1942,7 @@ 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,
@@ -1932,7 +1957,8 @@ async fn handle_dependency_job(
.unwrap_or_else(|| "no raw code"),
logs,
job_dir,
db
db,
worker_name
)
.await;
match content {
@@ -1967,6 +1993,7 @@ 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(
@@ -1997,6 +2024,7 @@ async fn handle_flow_dependency_job(
logs,
job_dir,
db,
worker_name
)
.await;
match new_lock {
@@ -2111,12 +2139,13 @@ async fn capture_dependency_job(
job_raw_code: &str,
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str
) -> 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 ).await
pip_compile(job_id, job_raw_code, logs, job_dir, db, worker_name).await
}
ScriptLang::Go => {
install_go_dependencies(
@@ -2127,6 +2156,7 @@ async fn capture_dependency_job(
db,
false,
false,
worker_name
)
.await
}
@@ -2144,6 +2174,7 @@ 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;
@@ -2176,7 +2207,7 @@ async fn pip_compile(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false)
handle_child(job_id, db, logs, child, false, worker_name)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
let path_lock = format!("{job_dir}/requirements.txt");
@@ -2199,6 +2230,7 @@ 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?;
@@ -2209,7 +2241,7 @@ async fn install_go_dependencies(
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false).await?;
handle_child(job_id, db, logs, child, false, worker_name).await?;
}
let child = Command::new(GO_PATH.as_str())
.current_dir(job_dir)
@@ -2217,7 +2249,7 @@ async fn install_go_dependencies(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false)
handle_child(job_id, db, logs, child, false, worker_name)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
@@ -2323,6 +2355,7 @@ 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);
@@ -2336,11 +2369,14 @@ 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 */
@@ -2350,10 +2386,22 @@ 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 };
@@ -2387,10 +2435,10 @@ async fn handle_child(
biased;
result = child.wait() => return result.map(Ok),
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
_ = update_job => KillReason::Cancelled,
_ = sleep(*TIMEOUT_DURATION) => KillReason::Timeout,
_ = update_job => KillReason::Cancelled,
};
tx.send(()).await.expect("rx should never be dropped");
tx.send(()).expect("rx should never be dropped");
drop(tx);
let set_reason = async {
@@ -2413,7 +2461,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))
@@ -2429,12 +2477,13 @@ 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;
let mut output = output.take_until(rx2.recv()).boxed();
/* `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 })
@@ -2449,11 +2498,14 @@ 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);
@@ -2473,6 +2525,7 @@ 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`
@@ -2489,8 +2542,7 @@ 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}");
@@ -2500,6 +2552,7 @@ async fn handle_child(
if *set_too_many_logs.borrow() {
break;
}
}
/* drop our end of the pipe */
@@ -2813,7 +2866,7 @@ async fn handle_python_reqs(
.spawn()?
};
let child = handle_child(&job.id, db, logs, child, false).await;
let child = handle_child(&job.id, db, logs, child, false, worker_name).await;
tracing::info!(
worker_name = %worker_name,
job_id = %job.id,

View File

@@ -326,6 +326,21 @@ pub async fn update_flow_status_after_job_completion(
)
.execute(&mut tx)
.await?;
if let Some(job_result) = new_status.job_result() {
sqlx::query!(
"
UPDATE queue
SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)
WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id
",
new_status.id(),
json!(job_result),
flow
)
.execute(&mut tx)
.await?;
}
}
}
@@ -1237,6 +1252,7 @@ async fn push_next_flow_job(
Ok(v) => (Some(v), None),
Err(e) => (None, Some(e)),
};
let root_job = flow_job.root_job.or_else(|| Some(flow_job.id));
let (uuid, inner_tx) = push(
tx,
&flow_job.workspace_id,
@@ -1248,6 +1264,7 @@ async fn push_next_flow_job(
scheduled_for_o,
flow_job.schedule_path.clone(),
Some(flow_job.id),
root_job,
true,
continue_on_same_worker,
err,

View File

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

View File

@@ -18,12 +18,12 @@ export type Context = {
};
async function tryResolveWorkspace(
opts: GlobalOptions,
opts: GlobalOptions
): Promise<
{ isError: false; value: Workspace } | { isError: true; error: string }
> {
const cache = (opts as any).__secret_workspace;
if (cache) return cache;
if (cache) return { isError: false, value: cache };
if (opts.workspace) {
const e = await getWorkspaceByName(opts.workspace);
@@ -49,21 +49,23 @@ async function tryResolveWorkspace(
}
export async function resolveWorkspace(
opts: GlobalOptions,
opts: GlobalOptions
): Promise<Workspace> {
const res = await tryResolveWorkspace(opts);
if (res.isError) {
console.log(res.error);
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);
if (!token) {
token = workspace.token;
}
@@ -74,26 +76,26 @@ export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo>
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) {
throw new Error("Could not reauth");
}
removeWorkspace(workspace.name);
removeWorkspace(workspace.name, false, opts);
workspace.token = newToken;
addWorkspace(workspace);
addWorkspace(workspace, opts);
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;
@@ -103,12 +105,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;
@@ -117,7 +119,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")) {
@@ -125,19 +127,16 @@ 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,6 +94,7 @@ export class FlowFile implements Resource, PushDiffs {
...changeset,
...base_changeset,
}
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
@@ -114,20 +115,11 @@ 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(remote ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
}
@@ -192,7 +184,7 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
}
async function run(
opts: GlobalOptions & {
input: string[];
data?: string;
silent: boolean;
},
path: string,
@@ -200,7 +192,8 @@ async function run(
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const input = await resolve(opts.input);
const input = opts.data ? await resolve(opts.data) : {};
const id = await JobService.runFlowByPath({
workspace: workspace.workspaceId,
@@ -236,6 +229,7 @@ async function run(
if (!opts.silent) {
console.log(colors.green.underline.bold("Flow ran to completion"));
console.log()
}
const jobInfo = await JobService.getCompletedJob({
workspace: workspace.workspaceId,
@@ -257,8 +251,8 @@ const command = new Command()
.command("run", "run a flow by path.")
.arguments("<path:string>")
.option(
"-i --input [inputs...:string]",
"Inputs specified as JSON objects or simply as <name>=<value>. Supports file inputs using @<filename> and stdin using @- these also need to be formatted as JSON. Later inputs override earlier ones.",
"-d --data <data:string>",
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-.",
)
.option(
"-s --silent",

View File

@@ -1,4 +1,4 @@
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
import { colors, Command, FolderService, microdiff } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
@@ -22,7 +22,9 @@ 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);
@@ -31,16 +33,10 @@ 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(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
@@ -66,22 +62,24 @@ export class FolderFile implements Resource, PushDiffs {
if (exists) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing folder...`,
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`,
),
);
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"].includes(diff.path[0] as string)
!["owners", "extra_perms", "display_name"].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") {
@@ -97,11 +95,10 @@ export class FolderFile implements Resource, PushDiffs {
if (!hasChanges) {
return;
}
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: changeset,
requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined}
});
} 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.70.1";
const VERSION = "v1.77.0";
let command: any = new Command()
.name("wmill")

View File

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

View File

@@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs {
})
) {
console.log(
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`),
);
const changeset: EditResource = {
@@ -51,11 +51,7 @@ export class ResourceFile implements Resource2, PushDiffs {
};
for (const diff of diffs) {
if (diff.path[0] === "is_oauth") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
//is_oauth is not updatable
continue;
}
if (
@@ -64,9 +60,10 @@ 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") {
@@ -110,19 +107,10 @@ 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(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
}

View File

@@ -11,6 +11,7 @@ import {
Table,
} from "./deps.ts";
import { Any, array, decoverto, model, property } from "./decoverto.ts";
import { writeAllSync } from "https://deno.land/std@0.176.0/streams/mod.ts";
@model()
export class ScriptFile {
@@ -41,6 +42,7 @@ export class ScriptFile {
},
toPlain: (data) => data,
})
@property(() => String)
kind?: "script" | "failure" | "trigger" | "command" | "approval";
constructor(summary: string, description: string) {
@@ -104,11 +106,21 @@ export async function handleFile(path: string, content: string, workspace: strin
} catch { }
const language = inferContentTypeFromFilePath(path);
let remote = undefined
try {
const remote = await ScriptService.getScriptByPath({
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: {
@@ -122,10 +134,11 @@ 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}`))
} catch {
} else {
// no parent hash
await ScriptService.createScript({
workspace: workspace,
@@ -143,7 +156,6 @@ export async function handleFile(path: string, content: string, workspace: strin
},
});
console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`))
}
return true
}
@@ -280,50 +292,27 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
.render();
}
export async function resolve(inputs: string[]): Promise<Record<string, any>> {
let result = {};
if (!inputs) {
return result;
export async function resolve(input: string): Promise<Record<string, any>> {
if (!input) {
throw new Error("No data given");
}
for (const input of inputs) {
let data: string;
if (input.startsWith("@")) {
if (input == "@-") {
data = new TextDecoder().decode(await readAll(Deno.stdin));
} else {
data = await Deno.readTextFile(input.substring(1));
}
} else {
if (input.startsWith("{")) {
data = input;
} else {
const key = input.split("=", 1)[0];
const value = input.substring(key.length + 1);
let o;
try {
o = JSON.parse(value);
} catch {
o = value;
}
data = JSON.stringify(Object.fromEntries([[key, o]]));
}
}
let jsonObj;
try {
jsonObj = JSON.parse(data);
} catch {
jsonObj = data;
}
result = { ...result, ...jsonObj };
if (input == "@-") {
input = new TextDecoder().decode(await readAll(Deno.stdin));
} if (input[0] == "@") {
input = await Deno.readTextFile(input.substring(1));
}
try {
return JSON.parse(input);
} catch (e) {
console.error("Impossible to parse input as JSON", input)
throw e
}
return result;
}
async function run(
opts: GlobalOptions & {
input: string[];
data?: string;
silent: boolean;
},
path: string,
@@ -331,7 +320,8 @@ async function run(
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const input = await resolve(opts.input);
const input = opts.data ? await resolve(opts.data) : {};
const id = await JobService.runScriptByPath({
workspace: workspace.workspaceId,
path,
@@ -364,7 +354,9 @@ export async function track_job(workspace: string, id: string) {
const result = await JobService.getCompletedJob({ workspace, id });
console.log(result.logs);
console.log()
console.log(colors.bold.underline.green("Job Completed"));
console.log()
return;
} catch {
/* ignore */
@@ -403,7 +395,7 @@ export async function track_job(workspace: string, id: string) {
}
if (updates.new_logs) {
console.log(updates.new_logs);
writeAllSync(Deno.stdout, new TextEncoder().encode(updates.new_logs));
logOffset += updates.new_logs.length;
}
@@ -426,12 +418,15 @@ export async function track_job(workspace: string, id: string) {
if ((final_job.logs?.length ?? -1) > logOffset) {
console.log(final_job.logs!.substring(logOffset));
}
console.log("\n")
if (final_job.success) {
console.log(colors.bold.underline.green("Job Completed"));
} else {
console.log(colors.bold.underline.red("Job Completed"));
}
console.log()
} catch {
console.log("Job appears to have completed, but no data can be retrieved");
}
@@ -466,8 +461,8 @@ const command = new Command()
.command("run", "run a script by path")
.arguments("<path:string>")
.option(
"-i --input [inputs...:string]",
"Inputs specified as JSON objects or simply as <name>=<value>. Supports file inputs using @<filename> and stdin using @- these also need to be formatted as JSON. Later inputs override earlier ones.",
"-d --data <data:string>",
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-.",
)
.option(
"-s --silent",

View File

@@ -13,7 +13,7 @@ import {
ResourceService,
VariableService,
AppService,
FlowService
FlowService,
} from "./deps.ts";
import {
Difference,
@@ -25,16 +25,13 @@ 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/mod.ts";
import { diffCharacters } from "https://deno.land/x/diff/mod.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";
type DynFSElement = {
isDirectory: boolean;
path: string;
@@ -44,10 +41,7 @@ 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),
@@ -113,16 +107,14 @@ 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;
@@ -130,18 +122,20 @@ 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;
}[] = [
{
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;
},
},
getContentText(): Promise<string> {
throw undefined;
},
}];
];
while (stack.length > 0) {
const e = stack.pop()!;
@@ -161,17 +155,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;
@@ -179,20 +173,26 @@ async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirecto
return map;
}
async function compareDynFSElement(
els1: DynFSElement, els2: DynFSElement,
ignore: (path: string, isDirectory: boolean) => boolean,
raw: boolean
els1: DynFSElement,
els2: DynFSElement | undefined,
ignore: (path: string, isDirectory: boolean) => boolean
): Promise<Change[]> {
const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] :
await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]);
const [m1, m2] = els2
? await Promise.all([
elementsToMap(els1, ignore),
elementsToMap(els2, ignore),
])
: [await elementsToMap(els1, 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,74 +263,96 @@ 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 = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
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(`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) {
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 && currentLocal !== change.after) {
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 },
)
console.log(`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`)
await applyDiff(
diffs,
target,
const diffs = microdiff(
JSON.parse(change.before),
JSON.parse(change.after),
{ cyclesFix: false }
);
console.log(
`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`
);
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) {
@@ -338,8 +360,10 @@ 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);
}
@@ -352,24 +376,29 @@ 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)) {
@@ -385,8 +414,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));
@@ -415,17 +444,20 @@ 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)
);
}
}
}
@@ -452,138 +484,208 @@ 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 = ZipFSElement((await downloadZip(workspace))!)
const local = await FSFSElement(path.join(Deno.cwd(), ""))
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
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(`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 {
Deno.remove(stateTarget)
} catch { }
await 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,
@@ -594,11 +696,11 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
| 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("/");
@@ -609,14 +711,14 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
}
}
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(e.body)
console.error("Failing to apply diffs to " + remotePath);
console.error(JSON.stringify(e));
}
}
}
@@ -624,18 +726,24 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
const command = new Command()
.command("pull")
.description(
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking.",
"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("--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")
.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("--yes", "Push without needing confirmation")
.option("--raw", "Push without using state, just overwrite.")
.action(push as any);

View File

@@ -11,7 +11,6 @@ import {
colors,
Command,
EditVariable,
ListableVariable,
microdiff,
Table,
VariableService,
@@ -67,7 +66,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...`,
`Applying ${diffs.length} diffs to existing variable... ${remotePath}`,
),
);
const changeset: EditVariable = {};
@@ -76,11 +75,12 @@ export class VariableFile implements Resource, PushDiffs {
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["path", "value", "is_secret", "description"].includes(
!["path", "value", "is_secret", "description", "account", "is_oauth"].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,6 +96,7 @@ export class VariableFile implements Resource, PushDiffs {
if (!hasChanges) {
return;
}
await VariableService.updateVariable({
workspace,
path: remotePath,
@@ -103,7 +104,6 @@ export class VariableFile implements Resource, PushDiffs {
requestBody: changeset,
});
console.log(changeset);
} else {
console.log(colors.yellow.bold("Creating new variable..."));
await VariableService.createVariable({
@@ -121,19 +121,10 @@ 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(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
}

View File

@@ -9,9 +9,11 @@ import {
Input,
setClient,
Table,
UserService,
WorkspaceService,
} from "./deps.ts";
import { decoverto, model, property } from "./decoverto.ts";
import { requireLogin } from "./context.ts";
@model()
export class Workspace {
@@ -137,6 +139,8 @@ async function list(opts: GlobalOptions) {
}),
)
.render();
console.log('Active: ' + colors.green.bold(activeName || 'none'))
}
async function switchC(opts: GlobalOptions, workspaceName: string) {
@@ -308,6 +312,13 @@ async function remove(_opts: GlobalOptions, name: string) {
await removeWorkspace(name, false, _opts);
}
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()
.description("workspace related commands")
.action(list as any)
@@ -335,6 +346,9 @@ const command = new Command()
.command("remove")
.description("Remove a workspace")
.arguments("<workspace_name:string>")
.action(remove as any);
.action(remove as any)
.command("whoami")
.description("Show the currently active user")
.action(whoami as any);
export default command;

View File

@@ -38,7 +38,9 @@ services:
depends_on:
db:
condition: service_healthy
# volumes:
# - ./oauth.json/:/usr/src/app/oauth.json
windmill_worker:
image: ghcr.io/windmill-labs/windmill:main
deploy:
@@ -61,7 +63,6 @@ services:
# to mount the worker folder to debug,, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
- worker_dependency_cache:/tmp/windmill/cache
# - ./oauth.json/:/usr/src/app/oauth.json
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest

1
frontend/.env Normal file
View File

@@ -0,0 +1 @@
VITE_DEFAULT_WEBHOOK_TYPE=async

1
frontend/.gitignore vendored
View File

@@ -7,3 +7,4 @@ node_modules
CaddyfileRemoteRuben
tests-out/
storageState.json
.env.production

View File

@@ -1,6 +1,6 @@
# Developing
## Starting the Development Server
## Starting The Development Server
Once you've created a project and installed dependencies with `npm install` (or
`pnpm install` or `yarn`), start a development server:
@@ -17,7 +17,7 @@ In the root folder:
```bash
docker build . -t windmill
docker-compose up db server
docker compose up db windmill_server windmill_worker
```
### 2. Backend is run by cargo
@@ -25,9 +25,9 @@ docker-compose up db server
**Prerequisites**
- Install Rust [as explained on the website](https://www.rust-lang.org/tools/install).
- Install llvm
- Install llvm
**on OSX:**
**On OSX:**
```bash
brew install llvm caddy gsed
@@ -36,7 +36,20 @@ docker-compose up db server
# now, restart your shell. You should now have the `lld` binary on your PATH.
```
- To test that you have Rust and Cargo installed run `cargo --version`
- In your terminal, go to the backend directory and run `cargo build`
- Run `cargo run`
**Known issue on M1 Mac while running `cargo build`**
- You may encounter `linking with cc failed` build time error.
- To solve this run:
```bash
echo 'export RUSTFLAGS="-L/opt/homebrew/opt/libomp/lib"' >> ~/.zshrc
source ~/.zshrc
```
**Do a Frontend Build**
In order to run the backend, you need to have a frontend build inside `frontend/build/`.
@@ -56,6 +69,14 @@ npm run build
# now, you'll have a `frontend/build` folder.
```
**Known issue while running `npm run build`**
- You may encounter `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory` error.
- To solve this run:
```bash
export NODE_OPTIONS=--max_old_space_size=8096
```
- run `npm run build` again
In the root folder:
```bash
@@ -82,7 +103,7 @@ sudo caddy run --config ./Caddyfile
and then go to <http://localhost>
### Backend is run by remote!
### 3. Backend is run by remote!
```bash
sudo caddy run --config ./CaddyfileRemote
@@ -119,8 +140,30 @@ Recommended config for VS Code:
```json
"[svelte]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
```
- turn _format on save_ on
## Building
The project is built with [SvelteKit](https://kit.svelte.dev/) and uses as output static files.
There are others adapters for sveltekit, but we use the static adapter.
To build the frontend as static assets, use:
```
npm run build
```
The output is in the `build` folder.
The default build assume you serve every non static files as the 200.html file which is catchall. If you prefer a normal layout, you can use:
```
NOTCATCHALL=true npm run build
```
which will generate an index.html and allow you to serve the frontend with any static server.
Env variables used for build are set in .env file. See [https://vitejs.dev/guide/env-and-mode.html#env-files](https://vitejs.dev/guide/env-and-mode.html#env-files) for more details.

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.70.1",
"version": "1.77.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -21,12 +21,12 @@
"@sveltejs/package": "^1.0.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
"@types/d3-zoom": "^3.0.2",
"@types/node": "^18.11.18",
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.4",
"@windmill-labs/svelvet": "^4.0.20",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -34,8 +34,10 @@
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0",
"ol": "^7.2.2",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
"pdfjs-dist": "^3.4.120",
"postcss": "^8.4.18",
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.3",
@@ -44,6 +46,7 @@
"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",
@@ -71,6 +74,7 @@
"chart.js": "^3.9.1",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-zoom": "^2.0.0",
"d3-zoom": "^3.0.0",
"date-fns": "^2.29.3",
"fast-equals": "^4.0.3",
"highlight.js": "^11.7.0",

View File

@@ -1,78 +1,2 @@
/// <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
}
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

@@ -50,12 +50,23 @@
.Template-editor span.mtk20 {
color: black !important;
}
::-webkit-scrollbar {
width: 9px;
height: 9px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(155, 155, 155, 0.5);
border: transparent;
}
}
@layer components {
/* Flow graph viewer -> Svelvet library internal class overwrite */
.Node {
display: flex !important;
cursor: pointer !important;
}
}

View File

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

View File

@@ -110,9 +110,9 @@
{#if result != undefined}
{#if resultKind && resultKind != 'json'}
<div class="mb-2 text-gray-500 text-sm bg-gray-50/20">
as JSON&nbsp;<input type="checkbox" bind:checked={forceJson} /></div
as JSON&nbsp;<input class="windmillapp" type="checkbox" bind:checked={forceJson} /></div
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
class="mb-2 text-sm text-gray-700 relative"
class="mb-2 min-w-[400px] text-sm text-gray-700 relative"
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
<div class="text-gray-500 text-sm absolute top-0 right-2">
<button on:click={jsonViewer.openDrawer}>Expand JSON</button>
@@ -248,6 +248,6 @@
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
{/if}
{:else}
<div class="text-gray-500 text-sm">No result</div>
<div class="text-gray-500 text-sm">No result: {JSON.stringify(result)}</div>
{/if}
</div>

View File

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

View File

@@ -11,17 +11,21 @@
| undefined = undefined
</script>
<div class="inline-flex flex-row items-center">
<div class="inline-flex flex-row items-center truncated">
<span class="font-semibold">
{label}
</span>
<Required {required} class="!ml-0" />
<span class="text-sm italic ml-1 text-indigo-800">
({type ?? 'any'}{contentEncoding && contentEncoding != ''
? `, encoding: ${contentEncoding}`
: ''}{format && format != '' ? `, format: ${format}` : ''}{itemsType?.type
? ` of ${itemsType?.type}s`
: ''})</span
>
{#if format && format != ''}
<span class="text-sm italic ml-1 text-indigo-800">
({format})
</span>
{:else}
<span class="text-sm italic ml-1 text-indigo-800">
({type ?? 'any'}{contentEncoding && contentEncoding != ''
? `, encoding: ${contentEncoding}`
: ''})</span
>
{/if}
</div>

View File

@@ -1,22 +1,20 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
import { FlowService, ScheduleService, type Flow, type FlowModule } from '$lib/gen'
import { initHistory, redo, undo } from '$lib/history'
import { userStore, workspaceStore } from '$lib/stores'
import { encodeState, formatCron, loadHubScripts, sendUserToast } from '$lib/utils'
import { faCalendarAlt, faEye, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { faCalendarAlt, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Button, Drawer, DrawerContent } from './common'
import { Button, ButtonPopup, ButtonPopupItem, UndoRedo } from './common'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { OFFSET } from './CronInput.svelte'
import FlowGraphViewer from './FlowGraphViewer.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import FlowEditor from './flows/FlowEditor.svelte'
import { flowStateStore } from './flows/flowState'
import { flowStore } from './flows/flowStore'
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'
@@ -27,8 +25,8 @@
export let selectedId: string | undefined
export let initialArgs: Record<string, any> = {}
export let loading = false
let pathError = ''
export let flowStore: Writable<Flow>
export let flowStateStore: Writable<FlowState>
async function createSchedule(path: string) {
const { cron, args, enabled } = $scheduleStore
@@ -51,7 +49,10 @@
}
}
async function saveFlow(): Promise<void> {
let loadingSave = false
async function saveFlow(leave: boolean): Promise<void> {
loadingSave = true
const flow = cleanInputs($flowStore)
const { cron, args, enabled } = $scheduleStore
$dirtyStore = false
@@ -113,8 +114,12 @@
await createSchedule(flow.path)
}
}
sendUserToast(`Flow saved at ${$flowStore.path}`)
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
loadingSave = false
if (leave) {
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
} else if (initialPath !== $flowStore.path) {
goto(`/flows/edit/${$flowStore.path}?workspace_id=${$workspaceStore}`)
}
}
let timeout: NodeJS.Timeout | undefined = undefined
@@ -142,11 +147,15 @@
}, 500)
}
const selectedIdStore = writable<string>(selectedId)
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
const previewArgsStore = writable<Record<string, any>>(initialArgs)
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
const history = initHistory($flowStore)
const testStepStore = writable<Record<string, any>>({})
function select(selectedId: string) {
selectedIdStore.set(selectedId)
@@ -155,9 +164,13 @@
setContext<FlowEditorContext>('FlowEditorContext', {
selectedId: selectedIdStore,
schedule: scheduleStore,
select,
previewArgs: previewArgsStore,
scriptEditorDrawer
scriptEditorDrawer,
moving,
history,
flowStateStore,
flowStore,
testStepStore
})
async function loadSchedule() {
@@ -180,39 +193,82 @@
loadHubScripts()
let flowViewer: Drawer
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 />
<Drawer bind:this={flowViewer} size="75%">
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
<div class="overflow-hidden h-full w-full">
<FlowGraphViewer flow={$flowStore} />
</div>
</DrawerContent>
</Drawer>
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
>
<div class="flex flex-row">
<div class="flex flex-row gap-4 items-center">
<FlowImportExportMenu />
<Button
btnClasses="inline-flex"
startIcon={{ icon: faEye }}
variant="border"
color="light"
size="sm"
on:click={flowViewer.openDrawer}
>
Graph
</Button>
<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>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
{#if $scheduleStore.enabled}
<Button
@@ -259,12 +315,22 @@
<div class="flex flex-row space-x-2">
<FlowPreviewButtons />
<div class="center-center">
<Button
disabled={pathError != ''}
startIcon={{ icon: faSave }}
<ButtonPopup
loading={loadingSave}
size="sm"
on:click={saveFlow}>Save</Button
startIcon={{ icon: faSave }}
on:click={() => saveFlow(false)}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => saveFlow(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/flows/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
</div>
</div>
</div>

View File

@@ -79,17 +79,16 @@
</Drawer>
<div class="grid grid-cols-3 w-full">
<div
bind:clientHeight={topHeight}
class="{noSide
? 'col-span-3'
: 'sm:col-span-2 col-span-3'} w-full border border-gray-400 h-screen"
: 'sm:col-span-2 col-span-3'} w-full border border-gray-400 max-h-screen"
class:overflow-auto={overflowAuto}
>
<FlowGraph
minHeight={topHeight}
minHeight={400}
modules={flow?.value?.modules}
failureModule={flow?.value?.failure_module}
on:click={(e) => (stepDetail = e.detail)}
on:select={(e) => (stepDetail = e.detail)}
/>
</div>
{#if !noSide}
@@ -97,7 +96,9 @@
class="w-full border-r border-b border-t border-gray-400 min-h-[150px] p-2 overflow-auto hidden sm:block"
>
{#if stepDetail == undefined}
<span class="font-black text-lg w-full my-4">
<SchemaViewer schema={flow?.schema} />
<span class="font-black text-lg w-full my-4 mt-14">
<span>Click on a step to see its details</span>
</span>
{:else if stepDetail == 'Input'}

View File

@@ -12,9 +12,9 @@
<div
class:border={!noBorder}
class="grid {!col ? 'grid-cols-2' : 'grid-rows-2'} shadow border-gray-400 h-full"
class="grid {!col ? 'grid-cols-2' : 'grid-rows-2'} shadow border-gray-400 h-full max-h-screen"
>
<div class="bg-white max-h-80 h-full p-1 overflow-auto relative">
<div class="bg-white {col ? '' : 'max-h-80'} h-full p-1 overflow-auto relative">
<span class="text-gray-500">Result</span>
{#if result}
<DisplayResult {result} />
@@ -24,7 +24,7 @@
<div class="text-gray-400">No result (result is undefined)</div>
{/if}
</div>
<div class="overflow-auto max-h-80 h-full relative">
<div class="overflow-auto {col ? '' : 'max-h-80'} h-full relative">
<LogViewer content={logs ?? ''} isLoading={false} />
</div>
</div>

View File

@@ -5,13 +5,12 @@
import { Button, Kbd } from './common'
import { createEventDispatcher, getContext } from 'svelte'
import Icon from 'svelte-awesome'
import { dfs, flowStore } from './flows/flowStore'
import { dfs } from './flows/flowStore'
import type { FlowEditorContext } from './flows/types'
import { runFlowPreview } from './flows/utils'
import SchemaForm from './SchemaForm.svelte'
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
import FlowProgressBar from './flows/FlowProgressBar.svelte'
import { flowStateStore } from './flows/flowState'
import CapturePayload from './flows/content/CapturePayload.svelte'
import { Loader2 } from 'lucide-svelte'
@@ -21,11 +20,11 @@
export let jobId: string | undefined = undefined
export let job: Job | undefined = undefined
let isValid: boolean = true
let isRunning: boolean = false
let jobProgressReset: () => void
const { selectedId, previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
function sliceModules(modules: FlowModule[], upTo: number, idOrders: string[]): FlowModule[] {
@@ -105,7 +104,6 @@
{#if isRunning}
<Button
disabled={!isValid}
color="red"
on:click={async () => {
isRunning = false
@@ -132,7 +130,6 @@
color="blue"
size="sm"
btnClasses="w-full max-w-lg"
disabled={!isValid}
on:click={() => runPreview($previewArgs)}
>
Test flow <Kbd class="ml-2">Ctrl+Enter</Kbd>
@@ -156,7 +153,6 @@
compact
class="py-4 max-w-3xl"
schema={$flowStore.schema}
bind:isValid
bind:args={$previewArgs}
/>
</div>

View File

@@ -452,13 +452,17 @@
<FlowGraph
success={isSuccess(job?.['success'])}
flowModuleStates={localFlowModuleStates}
on:click={(e) => {
if (e.detail.id) {
on:select={(e) => {
if (typeof e.detail == 'string') {
if (e.detail == 'Input') {
selectedNode = 'start'
} else if (e.detail == 'Result') {
selectedNode = 'end'
} else {
selectedNode = e.detail
}
} else {
selectedNode = e.detail.id
} else if (e.detail == 'Result') {
selectedNode = 'end'
} else if (e.detail == 'Input') {
selectedNode = 'start'
}
}}
modules={job.raw_flow?.modules ?? []}
@@ -505,6 +509,7 @@
<div class="px-1 border-b border-black">
<JobArgs args={node.args} />
</div>
<FlowJobResult
loading={job['running'] == true}
noBorder

View File

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

View File

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

View File

@@ -82,16 +82,10 @@
}
function connectProperty(rawValue: string) {
if (isStaticTemplate(inputCat)) {
arg.value = `\$\{${rawValue}}`
setPropertyType(arg.value)
monacoTemplate?.setCode(arg.value)
} else {
arg.expr = getDefaultExpr(undefined, previousModuleId, rawValue)
arg.type = 'javascript'
propertyType = 'javascript'
monaco?.setCode(arg.expr)
}
arg.expr = getDefaultExpr(undefined, previousModuleId, rawValue)
arg.type = 'javascript'
propertyType = 'javascript'
monaco?.setCode(arg.expr)
}
function onFocus() {
@@ -154,7 +148,7 @@
{/if}
</div>
{#if !noDynamicToggle}
<div class="flex flex-row gap-x-4 gap-y-1 flex-wrap">
<div class="flex flex-row gap-x-4 gap-y-1 flex-wrap z-10">
<ToggleButtonGroup
bind:selected={propertyType}
on:selected={(e) => {
@@ -196,10 +190,10 @@
>
{#if isStaticTemplate(inputCat)}
<ToggleButton light position="left" value="static" size="xs">
{'${} '}Template &nbsp; <Tooltip
>Write javascript expressions between "{openBracket}" and "{closeBracket}". You may
refer to contextual objects like 'flow_input', or 'result' or functions like
'resource' and 'variable'
{'${} '}&nbsp;
<Tooltip
>Write text or surround javascript with "{openBracket}" and "{closeBracket}". Use
`result` to connect to another node's output.
</Tooltip></ToggleButton
>
{:else}
@@ -212,9 +206,11 @@
value="javascript"
startIcon={{ icon: faCode }}
size="xs"
>&nbsp;<Tooltip
>Write javascript expressions directly, using 'flow_input' or 'result'. You can use
multiline javascript.
</Tooltip></ToggleButton
>
Dynamic (JS)
</ToggleButton>
</ToggleButtonGroup>
<Button
variant="contained"
@@ -305,6 +301,4 @@
Not recognized input type {argName}
{/if}
</div>
{:else}
<p class="text-sm text-gray-700">Argument at {argName} is undefined</p>
{/if}

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { VariableService, type InputTransform } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { Button } from './common'
import InputTransformForm from './InputTransformForm.svelte'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
export let schema: Schema
export let args: Record<string, InputTransform | any> = {}
export let isValid: boolean = true
export let extraLib: string = 'missing extraLib'
export let previousModuleId: string | undefined = undefined
export let filter: string[] | undefined = undefined
export let noDynamicToggle = false
let clazz: string = ''
export { clazz as class }
let inputCheck: { [id: string]: boolean } = {}
$: isValid = allTrue(inputCheck) ?? false
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
function removeExtraKey() {
const nargs = {}
Object.keys(args ?? {}).forEach((key) => {
if (keys.includes(key)) {
nargs[key] = args[key]
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let keys: string[] = []
$: {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
removeExtraKey()
}
}
</script>
<div class="w-full {clazz}">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
<div class="z-10">
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
bind:schema
bind:argName
bind:inputCheck={inputCheck[argName]}
bind:extraLib
{variableEditor}
{itemPicker}
bind:pickForField
{noDynamicToggle}
/>
</div>
{/if}
{/each}
{:else}
<div class="text-gray-500 text-sm">No inputs</div>
{/if}
</div>
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
args[pickForField].value = '$var:' + 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

@@ -42,7 +42,7 @@
<button on:click={logViewer.openDrawer}>Expand</button>
<div class="py-2 pr-2 text-xs flex gap-2 items-center">
Auto scroll
<input type="checkbox" bind:checked={scroll} />
<input class="windmillapp" type="checkbox" bind:checked={scroll} />
</div>
</div>
</div>

View File

@@ -8,14 +8,17 @@
import LogViewer from './LogViewer.svelte'
import DisplayResult from './DisplayResult.svelte'
import Button from './common/button/Button.svelte'
import { flowStateStore, testStepStore } from './flows/flowState'
import { flowStore } from './flows/flowStore'
import { workspaceStore } from '$lib/stores'
import { Loader2 } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
export let mod: FlowModule
export let schema: Schema
const { flowStore, flowStateStore, testStepStore } =
getContext<FlowEditorContext>('FlowEditorContext')
// Test
let testJobLoader: TestJobLoader
let testIsLoading = false
@@ -86,6 +89,7 @@
detailed={false}
topButton
bind:args={stepArgs}
isFlow={false}
/>
{#if testIsLoading}
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full mt-4" color="red" size="sm">

View File

@@ -167,7 +167,7 @@
{#if showOptions}
<ul
class="options"
transition:fly={{ duration: 200, y: 5 }}
transition:fly|local={{ duration: 200, y: 5 }}
on:mousedown|preventDefault={handleOptionMousedown}
>
{#each filtered as option}

View File

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

View File

@@ -23,6 +23,7 @@
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
@@ -277,15 +278,19 @@
folderCreated = undefined
}}
>
<div class="flex flex-row">
<input class="mr-2" placeholder="New folder name" bind:value={newFolderName} />
<Button size="md" startIcon={{ icon: faPlus }} disabled={!newFolderName} on:click={addFolder}>
New&nbsp;folder
</Button>
</div>
{#if folderCreated}
<div class="mt-8" />
{#if !folderCreated}
<div class="flex flex-row">
<input class="mr-2" placeholder="New folder name" bind:value={newFolderName} />
<Button
size="md"
startIcon={{ icon: faPlus }}
disabled={!newFolderName}
on:click={addFolder}
>
New&nbsp;folder
</Button>
</div>
{:else}
<FolderEditor name={folderCreated} />
{/if}
</DrawerContent>
@@ -302,7 +307,7 @@
{#if meta != undefined}
<div class="flex gap-4 shrink">
<label class="block">
<span class="text-gray-700 text-sm whitespace-nowrap">&nbsp;</span>
<span class="text-gray-700 text-sm whitespace-nowrap">Owner</span>
<ToggleButtonGroup
class="mt-0.5"
@@ -405,8 +410,26 @@
{/if}
</div>
<div class="flex-row flex justify-between">
<div><span class="font-mono text-sm">{path}</span></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="text-red-600 text-2xs">{error}</div>
</div>
</div>

View File

@@ -49,22 +49,11 @@
</script>
{#if notClickable}
<span
use:popperRef
on:mouseenter={open}
on:mouseleave={close}
class={$$props.class}
>
<span use:popperRef on:mouseenter={open} on:mouseleave={close} class={$$props.class}>
<slot />
</span>
{:else}
<button
use:popperRef
on:mouseenter={open}
on:mouseleave={close}
on:click
class={$$props.class}
>
<button use:popperRef on:mouseenter={open} on:mouseleave={close} on:click class={$$props.class}>
<slot />
</button>
{/if}

View File

@@ -11,17 +11,38 @@
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 v = value
resources = await ResourceService.listResource({ workspace: $workspaceStore!, resourceType })
value = v
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
}
$: {
if ($workspaceStore) {
loadResources(resourceType)
@@ -29,10 +50,6 @@
}
$: dispatch('change', value)
$: collection = resources.map((x) => ({
value: x.path,
label: x.path
}))
let appConnect: AppConnect
let resourceEditor: ResourceEditor
</script>
@@ -41,6 +58,7 @@
on:refresh={async (e) => {
await loadResources(resourceType)
value = e.detail
valueSelect = { value: e.detail, label: e.detail }
}}
newPageOAuth
bind:this={appConnect}
@@ -50,21 +68,27 @@
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
listAutoWidth={false}
value={collection.find((x) => x.value == value)}
bind:justValue={value}
value={valueSelect}
on:change={(e) => {
value = e.detail.value
valueSelect = e.detail
}}
on:clear={() => {
value = undefined
valueSelect = undefined
}}
items={collection}
class="text-clip grow min-w-0"
placeholder="{resourceType} resource"
placeholder="{resourceType ?? 'any'} 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.created_at as any,
x: job.started_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.created_at as any,
x: job.started_at as any,
y: job.duration_ms,
id: job.id,
path: job.script_path
@@ -71,7 +71,10 @@
enabled: true,
modifierKey: 'ctrl' as 'ctrl',
onPanComplete: ({ chart }) => {
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
dispatch('zoom', {
min: addSeconds(new Date(chart.scales.x.min), -1),
max: addSeconds(new Date(chart.scales.x.max), 1)
})
}
},
zoom: {
@@ -80,7 +83,10 @@
},
mode: 'x' as 'x',
onZoom: ({ chart }) => {
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
dispatch('zoom', {
min: addSeconds(new Date(chart.scales.x.min), -1),
max: addSeconds(new Date(chart.scales.x.max), 1)
})
}
}
}
@@ -88,6 +94,22 @@
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
@@ -115,7 +137,8 @@
display: false
},
type: 'time',
min: jobs?.[jobs?.length - 1]?.created_at ?? new Date().toString()
min: minTime,
max: maxTime
},
y: {
grid: {

View File

@@ -49,6 +49,7 @@
export let loading = false
export let noVariablePicker = false
export let viewCliRun = false
export let isFlow: boolean
export let args: Record<string, any> = decodeArgs($page.url.searchParams.get('args') ?? undefined)
@@ -64,9 +65,9 @@
let scheduledForStr: string | undefined
let invisible_to_owner: false
$: cliCommand = `wmill ${runnable?.kind} run ${runnable?.path} ${Object.entries(args)
.map(([k, v]) => `-i ${k}=${JSON.stringify(v)}`)
.join(' ')}`
$: cliCommand = `wmill ${isFlow ? 'flow' : 'script'} run ${runnable?.path} -d '${JSON.stringify(
args
)}'`
</script>
<div class="max-w-6xl">
@@ -145,7 +146,7 @@
</Button>
</div>
{#if viewOptions}
<div transition:slide class="mt-6">
<div transition:slide|local class="mt-6">
<div class="border rounded-md p-3 pt-4">
<div class="flex flex-row items-end">
<div class="w-max md:w-2/3 mt-2 mb-1">
@@ -213,14 +214,14 @@
<div class="my-10" />
<Button
color="light"
size="sm"
size="xs"
endIcon={{ icon: viewCliOptions ? faChevronUp : faChevronDown }}
on:click={() => (viewCliOptions = !viewCliOptions)}
>
Run it from the CLI
</Button>
{#if viewCliOptions}
<div transition:slide class="mt-2 px-4 pt-2">
<div transition:slide|local class="mt-2 px-4 pt-2">
<InlineCodeCopy content={cliCommand} />
<CliHelpBox />
</div>

View File

@@ -4,14 +4,11 @@
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { slide } from 'svelte/transition'
import ArgInput from './ArgInput.svelte'
import { Button } from './common'
import InputTransformForm from './InputTransformForm.svelte'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
export let inputTransform = false
export let schema: Schema
export let args: Record<string, InputTransform | any> = {}
export let disabledArgs: string[] = []
@@ -19,16 +16,12 @@
export let editableSchema = false
export let isValid: boolean = true
export let extraLib: string = 'missing extraLib'
export let autofocus = false
export let previousModuleId: string | undefined = undefined
export let shouldHideNoInputs: boolean = false
export let compact = false
export let password: string | undefined = undefined
export let noVariablePicker = false
export let filter: string[] | undefined = undefined
export let noDynamicToggle = false
export let flexWrap = false
export let noDelete = false
@@ -43,17 +36,19 @@
}
function removeExtraKey() {
const nargs = {}
Object.keys(args ?? {}).forEach((key) => {
if (!keys.includes(key)) {
delete args[key]
delete inputCheck[key]
if (keys.includes(key)) {
nargs[key] = args[key]
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let keys: string[] = []
$: {
let lkeys = Object.keys(schema?.properties ?? {})
@@ -69,48 +64,60 @@
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 gap-y-2' : ''}">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if !filter || filter.includes(argName)}
<div transition:slide|local>
{#if inputTransform}
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
bind:schema
bind:argName
bind:inputCheck={inputCheck[argName]}
bind:extraLib
{variableEditor}
{itemPicker}
bind:pickForField
{noDynamicToggle}
/>
{:else if typeof args == 'object'}
<ArgInput
autofocus={i == 0 && autofocus}
label={argName}
bind:description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
bind:pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={schema.properties[argName].default}
bind:enum_={schema.properties[argName].enum}
bind:format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
bind:itemsType={schema.properties[argName].items}
disabled={disabledArgs.includes(argName) || disabled}
{editableSchema}
{compact}
password={argName == password}
{variableEditor}
{itemPicker}
bind:pickForField
bind:extra={schema.properties[argName]}
/>
{:else}
Expected argument to be an object, got {JSON.stringify(args)} instead
{#if Object.keys(schema.properties ?? {}).includes(argName)}
<div>
{#if typeof args == 'object' && schema?.properties[argName]}
{#if editableSchema}
<ArgInput
autofocus={i == 0 && autofocus}
label={argName}
bind:description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
bind:pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={schema.properties[argName].default}
bind:enum_={schema.properties[argName].enum}
bind:format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
bind:itemsType={schema.properties[argName].items}
disabled={disabledArgs.includes(argName) || disabled}
{editableSchema}
{compact}
password={argName == password}
{variableEditor}
{itemPicker}
bind:pickForField
bind:extra={schema.properties[argName]}
/>
{:else}
<ArgInput
autofocus={i == 0 && autofocus}
label={argName}
description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={schema.properties[argName].default}
enum_={schema.properties[argName].enum}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
itemsType={schema.properties[argName].items}
disabled={disabledArgs.includes(argName) || disabled}
{editableSchema}
{compact}
password={argName == password}
{variableEditor}
{itemPicker}
bind:pickForField
extra={schema.properties[argName]}
/>
{/if}
{/if}
</div>
{/if}
@@ -125,11 +132,7 @@
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
if (inputTransform) {
args[pickForField].value = '$var:' + path
} else {
args[pickForField] = '$var:' + path
}
args[pickForField] = '$var:' + path
}
}}
itemName="Variable"

View File

@@ -1,24 +1,23 @@
<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, isInitialCode } from '$lib/script_helpers'
import { initialCode } 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, Kbd } from './common'
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
import { 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 = ''
@@ -32,14 +31,34 @@
['Go', Script.language.GO],
['Bash', Script.language.BASH]
]
let viewScriptKind = script.kind !== Script.kind.SCRIPT
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 pathError = ''
let summaryC: HTMLInputElement | undefined = undefined
let pathC: Path | undefined = undefined
let loadingSave = false
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
$: step = Number($page.url.searchParams.get('step')) || 1
if (script.content == '') {
@@ -54,7 +73,8 @@
script.content = initialCode(language, kind, template)
}
async function editScript(): Promise<void> {
async function editScript(leave: boolean): Promise<void> {
loadingSave = true
try {
$dirtyStore = false
localStorage.removeItem(script.path)
@@ -83,12 +103,17 @@
kind: script.kind
}
})
sendUserToast(`New script created at hash ${newHash}`)
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
if (leave) {
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
} else {
await goto(`/scripts/edit/${newHash}?step=2`)
script.hash = newHash
}
} catch (error) {
sendUserToast(`Impossible to save the script: ${error.body}`, true)
sendUserToast(`Impossible to save the script: ${error.body || error.message}`, true)
}
loadingSave = false
}
async function changeStep(step: number) {
@@ -114,10 +139,7 @@
}
</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">
@@ -183,14 +205,24 @@
>
Next {#if step == 1}<Kbd>Enter</Kbd>{/if}
</Button>
<Button
<ButtonPopup
loading={loadingSave}
size="sm"
variant={step == 1 ? 'border' : 'contained'}
disabled={step === 1 && pathError !== ''}
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
startIcon={{ icon: faSave }}
on:click={editScript}>Save</Button
on:click={() => editScript(false)}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => editScript(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/scripts/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
</div>
</div>
</div>
@@ -198,132 +230,111 @@
<!-- metadata -->
{#if step === 1}
<CenteredPage>
<h2 class="border-b pb-1 mt-8 mb-2">Path</h2>
<Path
bind:this={pathC}
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="script"
kind="script"
/>
<h2 class="border-b pb-1 mt-12 mb-4">Summary</h2>
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
/>
<h2 class="border-b pb-1 mt-12 mb-6">Language</h2>
{#if lockedLanguage}
<div class="text-sm text-gray-600 italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<SettingSection title="Path" element="h2">
<Path
bind:this={pathC}
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="script"
kind="script"
/>
</SettingSection>
<SettingSection title="Summary" element="h2">
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
/>
</SettingSection>
<SettingSection title="Language" element="h2">
{#if lockedLanguage}
<div class="text-sm text-gray-600 italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
}}
disabled={lockedLanguage}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2">{label}</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
}}
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={lockedLanguage}
>
<LanguageIcon {lang} /><span class="ml-2 py-4">{label}</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={lockedLanguage}
on:click={() => {
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-4">PostgreSQL</span>
</Button>
<!-- <Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-4">MySQL</span>
</Button> -->
</div>
<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'
initContent(script.language, e.detail, template)
}
on:click={() => {
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
}}
bind:value={script.kind}
/>
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
</Button>
<!-- <Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
</Button> -->
</div>
{/if}
<!-- <div class="ml-3">
<Toggle
bind:checked={script.is_template}
options={{ right: 'Save as a workspace template' }}
/>
</div> -->
</SettingSection>
<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={() => {
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}
</div>
</SettingSection>
</CenteredPage>
{:else if step === 2}
<ScriptEditor
@@ -343,3 +354,5 @@
{:else}
Script Builder not available to operators
{/if}
<svelte:window on:keydown={onKeyDown} />

View File

@@ -0,0 +1,47 @@
<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-start items-center"
on:click={() => (showContent = !showContent)}
>
<span class="rounded-full hover:bg-gray-100 focus:bg-gray-100 p-1 mr-1">
<ChevronDown size={22} class="rotate-0 duration-300 {showContent ? '!-rotate-180' : ''}" />
</span>
<svelte:element this={element}>
{title}
{#if tooltip}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
{/if}
</svelte:element>
</button>
{:else}
<svelte:element this={element}>
{title}
{#if tooltip}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
{/if}
</svelte:element>
{/if}
</div>
{#if showContent}
<div transition:slide={{ duration: 300 }}>
<slot />
</div>
{/if}

View File

@@ -209,7 +209,7 @@
})
</script>
<div bind:this={divEl} class="{$$props.class} editor" bind:clientWidth={width} />
<div bind:this={divEl} class="{$$props.class ?? ''} editor" bind:clientWidth={width} />
<style>
.editor {

View File

@@ -2,15 +2,20 @@
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}
<div class="my-4 px-2" transition:slide><slot /></div>
<div class="my-4 px-2" transition:slide|local><slot /></div>
{/if}

View File

@@ -596,7 +596,7 @@
<div
bind:this={divEl}
style="height: 18px;"
class="{$$props.class} template rounded-lg min-h-4 mx-0.5"
class="{$$props.class ?? ''} template rounded-lg min-h-4 mx-0.5 overflow-clip"
bind:clientWidth={width}
/>

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

@@ -13,10 +13,12 @@
<Popover notClickable {placement} class={wrapperClass}>
<Icon
class="{light
? 'text-gray-300'
? 'text-gray-400'
: ' text-gray-500'} font-thin inline-block align-middle w-4 {$$props.class}"
data={faInfoCircle}
{scale}
/>
<span slot="text"> <slot /> </span>
<svelte:fragment slot="text">
<slot />
</svelte:fragment>
</Popover>

View File

@@ -39,6 +39,7 @@
export function closeDrawer() {
drawer?.closeDrawer()
const index = $page.url.href.lastIndexOf('#')
if (index === -1) return
const hashRemoved = $page.url.href.slice(0, index)
goto(hashRemoved)
}

View File

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

View File

@@ -3,13 +3,14 @@
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { loadIcon } from '../icon'
import { twMerge } from 'tailwind-merge'
import { goto } from '$app/navigation'
export let id: string
export let componentInput: AppInput | undefined
@@ -21,10 +22,12 @@
export let noWFull = false
export let preclickAction: (() => Promise<void>) | undefined = undefined
export let customCss: ComponentCustomCSS<'button'> | undefined = undefined
export let render: boolean
export let initializing: boolean | undefined = true
export const staticOutputs: string[] = ['loading', 'result']
const { runnableComponents, worldStore, app } = getContext<AppEditorContext>('AppEditorContext')
const { worldStore, app } = getContext<AppViewerContext>('AppViewerContext')
let labelValue: string
let color: ButtonType.Color
@@ -32,7 +35,8 @@
let runnableComponent: RunnableComponent
let disabled: boolean | undefined = undefined
let fillContainer: boolean | undefined = undefined
let goto: string | undefined = undefined
let gotoUrl: string | undefined = undefined
let gotoNewTab: boolean | undefined = undefined
let isLoading: boolean = false
let ownClick: boolean = false
@@ -71,6 +75,7 @@
}
$: outputs?.loading.subscribe({
id: 'loading-' + id,
next: (value) => {
isLoading = value
if (ownClick && !value) {
@@ -84,10 +89,38 @@
$: errorsMessage = Object.values(errors)
.filter((x) => x != '')
.join('\n')
async function handleClick(event: CustomEvent) {
event?.stopPropagation()
event?.preventDefault()
if (preclickAction) {
await preclickAction()
}
ownClick = true
if (!runnableComponent) {
if (gotoUrl) {
if (gotoNewTab) {
window.open(gotoUrl, '_blank')
} else {
goto(gotoUrl)
}
}
} else {
await runnableComponent?.runComponent()
}
}
</script>
<InputValue {id} input={configuration.label} bind:value={labelValue} />
<InputValue {id} input={configuration.goto} bind:value={goto} />
<InputValue
on:done={() => initializing && (initializing = false)}
{id}
input={configuration.label}
bind:value={labelValue}
/>
<InputValue {id} input={configuration.goto} bind:value={gotoUrl} />
<InputValue {id} input={configuration.color} bind:value={color} />
<InputValue {id} input={configuration.size} bind:value={size} />
<InputValue {id} input={configuration.beforeIcon} bind:value={beforeIcon} />
@@ -97,19 +130,24 @@
<InputValue
{id}
input={configuration.disabled}
extraContext={extraQueryParams}
bind:value={disabled}
bind:error={errors.disabled}
/>
<InputValue {id} input={configuration.fillContainer} bind:value={fillContainer} />
<InputValue {id} input={configuration.gotoNewTab} bind:value={gotoNewTab} />
<RunnableWrapper
flexWrap
{recomputeIds}
bind:runnableComponent
bind:componentInput
{componentInput}
{id}
{extraQueryParams}
autoRefresh={false}
{goto}
goto={gotoUrl}
{gotoNewTab}
{render}
>
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
{#if errorsMessage}
@@ -127,31 +165,17 @@
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
on:click={async (e) => {
if (preclickAction) {
await preclickAction()
}
e?.stopPropagation()
e?.preventDefault()
ownClick = true
await runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
}}
on:click={handleClick}
{size}
{color}
{loading}
>
<span class="truncate inline-flex gap-2 items-center">
{#if beforeIconComponent}
{#if beforeIcon && beforeIconComponent}
<svelte:component this={beforeIconComponent} size={14} />
{/if}
<div>{labelValue}</div>
{#if afterIconComponent}
{#if afterIcon && 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 { AppEditorContext, ComponentCustomCSS } from '../../types'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
@@ -19,10 +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, runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
const { app, worldStore, stateId } = getContext<AppViewerContext>('AppViewerContext')
let labelValue: string = 'Default label'
let color: ButtonType.Color
@@ -33,7 +34,8 @@
let isLoading: boolean = false
$: noInputs =
componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0
$stateId != undefined &&
(componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0)
$: outputs = $worldStore?.outputsById[id] as {
result: Output<Array<any>>
@@ -45,6 +47,7 @@
}
$: outputs?.loading.subscribe({
id: 'loading-' + id,
next: (value) => {
isLoading = value
}
@@ -59,9 +62,10 @@
<InputValue {id} input={configuration.size} bind:value={size} />
<RunnableWrapper
defaultUserInput
{recomputeIds}
{render}
bind:runnableComponent
bind:componentInput
{componentInput}
{id}
{goto}
{extraQueryParams}
@@ -100,12 +104,6 @@
}}
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 { AppEditorContext, ComponentCustomCSS } from '../../types'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
@@ -22,10 +22,11 @@
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<AppEditorContext>('AppEditorContext')
const { app, runnableComponents, worldStore } = getContext<AppViewerContext>('AppViewerContext')
let labelValue: string = 'Default label'
let color: ButtonType.Color
@@ -56,6 +57,7 @@
}
$: outputs?.loading.subscribe({
id: 'loading-' + id,
next: (value) => {
isLoading = value
if (ownClick && !value) {
@@ -93,9 +95,10 @@
}}
>
<RunnableWrapper
defaultUserInput
{recomputeIds}
{render}
bind:runnableComponent
bind:componentInput
{componentInput}
{id}
{extraQueryParams}
autoRefresh={false}
@@ -127,12 +130,6 @@
on:click={async () => {
await runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
open = false
}}
size="xs"

View File

@@ -17,16 +17,17 @@
import InputValue from '../helpers/InputValue.svelte'
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import type { AppViewerContext, 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<AppEditorContext>('AppEditorContext')
const { app } = getContext<AppViewerContext>('AppViewerContext')
ChartJS.register(
Title,
@@ -89,7 +90,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.line} bind:value={lineChart} />
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper {render} 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

@@ -3,34 +3,43 @@
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../inputType'
import { IS_APP_PUBLIC_CONTEXT_KEY, type AppEditorContext, type ComponentCustomCSS } from '../../types'
import {
IS_APP_PUBLIC_CONTEXT_KEY,
type AppViewerContext,
type ComponentCustomCSS
} from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
export let id: string
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<AppEditorContext>('AppEditorContext')
const { app } = getContext<AppViewerContext>('AppViewerContext')
let result: any = undefined
export const staticOutputs: string[] = ['result', 'loading']
</script>
<RunnableWrapper flexWrap bind: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',
$app.css?.['displaycomponent']?.['header']?.class,
customCss?.header?.class
)}>
<RunnableWrapper {render} 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',
$app.css?.['displaycomponent']?.['header']?.class,
customCss?.header?.class
)}
>
Results
</div>
<div class={twMerge(
'p-2',
$app.css?.['displaycomponent']?.['container']?.class,
customCss?.container?.class
)}>
<div
class={twMerge(
'p-2',
$app.css?.['displaycomponent']?.['container']?.class,
customCss?.container?.class
)}
>
<DisplayResult {result} {requireHtmlApproval} />
</div>
</RunnableWrapper>

View File

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

View File

@@ -1,6 +1,9 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import { AlignWrapper, InputValue, RunnableWrapper } from '../helpers'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { AlignWrapper, InputValue } from '../helpers'
import { loadIcon } from '../icon'
export let id: string
@@ -8,6 +11,10 @@
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
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')
let icon: string | undefined = undefined
let size: number
@@ -15,13 +22,13 @@
let strokeWidth: number
let iconComponent: any
$: icon && handleIcon()
$: handleIcon(icon)
async function handleIcon() {
if (icon) {
iconComponent = await loadIcon(icon)
}
async function handleIcon(i?: string) {
iconComponent = i ? await loadIcon(i) : undefined
}
$: css = concatCustomCss($app.css?.iconcomponent, customCss)
</script>
<InputValue {id} input={configuration.icon} bind:value={icon} />
@@ -29,13 +36,21 @@
<InputValue {id} input={configuration.color} bind:value={color} />
<InputValue {id} input={configuration.strokeWidth} bind:value={strokeWidth} />
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if iconComponent}
<AlignWrapper
{render}
{horizontalAlignment}
{verticalAlignment}
class={css?.container?.class ?? ''}
style={css?.container?.style ?? ''}
>
{#if icon && iconComponent}
<svelte:component
this={iconComponent}
size={size || 24}
color={color || 'currentColor'}
strokeWidth={strokeWidth || 2}
class={css?.icon?.class ?? ''}
style={css?.icon?.style ?? ''}
/>
{/if}
</AlignWrapper>

View File

@@ -1,6 +1,10 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { staticValues } from '../../editor/componentsPanel/componentStaticValues'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
type FitOption = (typeof staticValues)['objectFitOptions'][number]
@@ -8,7 +12,10 @@
export let id: string
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 fit: Record<FitOption, string> = {
cover: 'object-cover',
contain: 'object-contain',
@@ -18,18 +25,20 @@
let source: string | undefined = undefined
let imageFit: FitOption | undefined = undefined
let altText: string | undefined = undefined
let customStyles: string | undefined = undefined
$: css = concatCustomCss($app.css?.imagecomponent, customCss)
</script>
<InputValue {id} input={configuration.source} bind:value={source} />
<InputValue {id} input={configuration.imageFit} bind:value={imageFit} />
<InputValue {id} input={configuration.altText} bind:value={altText} />
<InputValue {id} input={configuration.customStyles} bind:value={customStyles} />
<img
on:pointerdown|preventDefault
src={source}
alt={altText}
style={customStyles}
class="w-full h-full {fit[imageFit || 'cover']}"
/>
{#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}

View File

@@ -0,0 +1,225 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import { concatCustomCss } from '../../utils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { InputValue } from '../helpers'
import { twMerge } from 'tailwind-merge'
import { Map, View, Feature } from 'ol'
import { useGeographic } from 'ol/proj'
import { OSM, Vector as VectorSource } from 'ol/source'
import { Vector as VectorLayer, Tile as TileLayer } from 'ol/layer'
import { Point } from 'ol/geom'
import { defaults as defaultControls } from 'ol/control'
import { findGridItem } from '../../editor/appUtils'
import type { Output } from '../../rx'
interface Marker {
lon: number
lat: number
title?: string
radius?: number
color?: string
strokeWidth?: number
strokeColor?: string
}
const LAYER_NAME = {
MARKER: 'Marker'
} as const
export let id: string
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')
$: outputs = $worldStore?.outputsById[id] as {
mapRegion: Output<{
topLeft: { lat: number; lon: number }
bottomRight: { lat: number; lon: number }
}>
}
let map: Map
let mapElement: HTMLDivElement
let longitude: number | undefined = undefined
let latitude: number | undefined = undefined
let zoom: number | undefined = undefined
// If string, it's a JSON file read as text
let markers: Marker[] | string | undefined = undefined
$: if (map && longitude && latitude) {
map.getView().setCenter([longitude, latitude])
}
$: if (map && zoom) {
map.getView().setZoom(zoom)
}
$: if (map && markers) {
updateMarkers()
}
function selectComponent() {
if (!$connectingInput.opened) {
$selectedComponent = id
$focusedGrid = undefined
}
}
function getLayersByName(name: keyof typeof LAYER_NAME) {
return map
.getLayers()
.getArray()
.filter((l) => l.getProperties().name === LAYER_NAME[name])
}
function getMarkerArray(): Marker[] | undefined {
let array: Marker[] | undefined = undefined
if (typeof markers === 'string') {
try {
array = JSON.parse(markers)
} catch (e) {
return undefined
}
} else {
array = markers
}
return array?.filter((m) => !isNaN(+m.lat) && !isNaN(+m.lon))
}
function createMarkerLayers() {
const markerArray = getMarkerArray()
return markerArray?.map((m) => {
return new VectorLayer({
properties: {
name: LAYER_NAME.MARKER
},
source: new VectorSource({
features: [
new Feature({
geometry: new Point([+m.lon, +m.lat]),
name: m.title
})
]
}),
style: {
'circle-radius': m.radius ?? 7,
'circle-fill-color': m.color ?? '#dc2626',
'circle-stroke-width': m.strokeWidth ?? 3,
'circle-stroke-color': m.strokeColor ?? '#fca5a5'
}
})
})
}
function updateMarkers() {
const layers = getLayersByName('MARKER')
if (layers?.length) {
layers.forEach((l) => map.removeLayer(l))
}
createMarkerLayers()?.forEach((l) => map.addLayer(l))
}
onMount(() => {
useGeographic()
map = new Map({
target: mapElement,
layers: [
new TileLayer({
source: new OSM()
}),
...(createMarkerLayers() || [])
],
view: new View({
center: [longitude ?? 0, latitude ?? 0],
zoom: zoom ?? 2
}),
controls: defaultControls({
attribution: false
})
})
updateRegionOutput()
})
$: css = concatCustomCss($app.css?.mapcomponent, customCss)
function updateRegionOutput() {
if (map) {
let extent = map.getView().calculateExtent(map.getSize())
const [left, bottom, right, top] = extent
if (outputs?.mapRegion) {
outputs.mapRegion.set({
topLeft: { lat: top, lon: left },
bottomRight: { lat: bottom, lon: right }
})
}
}
}
function handleSyncRegion() {
const gridItem = findGridItem($app, id)
if (!map || !gridItem) {
return
}
const z = map.getView().getZoom()
updateRegionOutput()
if (z) {
gridItem.data.configuration.zoom.value = z
}
const center = map.getView().getCenter()
if (!center) {
return
}
if (gridItem) {
gridItem.data.configuration.longitude.value = center[0]
gridItem.data.configuration.latitude.value = center[1]
}
}
</script>
<InputValue {id} input={configuration.longitude} bind:value={longitude} />
<InputValue {id} input={configuration.latitude} bind:value={latitude} />
<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 ?? ''}
/>
{#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}
<style global lang="postcss">
.ol-overlaycontainer-stopevent {
@apply flex flex-col justify-start items-end;
}
.ol-control button {
@apply w-7 h-7 center-center bg-white border border-gray-300 text-gray-700
rounded mt-1 mr-1 shadow duration-200 hover:bg-gray-100 focus:bg-gray-100
hover:border-gray-500 focus:border-gray-500;
}
</style>

View File

@@ -0,0 +1,321 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
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 { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
import { throttle } from '../../../../utils'
import { Button } from '../../../common'
import { Download, Loader2, MoveHorizontal, ZoomIn, ZoomOut } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { findGridItem } from '../../editor/appUtils'
export let id: string
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')
let source: string | ArrayBuffer | undefined = undefined
let wrapper: HTMLDivElement | undefined = undefined
let error: string | undefined = undefined
let doc: PDFDocumentProxy | undefined = undefined
let pages: PDFPageProxy[] = []
let zoom: number | undefined = undefined
let controlsWidth: number | undefined = undefined
let controlsHeight: number | undefined = undefined
let pageNumber = 1
$: if (source == '') {
resetDoc()
error = 'Set the "Source" attribute of the PDF component'
}
$: zoom && handleZoom()
$: wrapper && loadDocument(source)
$: wideView = controlsWidth && controlsWidth > 450
async function resetDoc() {
await doc?.destroy()
doc = undefined
}
function handleZoom() {
if (zoom && wrapper) {
try {
renderPdf(false)
} catch (err) {
error = err?.message ?? (typeof err === 'string' ? err : 'Error loading PDF')
}
}
}
async function loadDocument(src: string | ArrayBuffer | undefined) {
if (!src) {
return
}
try {
await resetDoc()
doc = await getDocument(src).promise
pageNumber = 1
await renderPdf(false, false)
error = undefined
} catch (err) {
await resetDoc()
error = err?.message ?? (typeof err === 'string' ? err : 'Error loading PDF')
console.log(err)
}
}
async function renderPdf(scaleToViewport = true, resizing = false) {
if (!(doc && wrapper && zoom)) {
return
}
const scrollPosition = wrapper.scrollTop / wrapper.scrollHeight
if (!resizing) {
pages = []
}
const nextPages: typeof pages = []
const nextChildren: HTMLCanvasElement[] = []
const { width } = wrapper.getBoundingClientRect()
let scale = zoom / 100
if (scaleToViewport) {
const firstViewport = (await doc.getPage(1)).getViewport({ scale: 1 })
// Rounded to the first integer that is a multiple of 10 and is less than the viewport width
zoom = Math.floor((width / firstViewport.width) * 10) * 10
scale = zoom / 100
}
for (let i = 0; i < doc.numPages; i++) {
const canvas = document.createElement('canvas')
const canvasContext = canvas.getContext('2d')
if (!canvasContext) {
console.warn('Could not get canvas context for PDF page ' + i)
continue
}
const page = await doc.getPage(i + 1)
nextPages.push(page)
const viewport = page.getViewport({ scale })
canvas.height = viewport.height
canvas.width = viewport.width
canvas.classList.add('mx-auto', 'my-4', 'shadow-sm')
await page.render({ canvasContext, viewport }).promise
nextChildren.push(canvas)
}
while (wrapper.firstChild) {
wrapper.removeChild(wrapper.firstChild)
}
pages = [...nextPages]
wrapper.append(...nextChildren)
wrapper.scrollTo({
top: scrollPosition * wrapper.scrollHeight
})
}
function scrollToPage(page: number) {
page = pageNumber = minMax(page, 1, pages.length)
const offset = (wrapper?.children.item(page - 1) as HTMLCanvasElement | null)?.offsetTop
if (!offset) {
return
}
// controlsHeight + 2px border + half of the top margin
const padding = (controlsHeight ? controlsHeight + 2 : 0) + 8
wrapper?.scrollTo({
top: offset - padding
})
}
const throttledScroll = throttle(onScroll, 400)
function onScroll() {
if (!wrapper) {
return
}
const THRESHOLD = 50
let scrollPosition = wrapper.scrollTop + THRESHOLD + (controlsHeight ?? 0)
let page = 1
for (let i = 0; i < pages.length; i++) {
const canvas = wrapper.children.item(i) as HTMLCanvasElement | null
if (scrollPosition < (canvas?.offsetTop ?? wrapper.scrollHeight)) {
break
}
page = i + 1
}
pageNumber = page
}
function syncZoomValue() {
const gridItem = findGridItem($app, id)
if (gridItem && gridItem.data.configuration.zoom.value !== zoom) {
gridItem.data.configuration.zoom.value = zoom
}
$app = $app
}
async function downloadPdf() {
if (!doc) {
return
}
const data = await doc.saveDocument()
const url = URL.createObjectURL(new Blob([data.buffer]))
const link = document.createElement('a')
link.href = url
link.download = 'document.pdf'
link.click()
URL.revokeObjectURL(url)
}
function minMax(value: number, min: number, max: number) {
if (value < min) {
return min
} else if (value > max) {
return max
}
return value
}
$: css = concatCustomCss($app.css?.pdfcomponent, customCss)
</script>
<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">
<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 = 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>
{/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>
</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
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>
{/if}

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 { AppEditorContext, ComponentCustomCSS } from '../../types'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
@@ -23,9 +23,10 @@
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<AppEditorContext>('AppEditorContext')
const { app } = getContext<AppViewerContext>('AppViewerContext')
ChartJS.register(
Title,
@@ -72,7 +73,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.doughnutStyle} bind:value={doughnut} />
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper {render} 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}

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