Compare commits

...
Author SHA1 Message Date
Ruben Fiszel d8336aa525 all 2025-07-07 12:18:05 +00:00
Ruben Fiszel 3fbd3ec4f9 fix: tag select in script builder top bar (#6136)
* all

* update

* react 18

* update

* update

* update
2025-07-07 10:27:09 +00:00
HugoCasa d69d277ff9 fix: correctly set selected step editor code when reverting to snapshot (#6131) 2025-07-04 11:19:42 +00:00
HugoCasa be62977047 fix(frontend): make sure to set workspaceStore and token before mount in extension (#6129)
* fix(frontend): make sure to set workspaceStore and token before mount in extension

* nit
2025-07-04 11:19:21 +00:00
centdixandRuben Fiszel 1ed1c18f45 internal: better claude review (#6130)
* better claude review

* Update pr-ready-review.yml

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-04 11:18:57 +00:00
wendrul e3aee0c587 fix: ctrl k not showing navigation items + improve ai button by making it an item in the menu (#6132)
* Fix missing navigation after migration to svelte 5 of ctrl k search

.includes doesn't work anymore because the items inside the array are
treated as state, and thus svelte wraps them with a Proxy thing

* Change ask ai to be a menu item

* Remove comment
2025-07-04 11:13:58 +00:00
c498c48ced feat: allow editing messages in AI chat (#6117)
* feat: add message editing functionality to AI chat

- Add editMessage() method to AIChatManager for editing user messages
- Add edit button UI that appears on hover for all user messages  
- Transform message display into textarea when editing
- Remove subsequent messages and resend on Enter
- Add keyboard shortcuts: Enter to save, Escape to cancel
- Maintain existing restart generation functionality

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* fix

* big cleaning

* use aichatinput when editing message

* cleaning

* fix

* remove logs

* better retry button

* fix

* Update frontend/src/lib/components/copilot/chat/ContextTextarea.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* cleaning

* fix retry logic

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: centdix <farhadg110@gmail.com>
Co-authored-by: centdix <40307056+centdix@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-04 13:11:29 +02:00
835645643e feat: Better tracing for audit logs, including a graph to visualize them (#6078)
* Migrate audit log page to svelte 5

* Add email and span cols to audit table

* Add token_prefixs to audit logs (into AuditAuthorable trait)

* Add audit logs graph (wip)

* Add audit span on push and jwt

* Unify same job audit into the same audit span

* Improve the graph visually

* Fix typo

* functioning graph with svelte issue

* Fix leak

* feat: migrate AuditLogsTable from DataTable to VirtualList for performance

- Replace DataTable component with VirtualList for handling thousands of rows
- Migrate to Svelte 5 runes ($props, $bindable, $derived, $state)
- Implement flattenLogs() for virtual scrolling with grouped date headers
- Add sticky indices and dynamic height calculation
- Update parent component to use callback prop pattern instead of events
- Preserve all existing functionality: filtering, selection, pagination
- Follows RunsTable.svelte implementation pattern

Resolves performance issues when displaying large audit log datasets.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* Fix remaining virtual list issues

* WIP graph

* Fix chart styling

* Fix npm check

* Fix missing audit_span arguments

* Update sqlx

* use varchar 255 for email as in other tables

* Remove syntax inconsistency

* Match struct with ee crate

* Update ee-repo-ref.txt

* Update worker_flow.rs

* Remove redefinition of trait to prevent shadowing

* Re add trait on oss but only when no `private` flag

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-03 21:02:45 +00:00
13716c7df3 chore: update croner dependency to version 2.2.0 (#6119)
Update croner dependency from 2.0.6 to 2.2.0 as requested in #6118.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-03 09:08:25 +00:00
Diego Imbert f35dae4ad9 Fix unstable flow layout (#6126) 2025-07-03 08:48:56 +00:00
HugoCasa 23d624aa23 fix: error handling for S3 file loading in py and ts clients (#6124) 2025-07-02 21:28:13 +00:00
centdixandellipsis-dev[bot] 4f77fdeeb0 internal: add ee ref command (#6115)
* add eeref command

* fix

* Update .github/workflows/git-commands.yaml

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-02 16:32:14 +00:00
Alexander Petric 7284c51762 fix: carousel app component, expose current index (#6120)
* feat: carousel app component, expose current index

* minimal diff
2025-07-02 16:31:43 +00:00
Ruben Fiszelandrubenfiszel 846ca65b87 chore(main): release 1.502.2 (#6108)
* chore(main): release 1.502.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-02 05:47:44 +00:00
centdix 8f2d7c473f fix ai chat tooltip position issue (#6114) 2025-07-01 22:15:06 +00:00
Guilhem 0afe3f9691 fix(frontend): improve step job load (#6109)
* fix(frontend): improve step job load

* nit
2025-07-01 23:41:26 +02:00
HugoCasa 2fb912b78c fix: bad spacing ai chat context elements (#6111) 2025-07-01 23:39:01 +02:00
pyranota a2334629d3 ci: add tests for python's global-site-packages (#6112)
Signed-off-by: pyranota <pyra@duck.com>
2025-07-01 23:38:26 +02:00
pyranota 106485989c nit: remove Assistants button for Nushell (#6113)
Signed-off-by: pyranota <pyra@duck.com>
2025-07-01 23:38:06 +02:00
Guilhem 7042a6f52d fix(frontend): only show test button for script modules (#6107)
* fix(frontend): only show test button for script modules

* nit
2025-07-01 18:14:06 +02:00
Alexander Petric dd91089436 improve graphql error reporting (#6092) 2025-07-01 15:45:04 +02:00
Ruben Fiszelandrubenfiszel 218792c80b chore(main): release 1.502.1 (#6103)
* chore(main): release 1.502.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-01 14:16:13 +02:00
pyranota 626372954b build: add claude-code to nix shell (#6101)
Signed-off-by: pyranota <pyra@duck.com>
2025-07-01 14:11:30 +02:00
Guilhem a4c295b5e8 fix(frontend): update test job logs (#6102) 2025-07-01 12:09:33 +00:00
Ruben Fiszelandrubenfiszel 77a59501b6 chore(main): release 1.502.0 (#6082)
* chore(main): release 1.502.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-06-30 20:18:34 +00:00
c51e128920 fix: add support for GCS object storage (#6083)
* AI: Updates to files (run 15923369101)

* all

* ee ref

* unneeded

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: HugoCasa <hugo@casademont.ch>
2025-06-30 20:11:57 +00:00
Ruben Fiszel 8ba3959ada fix: fix s3 settings reset 2025-06-30 20:05:26 +00:00
Ruben Fiszel e1ff8c1e53 fix cargo check 2025-06-30 16:41:09 +00:00
Ruben Fiszel e4eae849c9 fix cargo check 2025-06-30 16:33:03 +00:00
HugoCasa 805a8b574c fix: test up to broken due to mutable flow ai chat preview (#6096) 2025-06-30 16:27:44 +00:00
Ruben Fiszel d3db848450 fix drawer issues 2025-06-30 08:08:51 +00:00
Ruben Fiszel b180ff1e56 fix drawer issues 2025-06-30 08:00:14 +00:00
Ruben Fiszel 53f615ca0b lazy load nits 2025-06-30 00:07:35 +00:00
Ruben Fiszel a5c4de0637 nit 2025-06-29 22:48:22 +00:00
Ruben Fiszel a7e78f01f1 fix: optimize public apps rendering 2025-06-29 22:46:27 +00:00
Ruben Fiszel cd35b82761 minor perf nits 2025-06-29 20:15:03 +00:00
Ruben Fiszel 9da84ba436 minot perf nits 2025-06-29 20:05:32 +00:00
Ruben Fiszel cf8b0e69d9 lazy load superadmin settings 2025-06-29 19:41:34 +00:00
Ruben Fiszel 52ac19642d finish migrating app editor to svelte 5 (#6090)
* all

* all

* all

* all
2025-06-29 19:01:06 +00:00
Ruben Fiszel 6d2c118b3e recompute all fix 2025-06-29 10:09:16 +00:00
Ruben Fiszel 5ac5c873fd app svelte 5 improvements 2025-06-29 09:29:28 +00:00
Ruben Fiszel 8a6426bb4e improve arraystaticinputeditor 2025-06-29 09:03:35 +00:00
Ruben Fiszel 2c7705b78c more app svelte 5 migrations 2025-06-29 08:43:37 +00:00
Ruben Fiszel 021742b9ba fix appDisplayComponentByJobId 2025-06-29 08:18:46 +00:00
Ruben Fiszel c511ad28d0 fix delete issue of app 2025-06-29 07:54:41 +00:00
Ruben Fiszel d293dbca22 svelte 5 nits 2025-06-29 07:43:36 +00:00
Ruben Fiszel 029f8c9f43 svelte 5 nits 2025-06-28 21:54:36 +00:00
Ruben Fiszel 123b17dec9 svelte 5 nits 2025-06-28 21:46:49 +00:00
Ruben Fiszel e6a9f2d593 nits svelte 5 migration 2025-06-28 20:13:55 +02:00
Ruben Fiszel ca368aba7a fix: public url in app menu 2025-06-28 19:41:12 +02:00
Ruben Fiszel bb110985e2 make OUTSTANDING_WAITING_TIME_TS configurable 2025-06-28 19:06:55 +02:00
Ruben Fiszel d15b889fb4 warn after long list_jobs duration 2025-06-28 19:02:34 +02:00
Ruben Fiszel 7f18592a5e update rust to 1.88 2025-06-28 11:35:45 +02:00
Alexander Petric 3198da9709 hubpaths for git sync gpg improvement: add commit author (#6087) 2025-06-28 08:10:56 +00:00
Ruben Fiszel b8b7818a97 nit resource picker 2025-06-28 08:09:21 +00:00
HugoCasa 8edf4b2b92 feat: kafka better retry and errors (#6067)
* feat: kafka better retry and errors

* update ee ref
2025-06-27 22:34:58 +02:00
Diego Imbert ff4d268b93 nit flow ai button (#6084) 2025-06-27 22:34:49 +02:00
Ruben Fiszel 467b21c223 nit fix 2025-06-27 21:58:13 +02:00
Ruben Fiszel 42f56c431f fix resource select broken 2025-06-27 21:46:30 +02:00
Alexander Petric c56f115cce only refresh token token expires in <30mins (#6085)
* fix: align frontend with backend session invalidity time

* simplify

* no more async

* align

* also change the other interval
2025-06-27 18:48:29 +02:00
ded54f2e68 fix(frontend): improve flow editor settings bar UX (#6049)
* move settings and static inputs into top node

* Move test button in the top nodes

* Revert "Move test button in the top nodes"

This reverts commit 1c8648a538.

* Add error handler to top toolbar

* nit

* polishing

* add flow settings to topbar dropdown

* remove unused files

* progress

* progress

* fixes

* fix

* fix

* fix

* fix

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-06-27 15:26:03 +00:00
pyranota bcba46225f fix: fix critical alerts flapping on low disk (#6075)
* fix: fix critical alert on low disk flapping

* sqlx update

* Update ee-repo-ref.txt
2025-06-27 09:31:44 +00:00
claude[bot]andRuben Fiszel 115785644b Allow change_workspace_id on CLOUD_HOSTED for superadmins (#6073)
* feat: allow change_workspace_id on CLOUD_HOSTED for superadmins

- Import is_super_admin_email function from windmill_common::auth
- Modify CLOUD_HOSTED restriction to bypass for superadmin users  
- Resolves request to allow workspace ID changes for superadmins only

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* feat: allow superadmins to change workspace ID on cloud hosted environments

Update frontend conditional logic to show workspace ID change button for superadmins on cloud hosted environments. This complements the backend changes that already allow superadmins to perform the workspace ID change operation.

Changes:
- Add superadmin import from $lib/stores
- Update condition from `!isCloudHosted()` to `!isCloudHosted() || $superadmin`

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
2025-06-27 08:50:07 +00:00
HugoCasa 431437c344 feat: use FIM for code autocomplete (#6081)
* feat: use FIM for code autocomplete

* nits
2025-06-27 08:09:09 +00:00
dieriba 4a0857e217 show placeholder for select in script picker (#6074)
* add clearbable for script picker

* fix
2025-06-27 08:06:21 +00:00
90d7338e83 add GitHub app popover to ResourceEditor + refactor (#6079)
* Add GitHub App functionality to ResourceEditor and extract reusable component

- Extract GitHub App logic from ApiConnectForm into reusable GitHubAppIntegration component
- Add GitHub App functionality to ResourceEditor for consistent experience across workflows
- Create githubApp.ts service layer with comprehensive error handling and state management
- Maintain all existing functionality while improving code reusability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix code formatting for GitHub App integration files

Apply Prettier formatting to newly created and modified components to ensure
consistent code style across the GitHub App integration implementation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* linter

* Update frontend/src/lib/githubApp.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update frontend/src/lib/components/GitHubAppIntegration.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-27 08:04:46 +00:00
Alexander Petric aaf6e9d693 center icon if no label present (#6080) 2025-06-26 21:59:03 +00:00
dieriba 02d7f25f53 add clearbable for script picker (#6071) 2025-06-26 17:22:09 +02:00
Diego Imbert 63e52bbf96 fix no feedback on folder deletion (#6070) 2025-06-26 17:19:23 +02:00
Ruben Fiszelandrubenfiszel 57bc221458 chore(main): release 1.501.4 (#6061)
* chore(main): release 1.501.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-06-26 14:41:26 +02:00
Diego Imbert 4da3674577 fix buggy virtual list scroll on Firefox (#6068) 2025-06-26 14:35:50 +02:00
Diego Imbert 1a4b096f3c fix: schemaform reorder (#6069) 2025-06-26 14:35:30 +02:00
491 changed files with 13680 additions and 9179 deletions
+4 -4
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -44,7 +44,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -81,7 +81,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -121,7 +121,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -1,4 +1,4 @@
name: Update SQLx
name: Git commands
on:
issue_comment:
@@ -103,3 +103,73 @@ jobs:
repo: context.repo.repo,
body: 'Successfully ran sqlx update'
})
update-ee-ref:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/eeref')
runs-on: ubicloud-standard-2
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Comment on PR - Starting
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Starting ee ref update...'
})
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: ${{ github.event.issue.pull_request.head.ref }}
fetch-depth: 0
- name: Checkout windmill-ee-private
uses: actions/checkout@v3
with:
repository: windmill-labs/windmill-ee-private
path: windmill-ee-private
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
- name: Get last commit hash of private-repo
id: get-commit-hash
run: |
cd windmill-ee-private
COMMIT_HASH=$(git rev-parse HEAD)
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
echo "Latest commit hash: $COMMIT_HASH"
- name: Update ee-repo-ref.txt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${{ steps.get-commit-hash.outputs.commit_hash }}" > backend/ee-repo-ref.txt
echo "Updated backend/ee-repo-ref.txt with commit hash: ${{ steps.get-commit-hash.outputs.commit_hash }}"
# commit and push the changes
PR_NUMBER=${{ github.event.issue.number }}
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add backend/ee-repo-ref.txt
git commit -m "Update ee-repo-ref.txt" || echo "No changes to commit"
git push origin $BRANCH_NAME
- name: Comment on PR - Completed
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Successfully updated ee-repo-ref.txt'
})
+34 -15
View File
@@ -1,22 +1,41 @@
name: Auto Comment on PR Ready for Review
name: Claude Auto Review
on:
pull_request:
types: [opened, ready_for_review]
types: [ready_for_review, opened]
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
add-review-comment:
if: github.event.pull_request.draft == false
runs-on: ubicloud-standard-2
auto-review:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false || github.event.pull_request.ready_for_review == true
permissions:
contents: read
pull-requests: read
id-token: write
steps:
- name: Add review comment
uses: actions/github-script@v7
- name: Checkout repository
uses: actions/checkout@v4
with:
github-token: ${{ secrets.PUBLIC_REPO_TOKEN }}
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: '/ai review this PR'
});
fetch-depth: 1
- name: Automatic PR Review
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
direct_prompt: |
Please review this pull request and provide comprehensive feedback.
Focus on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security implications
Provide constructive feedback with specific suggestions for improvement.
Use inline comments to highlight specific areas of concern.
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
+44
View File
@@ -1,5 +1,49 @@
# Changelog
## [1.502.2](https://github.com/windmill-labs/windmill/compare/v1.502.1...v1.502.2) (2025-07-01)
### Bug Fixes
* bad spacing ai chat context elements ([#6111](https://github.com/windmill-labs/windmill/issues/6111)) ([2fb912b](https://github.com/windmill-labs/windmill/commit/2fb912b78c90d9e70d3db4b0c3c473831161c8b4))
* **frontend:** improve step job load ([#6109](https://github.com/windmill-labs/windmill/issues/6109)) ([0afe3f9](https://github.com/windmill-labs/windmill/commit/0afe3f9691d93f837b10b29a0cf125eaa175589d))
* **frontend:** only show test button for script modules ([#6107](https://github.com/windmill-labs/windmill/issues/6107)) ([7042a6f](https://github.com/windmill-labs/windmill/commit/7042a6f52db823d6b9b5ad14fa83af36880bd2d5))
## [1.502.1](https://github.com/windmill-labs/windmill/compare/v1.502.0...v1.502.1) (2025-07-01)
### Bug Fixes
* **frontend:** update test job logs ([#6102](https://github.com/windmill-labs/windmill/issues/6102)) ([a4c295b](https://github.com/windmill-labs/windmill/commit/a4c295b5e857314d78de6bb6ab942dc60ff99279))
## [1.502.0](https://github.com/windmill-labs/windmill/compare/v1.501.4...v1.502.0) (2025-06-30)
### Features
* kafka better retry and errors ([#6067](https://github.com/windmill-labs/windmill/issues/6067)) ([8edf4b2](https://github.com/windmill-labs/windmill/commit/8edf4b2b92fe77ad86d96d41095b05540176e783))
* use FIM for code autocomplete ([#6081](https://github.com/windmill-labs/windmill/issues/6081)) ([431437c](https://github.com/windmill-labs/windmill/commit/431437c3449ddcd8c45bacd696a4db209577773b))
### Bug Fixes
* add support for GCS object storage ([#6083](https://github.com/windmill-labs/windmill/issues/6083)) ([c51e128](https://github.com/windmill-labs/windmill/commit/c51e128920801ec7199033c59655a0bcdd5341ba))
* fix critical alerts flapping on low disk ([#6075](https://github.com/windmill-labs/windmill/issues/6075)) ([bcba462](https://github.com/windmill-labs/windmill/commit/bcba46225f094e60bb7e77ed74fc060ffaccb6c6))
* fix s3 settings reset ([8ba3959](https://github.com/windmill-labs/windmill/commit/8ba3959adac955cd4ec5cbebd357703992c68baa))
* **frontend:** improve flow editor settings bar UX ([#6049](https://github.com/windmill-labs/windmill/issues/6049)) ([ded54f2](https://github.com/windmill-labs/windmill/commit/ded54f2e68da09618c377cd699e0a2eaa53a63a8))
* optimize public apps rendering ([a7e78f0](https://github.com/windmill-labs/windmill/commit/a7e78f01f1697b8a4a3c61cd4377bc34b8077d38))
* public url in app menu ([ca368ab](https://github.com/windmill-labs/windmill/commit/ca368aba7a334efc9963f69b3a4462d9972997f4))
* test up to broken due to mutable flow ai chat preview ([#6096](https://github.com/windmill-labs/windmill/issues/6096)) ([805a8b5](https://github.com/windmill-labs/windmill/commit/805a8b574c057b911bff5d335e0c63051c6587ee))
## [1.501.4](https://github.com/windmill-labs/windmill/compare/v1.501.3...v1.501.4) (2025-06-26)
### Bug Fixes
* add windows paths to uv install to find git/ssh ([#6063](https://github.com/windmill-labs/windmill/issues/6063)) ([835f1d2](https://github.com/windmill-labs/windmill/commit/835f1d2ec945145942deaa41cb3bd176ed276279))
* optionally enable CSP headers ([#6033](https://github.com/windmill-labs/windmill/issues/6033)) ([d933648](https://github.com/windmill-labs/windmill/commit/d933648d3666b2ca9d813e04b9f19ddc3c7efda3))
* schemaform reorder ([#6069](https://github.com/windmill-labs/windmill/issues/6069)) ([1a4b096](https://github.com/windmill-labs/windmill/commit/1a4b096f3ce40e238f1724aa4fd26649d70cb62a))
## [1.501.3](https://github.com/windmill-labs/windmill/compare/v1.501.2...v1.501.3) (2025-06-25)
+1 -1
View File
@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.86-slim-bookworm
ARG RUST_IMAGE=rust:1.88-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
+164 -151
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.501.3"
version = "1.502.2"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.501.3"
version = "1.502.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -348,7 +348,7 @@ nkeys = "0.4.4"
nu-parser = { version = "0.101.0", default-features = false }
datafusion = "47.0.0"
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure"] }
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure", "gcp"] }
openidconnect = { version = "4.0.0-rc.1" }
aws-config = "^1"
aws-sdk-sqs = "1.57.0"
+1 -1
View File
@@ -1 +1 @@
835a91c7c31ea749759cd8af0922ad837049ea2a
21aabec96e91c8075dd637d1e32af90e495082fc
@@ -0,0 +1,3 @@
-- Remove email and span columns from audit table
ALTER TABLE audit DROP COLUMN email;
ALTER TABLE audit DROP COLUMN span;
@@ -0,0 +1,3 @@
-- Add email and span columns to audit table
ALTER TABLE audit ADD COLUMN email VARCHAR(255);
ALTER TABLE audit ADD COLUMN span VARCHAR(255);
+4 -3
View File
@@ -54,7 +54,8 @@ use windmill_common::{
stats_oss::schedule_stats,
triggers::TriggerKind,
utils::{
create_default_worker_suffix, create_ssh_agent_worker_suffix, worker_name_with_suffix,
create_worker_suffix,
worker_name_with_suffix,
Mode, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS,
},
worker::{
@@ -347,7 +348,7 @@ async fn windmill_main() -> anyhow::Result<()> {
"Creating http client for cluster using base internal url {}",
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
);
let suffix = create_ssh_agent_worker_suffix(&hostname);
let suffix = create_worker_suffix(&hostname);
(
Connection::Http(build_agent_http_client(&suffix)),
Some(suffix),
@@ -687,7 +688,7 @@ Windmill Community Edition {GIT_VERSION}
let suffix = if i == 0 && first_suffix.is_some() {
first_suffix.as_ref().unwrap().clone()
} else {
create_default_worker_suffix(&hostname)
create_worker_suffix(&hostname)
};
let worker_conn = WorkerConn {
+1
View File
@@ -1926,6 +1926,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
&job.email,
&job.id,
None,
Some(format!("handle_zombie_jobs")),
)
.await
.expect("could not create job token");
+87 -2
View File
@@ -322,7 +322,7 @@ mod suspend_resume {
let second = completed.next().await.unwrap();
// print_job(second, &db).await;
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
))
@@ -427,7 +427,7 @@ mod suspend_resume {
/* ... and send a request resume it. */
let second = completed.next().await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
))
@@ -935,6 +935,7 @@ impl RunJob {
/* user */ "test-user",
/* email */ "test@windmill.dev",
/* permissioned_as */ "u/test-user".to_string(),
/* token_prefix */ None,
/* scheduled_for_o */ None,
/* schedule_path */ None,
/* parent_job */ None,
@@ -2089,6 +2090,89 @@ def main():
assert_eq!(result, serde_json::json!("hello world"));
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_global_site_packages(db: Pool<Postgres>) {
use windmill_common::{cache::concatcp, worker::ROOT_CACHE_DIR};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
// Shared for all 3.12.*
let path = concatcp!(ROOT_CACHE_DIR, "python_3_12/global-site-packages").to_owned();
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path + "/my_global_site_package_3_12_any.py", "").unwrap();
// 3.12
{
let content = r#"# py: ==3.12
#requirements:
#
import my_global_site_package_3_12_any
def main():
return "hello world"
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
lock: None,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
}
// 3.12.1
{
let content = r#"# py: ==3.12.1
#requirements:
#
import my_global_site_package_3_12_any
def main():
return "hello world"
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
lock: None,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
}
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_job_heavy_dep(db: Pool<Postgres>) {
@@ -4126,6 +4210,7 @@ async fn test_result_format(db: Pool<Postgres>) {
"",
&Uuid::nil(),
None,
None,
)
.await
.unwrap();
+9 -3
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.501.3
version: 1.502.2
title: Windmill API
contact:
@@ -14304,6 +14304,8 @@ components:
type: string
parameters:
type: object
span:
type: string
required:
- id
- timestamp
@@ -16563,11 +16565,13 @@ components:
properties:
type:
type: string
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
s3_resource_path:
type: string
azure_blob_resource_path:
type: string
gcs_resource_path:
type: string
public_resource:
type: boolean
secondary_storage:
@@ -16578,11 +16582,13 @@ components:
type:
type: string
enum:
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
s3_resource_path:
type: string
azure_blob_resource_path:
type: string
gcs_resource_path:
type: string
public_resource:
type: boolean
+4
View File
@@ -4,6 +4,7 @@ use uuid::Uuid;
use std::str::FromStr;
use regex::Regex;
use serde_json::Value;
use crate::auth::OptTokened;
use crate::db::{ApiAuthed, DB};
use crate::jobs::{cancel_suspended_job, resume_suspended_job, QueryApprover, QueryOrBody, ResumeUrls, get_resume_urls_internal};
use axum::{extract::{Path, Query}, Extension};
@@ -101,6 +102,7 @@ pub fn extract_w_id_from_resume_url(resume_url: &str) -> Result<&str, Error> {
pub async fn handle_resume_action(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
db: DB,
resume_url: &str,
form_data: Value,
@@ -135,6 +137,7 @@ pub async fn handle_resume_action(
let res = if action == "resume" {
resume_suspended_job(
authed,
opt_tokened,
Extension(db.clone()),
Path((
w_id.to_string(),
@@ -149,6 +152,7 @@ pub async fn handle_resume_action(
} else {
cancel_suspended_job(
authed,
opt_tokened,
Extension(db.clone()),
Path((
w_id.to_string(),
+9
View File
@@ -9,6 +9,7 @@ use std::{collections::HashMap, sync::Arc};
*/
use crate::{
auth::OptTokened,
db::{ApiAuthed, DB},
resources::get_resource_value_interpolated_internal,
users::{require_owner_of_path, OptAuthed},
@@ -52,6 +53,7 @@ use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
apps::{AppScriptId, ListAppQuery},
auth::TOKEN_PREFIX_LEN,
cache::{self, future::FutureCachedExt},
db::UserDB,
error::{to_anyhow, Error, JsonResult, Result},
@@ -1040,6 +1042,7 @@ async fn create_app_internal<'a>(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -1411,6 +1414,7 @@ async fn update_app_internal<'a>(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -1517,6 +1521,7 @@ fn empty_triggerables(mut policy: Policy) -> Policy {
async fn execute_component(
OptAuthed(opt_authed): OptAuthed,
tokened: OptTokened,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -1719,6 +1724,10 @@ async fn execute_component(
&username,
email,
permissioned_as,
opt_authed
.and_then(|a| a.token_prefix)
.or_else(|| tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()))
.as_deref(),
None,
None,
None,
+9 -1
View File
@@ -21,7 +21,7 @@ use std::sync::{
use tokio::sync::RwLock;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims},
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, TOKEN_PREFIX_LEN},
jwt,
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
@@ -134,6 +134,7 @@ impl AuthCache {
folders: claims.folders,
scopes: None,
username_override,
token_prefix: claims.audit_span,
};
AUTH_CACHE.insert(
@@ -217,6 +218,7 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
} else {
let groups = vec![name.to_string()];
@@ -238,6 +240,7 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
} else {
@@ -252,6 +255,7 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
}
@@ -299,6 +303,7 @@ impl AuthCache {
folders,
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
None if super_admin => Some(ApiAuthed {
@@ -310,6 +315,7 @@ impl AuthCache {
folders: vec![],
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
}),
None => None,
}
@@ -323,6 +329,7 @@ impl AuthCache {
folders: Vec::new(),
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
}
@@ -355,6 +362,7 @@ impl AuthCache {
folders: Vec::new(),
scopes: None,
username_override: None,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
} else {
None
+6
View File
@@ -836,6 +836,7 @@ pub struct ApiAuthed {
pub folders: Vec<(String, bool, bool)>,
pub scopes: Option<Vec<String>>,
pub username_override: Option<String>,
pub token_prefix: Option<String>,
}
impl From<ApiAuthed> for Authed {
@@ -848,6 +849,7 @@ impl From<ApiAuthed> for Authed {
groups: value.groups,
folders: value.folders,
scopes: value.scopes,
token_prefix: value.token_prefix,
}
}
}
@@ -858,6 +860,7 @@ impl From<&ApiAuthed> for AuditAuthor {
email: value.email.clone(),
username: value.username.clone(),
username_override: value.username_override.clone(),
token_prefix: value.token_prefix.clone(),
}
}
}
@@ -878,6 +881,9 @@ impl AuditAuthorable for ApiAuthed {
fn username_override(&self) -> Option<&str> {
self.username_override.as_deref()
}
fn token_prefix(&self) -> Option<&str> {
self.token_prefix.as_deref()
}
}
impl Authable for ApiAuthed {
+2
View File
@@ -494,6 +494,7 @@ async fn create_flow(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -935,6 +936,7 @@ async fn update_flow(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
+73 -76
View File
@@ -20,22 +20,20 @@ use sqlx::Pool;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
#[cfg(feature = "prometheus")]
use std::sync::atomic::Ordering;
use tokio::io::AsyncReadExt;
#[cfg(feature = "prometheus")]
use tokio::time::Instant;
use tower::ServiceBuilder;
use windmill_common::auth::is_super_admin_email;
use windmill_common::auth::{is_super_admin_email, TOKEN_PREFIX_LEN};
use windmill_common::error::JsonResult;
use windmill_common::flow_status::{JobResult, RestartedFrom};
use windmill_common::jobs::{format_completed_job_result, format_result, ENTRYPOINT_OVERRIDE};
use windmill_common::utils::WarnAfterExt;
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
use windmill_common::variables::get_workspace_key;
use crate::add_webhook_allowed_origin;
use crate::auth::{OptTokened, Tokened};
use crate::concurrency_groups::join_concurrency_key;
use crate::db::ApiAuthed;
@@ -84,9 +82,6 @@ use windmill_common::{
},
};
#[cfg(feature = "prometheus")]
use windmill_common::{METRICS_DEBUG_ENABLED, METRICS_ENABLED};
use windmill_common::{
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, BASE_URL,
@@ -96,36 +91,6 @@ use windmill_queue::{
PushArgsOwned, PushIsolationLevel,
};
#[cfg(feature = "prometheus")]
type Histo = prometheus::Histogram;
#[cfg(not(feature = "prometheus"))]
type Histo = ();
#[cfg(feature = "prometheus")]
fn setup_list_jobs_debug_metrics() -> Option<Histo> {
let api_list_jobs_query_duration = if METRICS_DEBUG_ENABLED.load(Ordering::Relaxed)
&& METRICS_ENABLED.load(Ordering::Relaxed)
{
Some(
prometheus::register_histogram!(prometheus::HistogramOpts::new(
"api_list_jobs_query_duration",
"Duration of listing jobs (query)",
))
.expect("register prometheus metric"),
)
} else {
None
};
api_list_jobs_query_duration
}
#[cfg(not(feature = "prometheus"))]
fn setup_list_jobs_debug_metrics() -> Option<Histo> {
None
}
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
@@ -136,8 +101,6 @@ pub fn workspaced_service() -> Router {
let ce_headers =
ServiceBuilder::new().layer(axum::middleware::from_fn(add_webhook_allowed_origin));
let api_list_jobs_query_duration = setup_list_jobs_debug_metrics();
Router::new()
.route(
"/run/f/*script_path",
@@ -212,10 +175,7 @@ pub fn workspaced_service() -> Router {
)
.route("/add_batch_jobs/:n", post(add_batch_jobs))
.route("/run/preview_flow", post(run_preview_flow_job))
.route(
"/list",
get(list_jobs).layer(Extension(api_list_jobs_query_duration)),
)
.route("/list", get(list_jobs))
.route(
"/list_selected_job_groups",
// We use post because sending a huge array as a query param can produce
@@ -334,6 +294,7 @@ struct JsonPath {
}
async fn get_result_by_id(
authed: ApiAuthed,
tokened: Tokened,
Extension(db): Extension<DB>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
Query(JsonPath { json_path, .. }): Query<JsonPath>,
@@ -342,7 +303,7 @@ async fn get_result_by_id(
windmill_queue::get_result_by_id(db.clone(), w_id.clone(), flow_id, node_id, json_path)
.await?;
log_job_view(&db, Some(&authed), &w_id, &flow_id).await?;
log_job_view(&db, Some(&authed), Some(&tokened.token), &w_id, &flow_id).await?;
Ok(Json(res))
}
@@ -378,6 +339,7 @@ async fn get_db_clock(Extension(db): Extension<DB>) -> windmill_common::error::J
async fn cancel_job_api(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>,
@@ -390,6 +352,7 @@ async fn cancel_job_api(
username: "anonymous".to_string(),
username_override: None,
email: "anonymous".to_string(),
token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()),
},
};
let (mut tx, job_option) = tokio::time::timeout(
@@ -488,6 +451,7 @@ async fn cancel_persistent_script_api(
async fn force_cancel(
OptAuthed(opt_authed): OptAuthed,
tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>,
@@ -500,6 +464,7 @@ async fn force_cancel(
username: "anonymous".to_string(),
username_override: None,
email: "anonymous".to_string(),
token_prefix: tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()),
},
};
@@ -551,6 +516,7 @@ async fn force_cancel(
async fn get_flow_job_debug_info(
OptAuthed(opt_authed): OptAuthed,
tokened_o: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
@@ -602,7 +568,7 @@ async fn get_flow_job_debug_info(
}
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), tokened_o.token.as_deref(), &w_id, &id).await?;
Ok(Json(jobs).into_response())
} else {
@@ -662,6 +628,7 @@ struct GetJobQuery {
async fn get_job(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(GetJobQuery { no_logs }): Query<GetJobQuery>,
@@ -681,7 +648,7 @@ async fn get_job(
let mut job = get.fetch(&db, id, &w_id).await?;
job.fetch_outstanding_wait_time(&db).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
Ok(Json(job).into_response())
}
@@ -1129,6 +1096,7 @@ async fn get_logs_from_disk(
async fn get_job_logs(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
@@ -1166,7 +1134,7 @@ async fn get_job_logs(
}
let logs = record.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) = get_logs_from_store(record.log_offset, &logs, &record.log_file_index).await
@@ -1203,7 +1171,7 @@ async fn get_job_logs(
}
let logs = text.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) =
@@ -1227,6 +1195,7 @@ async fn get_job_logs(
async fn get_args(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> JsonResult<Box<RawValue>> {
@@ -1252,7 +1221,7 @@ async fn get_args(
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
} else {
@@ -1273,7 +1242,7 @@ async fn get_args(
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
}
@@ -1926,7 +1895,6 @@ async fn list_jobs(
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListCompletedQuery>,
Extension(_api_list_jobs_query_duration): Extension<Option<Histo>>,
) -> error::JsonResult<Vec<Job>> {
check_scopes(&authed, || format!("jobs:listjobs"))?;
@@ -1990,24 +1958,12 @@ async fn list_jobs(
};
let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?;
#[cfg(feature = "prometheus")]
let start = Instant::now();
#[cfg(feature = "prometheus")]
if _api_list_jobs_query_duration.is_some() || true {
tracing::info!("list_jobs query: {}", sql);
}
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql).fetch_all(&mut *tx).await?;
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql)
.fetch_all(&mut *tx)
.warn_after_seconds_with_sql(5, format!("list_jobs: {}", sql))
.await?;
tx.commit().await?;
#[cfg(feature = "prometheus")]
if let Some(api_list_jobs_query_duration) = _api_list_jobs_query_duration {
let duration = start.elapsed().as_secs_f64();
api_list_jobs_query_duration.observe(duration);
tracing::info!("list_jobs query took {}s: {}", duration, sql);
}
Ok(Json(jobs.into_iter().map(From::from).collect()))
}
@@ -2047,13 +2003,23 @@ pub async fn resume_suspended_flow_as_owner(
pub async fn resume_suspended_job(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
resume_suspended_job_internal(
value, db, w_id, job_id, resume_id, approver, secret, authed, true,
value,
db,
w_id,
job_id,
resume_id,
approver,
secret,
authed,
opt_tokened,
true,
)
.await
}
@@ -2067,6 +2033,7 @@ async fn resume_suspended_job_internal(
approver: QueryApprover,
secret: String,
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
approved: bool,
) -> Result<StatusCode, Error> {
let value = value.unwrap_or(serde_json::Value::Null);
@@ -2145,6 +2112,7 @@ async fn resume_suspended_job_internal(
email: approver.clone(),
username: approver.clone(),
username_override: None,
token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()),
},
};
audit_log(
@@ -2314,13 +2282,14 @@ async fn get_suspended_flow_info<'c>(
pub async fn cancel_suspended_job(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
resume_suspended_job_internal(
value, db, w_id, job_id, resume_id, approver, secret, authed, false,
value, db, w_id, job_id, resume_id, approver, secret, authed, opt_tokened, false,
)
.await
}
@@ -2338,6 +2307,7 @@ pub struct QueryApprover {
pub async fn get_suspended_job_flow(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
@@ -2404,7 +2374,7 @@ pub async fn get_suspended_job_flow(
approvers_from_status
};
log_job_view(&db, authed.as_ref(), &w_id, &job).await?;
log_job_view(&db, authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job).await?;
Ok(Json(SuspendedJobFlow { job: flow, approvers }).into_response())
}
@@ -3561,6 +3531,7 @@ pub async fn run_flow_by_path_inner(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
@@ -3654,6 +3625,7 @@ pub async fn restart_flow(
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
@@ -3746,6 +3718,7 @@ pub async fn run_script_by_path_inner(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
@@ -3891,6 +3864,7 @@ pub async fn run_workflow_as_code(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
Some(job_id),
@@ -4306,6 +4280,7 @@ impl JobViewCache {
async fn log_job_view(
db: &DB,
opt_authed: Option<&ApiAuthed>,
opt_token: Option<&str>,
w_id: &str,
job_id: &Uuid,
) -> error::Result<()> {
@@ -4316,6 +4291,7 @@ async fn log_job_view(
username: "anonymous".to_string(),
username_override: None,
email: "anonymous".to_string(),
token_prefix: opt_token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string())
},
};
if JOB_VIEW_CACHE
@@ -4414,6 +4390,7 @@ pub async fn run_wait_result_job_by_path_get(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
run_query.parent_job,
@@ -4554,6 +4531,7 @@ pub async fn run_wait_result_script_by_path_internal(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
run_query.parent_job,
@@ -4667,6 +4645,7 @@ pub async fn run_wait_result_script_by_hash(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
run_query.parent_job,
@@ -4781,6 +4760,7 @@ pub async fn run_wait_result_flow_by_path_internal(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
@@ -4851,6 +4831,7 @@ async fn run_preview_script(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
None,
@@ -4939,6 +4920,7 @@ async fn run_bundle_preview_script(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
None,
@@ -5106,6 +5088,7 @@ async fn run_dependencies_job(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -5163,6 +5146,7 @@ async fn run_flow_dependencies_job(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -5503,6 +5487,7 @@ async fn run_preview_flow_job(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
None,
@@ -5622,6 +5607,7 @@ pub async fn run_job_by_hash_inner(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
@@ -5714,6 +5700,7 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
async fn get_job_update(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job_id)): Path<(String, Uuid)>,
Query(JobUpdateQuery { log_offset, get_progress, running }): Query<JobUpdateQuery>,
@@ -5770,7 +5757,7 @@ async fn get_job_update(
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &job_id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job_id).await?;
Ok(Json(JobUpdate {
running: record.running,
completed: record.completed,
@@ -6058,6 +6045,7 @@ async fn list_completed_jobs(
async fn get_completed_job<'a>(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
@@ -6083,7 +6071,7 @@ async fn get_completed_job<'a>(
// .fetch_optional(db)
// .await.ok().flatten().flatten();
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
Ok(response)
}
@@ -6097,6 +6085,7 @@ pub struct RawResult {
async fn get_completed_job_result(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(JsonPath { json_path, suspended_job, approver, resume_id, secret }): Query<JsonPath>,
@@ -6185,7 +6174,7 @@ async fn get_completed_job_result(
raw_result.result.as_mut(),
);
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
Ok(Json(raw_result.result).into_response())
}
@@ -6243,6 +6232,7 @@ struct GetCompletedJobQuery {
async fn get_completed_job_result_maybe(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(GetCompletedJobQuery { get_started }): Query<GetCompletedJobQuery>,
@@ -6275,7 +6265,7 @@ async fn get_completed_job_result_maybe(
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
Ok(Json(CompletedJobResult {
started: Some(true),
@@ -6313,6 +6303,7 @@ async fn get_completed_job_result_maybe(
async fn delete_completed_job<'a>(
authed: ApiAuthed,
Tokened { token }: Tokened,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
@@ -6362,5 +6353,11 @@ async fn delete_completed_job<'a>(
.await?;
tx.commit().await?;
return get_completed_job(OptAuthed(Some(authed)), Extension(db), Path((w_id, id))).await;
return get_completed_job(
OptAuthed(Some(authed)),
OptTokened { token: Some(token) },
Extension(db),
Path((w_id, id)),
)
.await;
}
+1
View File
@@ -508,6 +508,7 @@ pub async fn transform_json_value<'c>(
email: "backend".to_string(),
username: "backend".to_string(),
username_override: None,
token_prefix: None,
}),
)
.await?;
+2 -9
View File
@@ -7,10 +7,7 @@
*/
use crate::{
db::{ApiAuthed, DB},
settings::{delete_global_setting, set_global_setting_internal},
users::maybe_refresh_folders,
utils::require_super_admin,
db::{ApiAuthed, DB}, settings::{delete_global_setting, set_global_setting_internal}, users::maybe_refresh_folders, utils::require_super_admin
};
use axum::{
extract::{Extension, Path, Query},
@@ -25,11 +22,7 @@ use std::str::FromStr;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
schedule::Schedule,
utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath},
worker::to_raw_value,
db::UserDB, error::{Error, JsonResult, Result}, schedule::Schedule, utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, worker::to_raw_value
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::schedule::push_scheduled_job;
+1
View File
@@ -941,6 +941,7 @@ async fn create_script_internal<'c>(
&authed.username,
&authed.email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
None,
+7 -5
View File
@@ -11,11 +11,11 @@ use std::collections::HashMap;
use windmill_common::error::Error;
use windmill_common::variables::get_secret_value_as_admin;
use crate::approvals::{
use crate::{approvals::{
extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType,
MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage,
ResumeFormField, ResumeSchema,
};
}, auth::OptTokened};
use crate::db::{ApiAuthed, DB};
use crate::jobs::{QueryApprover, ResumeUrls};
@@ -116,6 +116,7 @@ struct PrivateMetadata {
pub async fn slack_app_callback_handler(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Form(form_data): Form<SlackFormData>,
) -> Result<StatusCode, Error> {
@@ -124,8 +125,8 @@ pub async fn slack_app_callback_handler(
tracing::debug!("Payload: {:#?}", payload);
match payload.r#type {
PayloadType::ViewSubmission => handle_submission(authed, db, &payload, "resume").await?,
PayloadType::ViewClosed => handle_submission(authed, db, &payload, "cancel").await?,
PayloadType::ViewSubmission => handle_submission(authed, opt_tokened, db, &payload, "resume").await?,
PayloadType::ViewClosed => handle_submission(authed, opt_tokened, db, &payload, "cancel").await?,
_ => {
if let Some(actions) = &payload.actions {
if let Some(action) = actions.first() {
@@ -256,6 +257,7 @@ pub async fn request_slack_approval(
async fn handle_submission(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
db: DB,
payload: &Payload,
action: &str,
@@ -294,7 +296,7 @@ async fn handle_submission(
}
// Use the common handler to process the resume/cancel action
handle_resume_action(authed, db.clone(), &resume_url, state_json, action).await?;
handle_resume_action(authed, opt_tokened, db.clone(), &resume_url, state_json, action).await?;
let w_id = extract_w_id_from_resume_url(&resume_url)?;
let slack_token = get_slack_token(&db, &resource_path, w_id).await?;
+28 -6
View File
@@ -44,7 +44,7 @@ use tower_cookies::{Cookie, Cookies};
use tracing::Instrument;
use windmill_audit::audit_oss::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::auth::fetch_authed_from_permissioned_as;
use windmill_common::auth::{fetch_authed_from_permissioned_as, TOKEN_PREFIX_LEN};
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
use windmill_common::oauth2::InstanceEvent;
use windmill_common::users::COOKIE_NAME;
@@ -243,13 +243,14 @@ pub async fn fetch_api_authed_from_permissioned_as(
let api_authed = ApiAuthed {
username: authed.username,
email: email,
email,
is_admin: authed.is_admin,
is_operator: authed.is_operator,
groups: authed.groups,
folders: authed.folders,
scopes: authed.scopes,
username_override: None,
token_prefix: authed.token_prefix,
};
API_AUTHED_CACHE.insert(
@@ -690,7 +691,12 @@ async fn logout(
};
audit_log(
&mut *tx,
&AuditAuthor { email: email.clone(), username: email, username_override: None },
&AuditAuthor {
email: email.clone(),
username: email,
username_override: None,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
},
audit_message,
ActionKind::Delete,
"global",
@@ -1274,7 +1280,10 @@ async fn join_workspace<'c>(
Ok((tx, username))
}
async fn leave_instance(Extension(db): Extension<DB>, authed: ApiAuthed) -> Result<String> {
async fn leave_instance(
Extension(db): Extension<DB>,
authed: ApiAuthed,
) -> Result<String> {
let mut tx = db.begin().await?;
sqlx::query!("DELETE FROM password WHERE email = $1", &authed.email)
.execute(&mut *tx)
@@ -1639,8 +1648,12 @@ async fn login(
) -> Result<String> {
let mut tx = db.begin().await?;
let email = email.to_lowercase();
let audit_author =
AuditAuthor { email: email.clone(), username: email.clone(), username_override: None };
let audit_author = AuditAuthor {
email: email.clone(),
username: email.clone(),
username_override: None,
token_prefix: None,
};
let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as(
"SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \
'password'",
@@ -1689,6 +1702,13 @@ async fn login(
let token = create_session_token(&email, super_admin, &mut tx, cookies).await?;
let audit_author = AuditAuthor {
email: email.clone(),
username: email.clone(),
username_override: None,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
};
audit_log(
&mut *tx,
&audit_author,
@@ -1758,6 +1778,7 @@ async fn refresh_token(
email: authed.email.to_string(),
username: authed.email.to_string(),
username_override: None,
token_prefix: authed.token_prefix,
},
"users.token.refresh",
ActionKind::Create,
@@ -1798,6 +1819,7 @@ pub async fn create_session_token<'c>(
email: email.to_string(),
username: email.to_string(),
username_override: None,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
},
"users.token.invalidate_old_sessions",
ActionKind::Delete,
+6
View File
@@ -415,3 +415,9 @@ pub struct ExpiringCacheEntry<T> {
pub value: T,
pub expiry: std::time::Instant,
}
#[cfg(all(feature = "kafka", feature = "enterprise", feature = "private"))]
pub async fn update_rw_lock<T>(lock: std::sync::Arc<tokio::sync::RwLock<T>>, value: T) -> () {
let mut w = lock.write().await;
*w = value;
}
+2 -7
View File
@@ -7,9 +7,7 @@
*/
use crate::{
db::{ApiAuthed, DB},
users::{maybe_refresh_folders, require_owner_of_path},
webhook_util::{WebhookMessage, WebhookShared},
db::{ApiAuthed, DB}, users::{maybe_refresh_folders, require_owner_of_path}, webhook_util::{WebhookMessage, WebhookShared}
};
use axum::{
@@ -23,10 +21,7 @@ use serde_json::Value;
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
use windmill_audit::ActionKind;
use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
utils::{not_found_if_none, paginate, Pagination, StripPath},
variables::{
db::UserDB, error::{Error, JsonResult, Result}, utils::{not_found_if_none, paginate, Pagination, StripPath}, variables::{
build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable,
},
worker::CLOUD_HOSTED,
+2 -1
View File
@@ -14,6 +14,7 @@ use windmill_audit::ActionKind;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
auth::is_super_admin_email,
error::{Error, Result},
utils::require_admin,
};
@@ -32,7 +33,7 @@ pub(crate) async fn change_workspace_id(
Extension(db): Extension<DB>,
Json(rw): Json<ChangeWorkspaceId>,
) -> Result<String> {
if *CLOUD_HOSTED {
if *CLOUD_HOSTED && !is_super_admin_email(&db, &authed.email).await? {
return Err(Error::BadRequest(
"This feature is not available on the cloud".to_string(),
));
+16 -8
View File
@@ -20,14 +20,6 @@ use {
},
};
#[derive(Clone)]
#[cfg(not(feature = "private"))]
pub struct AuditAuthor {
pub username: String,
pub email: String,
pub username_override: Option<String>,
}
#[cfg(not(feature = "private"))]
impl AuditAuthorable for AuditAuthor {
fn email(&self) -> &str {
@@ -41,6 +33,10 @@ impl AuditAuthorable for AuditAuthor {
fn username_override(&self) -> Option<&str> {
self.username_override.as_deref()
}
fn token_prefix(&self) -> Option<&str> {
self.token_prefix.as_deref()
}
}
#[cfg(not(feature = "private"))]
@@ -48,6 +44,18 @@ pub trait AuditAuthorable {
fn username(&self) -> &str;
fn email(&self) -> &str;
fn username_override(&self) -> Option<&str>;
fn token_prefix(&self) -> Option<&str> {
None
}
}
#[derive(Clone)]
#[cfg(not(feature = "private"))]
pub struct AuditAuthor {
pub username: String,
pub email: String,
pub username_override: Option<String>,
pub token_prefix: Option<String>,
}
#[cfg(not(feature = "private"))]
+1
View File
@@ -24,6 +24,7 @@ pub struct AuditLog {
pub action_kind: ActionKind,
pub resource: Option<String>,
pub parameters: Option<serde_json::Value>,
pub span: Option<String>,
}
#[derive(Deserialize)]
+1 -1
View File
@@ -83,7 +83,7 @@ strum_macros.workspace = true
url.workspace = true
semver.workspace = true
croner = "2.0.6"
croner = "2.2.0"
quick_cache.workspace = true
pin-project-lite.workspace = true
futures.workspace = true
+12 -3
View File
@@ -17,6 +17,8 @@ pub struct IdToken {
expiration: DateTime<Utc>,
}
pub const TOKEN_PREFIX_LEN: usize = 10;
pub fn has_expired(expiration_time: DateTime<Utc>, take: Option<Duration>) -> bool {
let now = Utc::now();
@@ -66,6 +68,7 @@ pub struct JWTAuthClaims {
pub exp: usize,
pub job_id: Option<String>,
pub scopes: Option<Vec<String>>,
pub audit_span: Option<String>,
}
#[derive(Deserialize, Debug)]
@@ -92,6 +95,7 @@ impl From<JobPerms> for Authed {
.filter_map(|x| serde_json::from_value::<(String, bool, bool)>(x).ok())
.collect(),
scopes: None,
token_prefix: None,
}
}
}
@@ -171,38 +175,41 @@ pub async fn fetch_authed_from_permissioned_as(
let folders = get_folders_for_user(w_id, &name, &groups, db).await?;
Ok(Authed {
email: email,
email,
username: name.to_string(),
is_admin,
is_operator,
groups,
folders,
scopes: None,
token_prefix: None,
})
} else {
let groups = vec![name.to_string()];
let folders = get_folders_for_user(&w_id, "", &groups, db).await?;
Ok(Authed {
email: email,
email,
username: format!("group-{name}"),
is_admin: false,
groups,
is_operator: false,
folders,
scopes: None,
token_prefix: None,
})
}
} else {
let groups = vec![];
let folders = vec![];
Ok(Authed {
email: email,
email,
username: permissioned_as,
is_admin: super_admin,
is_operator: true,
groups,
folders,
scopes: None,
token_prefix: None,
})
}
}
@@ -262,6 +269,7 @@ pub async fn create_token_for_owner(
email: &str,
job_id: &Uuid,
perms: Option<JobPerms>,
audit_span: Option<String>,
) -> crate::error::Result<String> {
let job_perms = if perms.is_some() {
Ok(perms)
@@ -302,6 +310,7 @@ pub async fn create_token_for_owner(
as usize,
job_id: Some(job_id.to_string()),
scopes: None,
audit_span,
};
let token = jwt::encode_with_internal_secret(&payload)
+1
View File
@@ -12,6 +12,7 @@ pub struct Authed {
// (folder name, can write, is owner)
pub folders: Vec<(String, bool, bool)>,
pub scopes: Option<Vec<String>>,
pub token_prefix: Option<String>,
}
#[derive(Clone)]
+78 -1
View File
@@ -10,6 +10,8 @@ use object_store::aws::AwsCredential;
#[cfg(feature = "parquet")]
use object_store::azure::MicrosoftAzureBuilder;
#[cfg(feature = "parquet")]
use object_store::gcp::GoogleCloudStorageBuilder;
#[cfg(feature = "parquet")]
use object_store::ObjectStore;
#[cfg(feature = "parquet")]
use object_store::{aws::AmazonS3Builder, ClientOptions};
@@ -221,6 +223,7 @@ pub enum LargeFileStorage {
AzureBlobStorage(AzureBlobStorage),
S3AwsOidc(S3Storage),
AzureWorkloadIdentity(AzureBlobStorage),
GoogleCloudStorage(GoogleCloudStorage),
// TODO: Add a filesystem type here in the future if needed
}
@@ -238,10 +241,18 @@ pub struct AzureBlobStorage {
pub public_resource: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GoogleCloudStorage {
pub gcs_resource_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_resource: Option<bool>,
}
#[derive(Clone, Debug)]
pub enum ObjectStoreResource {
S3(S3Resource),
Azure(AzureBlobResource),
Gcs(GcsResource),
}
impl ObjectStoreResource {
@@ -259,6 +270,7 @@ pub enum StorageResourceType {
AzureBlob,
S3AwsOidc,
AzureWorkloadIdentity,
GoogleCloudStorage,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -300,6 +312,22 @@ pub struct AzureBlobResource {
pub federated_token_file: Option<String>,
}
fn as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
serde_json::to_string(&v).map_err(serde::de::Error::custom)
}
#[derive(Debug, Deserialize, Clone)]
pub struct GcsResource {
pub bucket: String,
#[serde(rename = "serviceAccountKey")]
#[serde(deserialize_with = "as_string")]
pub service_account_key: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, Hash)]
pub struct S3AwsOidcResource {
#[serde(rename = "bucket")]
@@ -380,6 +408,7 @@ pub async fn build_object_store_client(
ObjectStoreResource::Azure(azure_blob_resource_ref) => {
build_azure_blob_client(&azure_blob_resource_ref)
}
ObjectStoreResource::Gcs(gcs_resource_ref) => build_gcs_client(&gcs_resource_ref).await,
}
}
@@ -575,18 +604,59 @@ fn build_azure_blob_client(
return Ok(Arc::new(store));
}
#[cfg(feature = "parquet")]
async fn build_gcs_client(gcs_resource_ref: &GcsResource) -> error::Result<Arc<dyn ObjectStore>> {
let gcs_resource = gcs_resource_ref.clone();
let mut store_builder = GoogleCloudStorageBuilder::new()
.with_client_options(
ClientOptions::new()
.with_timeout_disabled()
.with_default_headers(HeaderMap::from_iter(vec![(
"Accept-Encoding".parse().unwrap(),
"".parse().unwrap(),
)])),
)
.with_bucket_name(gcs_resource.bucket);
store_builder = store_builder.with_service_account_key(gcs_resource.service_account_key);
// if private key is malformed, it will panic => https://github.com/apache/arrow-rs-object-store/issues/419
let store = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| store_builder.build()))
.map_err(|panic_info| {
tracing::error!(
"Panic while building GCS object store client: {:?}",
panic_info
);
error::Error::internal_err(format!(
"Panic while building GCS object store client: {:?}",
panic_info
))
})?
.map_err(|err| {
tracing::error!("Error building GCS object store client: {:?}", err);
error::Error::internal_err(format!(
"Error building GCS object store client: {}",
err.to_string()
))
})?;
return Ok(Arc::new(store));
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "typ", content = "value")]
pub enum ObjectStoreSettings {
S3(S3Settings),
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum ObjectSettings {
S3(S3Settings),
Azure(AzureBlobResource),
AwsOidc(S3AwsOidcResource),
Gcs(GcsResource),
}
impl ObjectSettings {
@@ -595,6 +665,7 @@ impl ObjectSettings {
ObjectSettings::S3(s3_settings) => s3_settings.bucket.as_ref(),
ObjectSettings::Azure(azure_settings) => Some(&azure_settings.container_name),
ObjectSettings::AwsOidc(s3_aws_oidc_settings) => Some(&s3_aws_oidc_settings.bucket),
ObjectSettings::Gcs(gcs_settings) => Some(&gcs_settings.bucket),
}
}
}
@@ -628,6 +699,12 @@ pub async fn build_object_store_from_settings(
refresh: Some(ObjectStoreRefresh::new(settings.clone(), res.expiration())),
})
}
ObjectSettings::Gcs(gcs_settings) => {
let gcs_resource = gcs_settings;
build_gcs_client(&gcs_resource)
.await
.map(|x| ExpirableObjectStore::from(x))
}
}
}
+28 -18
View File
@@ -196,22 +196,9 @@ fn instance_name(hostname: &str) -> String {
}
const DEFAULT_WORKER_SUFFIX_LEN: usize = 5;
pub const SSH_AGENT_WORKER_SUFFIX: &'static str = "/ssh";
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize, ssh_ag_worker: bool) -> String {
let mut wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
if ssh_ag_worker {
wk_suffix.push_str(SSH_AGENT_WORKER_SUFFIX);
}
wk_suffix
}
pub fn create_ssh_agent_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, true)
}
pub fn create_default_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, false)
pub fn create_worker_suffix(hostname: &str) -> String {
format!("{}-{}", instance_name(hostname), rd_string(DEFAULT_WORKER_SUFFIX_LEN))
}
pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str) -> String {
@@ -752,16 +739,31 @@ pub trait WarnAfterExt: Future + Sized {
#[track_caller]
fn warn_after_seconds(self, seconds: u8) -> WarnAfterFuture<Self> {
let caller = Location::caller();
self.build_from_caller(seconds, caller, None)
}
fn build_from_caller(
self,
seconds: u8,
caller: &Location,
sql: Option<String>,
) -> WarnAfterFuture<Self> {
let location = format!("{}:{}", caller.file(), caller.line());
WarnAfterFuture {
future: self,
timeout: time::sleep(Duration::from_secs(seconds as u64)),
warned: false,
start_time: std::time::Instant::now(),
location: location,
location,
seconds,
sql,
}
}
#[track_caller]
fn warn_after_seconds_with_sql(self, seconds: u8, sql: String) -> WarnAfterFuture<Self> {
let caller = Location::caller();
self.build_from_caller(seconds, caller, Some(sql))
}
}
// Blanket implementation for all futures.
@@ -778,6 +780,7 @@ pin_project! {
location: String,
start_time: std::time::Instant,
seconds: u8,
sql: Option<String>,
}
}
@@ -787,13 +790,20 @@ impl<F: Future> Future for WarnAfterFuture<F> {
fn poll(self: Pin<&mut Self>, cx: &mut TContext<'_>) -> Poll<Self::Output> {
let this = self.project();
fn build_query_string(location: &str, sql: Option<&str>) -> String {
match sql {
Some(sql) => format!("{}: {}", location, sql),
None => location.to_string(),
}
}
// Poll the timeout future to check if it has elapsed.
if !*this.warned {
if this.timeout.poll(cx).is_ready() {
tracing::warn!(
location = this.location,
"SLOW_QUERY: query {} to db taking longer than expected (> {} seconds)",
this.location,
build_query_string(&this.location, this.sql.as_deref()),
this.seconds,
);
*this.warned = true;
@@ -808,7 +818,7 @@ impl<F: Future> Future for WarnAfterFuture<F> {
tracing::warn!(
location = this.location,
"SLOW_QUERY: completed query {} with total duration: {:.2?}",
this.location,
build_query_string(&this.location, this.sql.as_deref()),
elapsed
);
}
+10 -1
View File
@@ -428,6 +428,7 @@ pub async fn push_init_job<'c>(
worker_name,
"worker@windmill.dev",
SUPERADMIN_SECRET_EMAIL.to_string(),
Some("worker_init_job"),
None,
None,
None,
@@ -1212,6 +1213,7 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
&queued_job.created_by,
&queued_job.permissioned_as_email,
queued_job.permissioned_as.clone(),
Some(&format!("add.completed.job{}", queued_job.id)),
scheduled_for,
queued_job.schedule_path(),
None,
@@ -1735,6 +1737,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
},
email,
permissioned_as,
Some(&format!("error.handler.{job_id}")),
None,
None,
Some(job_id),
@@ -1842,6 +1845,7 @@ async fn handle_recovered_schedule<'a, 'c, T: Serialize + Send + Sync>(
SCHEDULE_RECOVERY_HANDLER_USERNAME,
email,
permissioned_as,
Some(&format!("recovered.schedule.{job_id}")),
None,
None,
Some(job_id),
@@ -1930,6 +1934,7 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>(
SCHEDULE_RECOVERY_HANDLER_USERNAME,
email,
permissioned_as,
Some(&format!("successful.schedule.recovery{job_id}")),
None,
None,
Some(job_id),
@@ -2220,6 +2225,7 @@ pub async fn create_token(db: &DB, job: &MiniPulledJob, perms: Option<JobPerms>)
&job.permissioned_as_email,
&job.id,
perms,
Some(format!("job-span-{}", job.flow_innermost_root_job.unwrap_or(job.id))),
)
.warn_after_seconds(5)
.await
@@ -3257,6 +3263,7 @@ pub async fn push<'c, 'd>(
user: &str,
mut email: &str,
mut permissioned_as: String,
token_prefix: Option<&str>,
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
schedule_path: Option<String>,
parent_job: Option<Uuid>,
@@ -4372,12 +4379,14 @@ pub async fn push<'c, 'd>(
email: email.to_string(),
username: permissioned_as.trim_start_matches("u/").to_string(),
username_override: Some(user.to_string()),
token_prefix: token_prefix.map(|s| s.to_string()),
}
} else {
AuditAuthor {
email: email.to_string(),
username: user.to_string(),
username_override: None,
token_prefix: token_prefix.map(|s| s.to_string()),
}
};
@@ -4700,4 +4709,4 @@ pub async fn get_same_worker_job(
same_worker_job.job_id, e
))
})
}
}
+1
View File
@@ -271,6 +271,7 @@ pub async fn push_scheduled_job<'c>(
&schedule_to_user(&schedule.path),
email,
permissioned_as,
Some(&schedule.path),
Some(next),
Some(schedule.path.clone()),
None,
+7
View File
@@ -775,6 +775,13 @@ async fn get_workspace_s3_resource_path(
resource_path.to_string(),
)
}
Some(LargeFileStorage::GoogleCloudStorage(gcs)) => {
let resource_path = gcs.gcs_resource_path.trim_start_matches("$res:");
(
StorageResourceType::GoogleCloudStorage,
resource_path.to_string(),
)
}
None => {
return Ok(None);
}
@@ -10,9 +10,9 @@ use windmill_queue::{CanceledBy, MiniPulledJob};
use serde::Deserialize;
use crate::common::build_args_map;
use crate::common::{build_http_client, resolve_job_timeout, OccupancyMetrics};
use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::common::build_args_map;
use windmill_common::client::AuthedClient;
#[derive(Deserialize)]
@@ -107,6 +107,20 @@ pub async fn do_graphql(
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
// Check HTTP status before processing response
if !response.status().is_success() {
let status = response.status();
let error_body = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(Error::ExecutionErr(format!(
"GraphQL request failed with HTTP {}: {}",
status.as_u16(),
error_body
)));
}
let result_stream = response.bytes_stream();
let mut i = 0;
@@ -17,12 +17,7 @@ use windmill_common::otel_oss::FutureExt;
use uuid::Uuid;
use windmill_common::{
add_time,
error::{self, Error},
jobs::JobKind,
utils::WarnAfterExt,
worker::{to_raw_value, Connection, WORKER_GROUP},
KillpillSender, DB,
add_time, error::{self, Error}, jobs::JobKind, utils::WarnAfterExt, worker::{to_raw_value, Connection, WORKER_GROUP}, KillpillSender, DB
};
#[cfg(feature = "benchmark")]
+5 -3
View File
@@ -367,6 +367,10 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse().ok())
.unwrap_or(false);
pub static ref OUTSTANDING_WAIT_TIME_THRESHOLD_MS: i64 = std::env::var("OUTSTANDING_WAIT_TIME_THRESHOLD_MS")
.ok()
.and_then(|x| x.parse::<i64>().ok())
.unwrap_or(1000);
}
type Envs = Vec<(String, String)>;
@@ -588,8 +592,6 @@ pub async fn drop_cache() {
}
}
const OUTSTANDING_WAIT_TIME_THRESHOLD_MS: i64 = 1000;
async fn insert_wait_time(
job_id: Uuid,
root_job_id: Option<Uuid>,
@@ -1620,7 +1622,7 @@ pub async fn run_worker(
.expect("send job completed END");
add_time!(bench, "sent job completed");
} else {
add_outstanding_wait_time(&conn, &job, OUTSTANDING_WAIT_TIME_THRESHOLD_MS);
add_outstanding_wait_time(&conn, &job, *OUTSTANDING_WAIT_TIME_THRESHOLD_MS);
#[cfg(feature = "prometheus")]
register_metric(
@@ -17,6 +17,7 @@ use crate::worker_utils::get_tag_and_concurrency;
use crate::{
JobCompletedSender, PreviousResult, SameWorkerSender, SendResult, UpdateFlow, KEEP_JOB_DIR,
};
use anyhow::Context;
use futures::TryFutureExt;
use mappable_rc::Marc;
@@ -2030,6 +2031,7 @@ async fn push_next_flow_job(
.to_string(),
email: flow_job.permissioned_as_email.clone(),
username_override: None,
token_prefix: Some(format!("psh.nxt.flowjob-{}", client.token.to_string())),
};
if can_be_resumed || disapproved_or_timeout_but_continue {
@@ -2772,6 +2774,7 @@ async fn push_next_flow_job(
&flow_job.created_by,
email,
permissioned_as,
Some(&format!("job-span-{}", flow_job.flow_innermost_root_job.unwrap_or(flow_job.id))),
scheduled_for_o,
flow_job.schedule_path(),
Some(flow_job.id),
@@ -608,6 +608,7 @@ async fn trigger_dependents_to_recompute_dependencies(
&created_by,
email,
permissioned_as.to_string(),
Some("trigger.dependents.to.recompute.dependencies"),
None,
None,
None,
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.501.3";
export const VERSION = "v1.502.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -63,7 +63,7 @@ export {
// }
// });
export const VERSION = "1.501.3";
export const VERSION = "1.502.2";
const command = new Command()
.name("wmill")
+2 -2
View File
@@ -1,8 +1,8 @@
FROM ghcr.io/windmill-labs/windmill:dev
# Rust
COPY --from=rust:1.86.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.86.0 /usr/local/rustup /usr/local/rustup
COPY --from=rust:1.88.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.88.0 /usr/local/rustup /usr/local/rustup
RUN /usr/local/cargo/bin/cargo install cargo-sweep --version ^0.7
# Ansible
+2 -2
View File
@@ -20,8 +20,8 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
FROM ghcr.io/windmill-labs/windmill-ee:dev
# Rust
COPY --from=rust:1.86.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.86.0 /usr/local/rustup /usr/local/rustup
COPY --from=rust:1.88.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.88.0 /usr/local/rustup /usr/local/rustup
RUN /usr/local/cargo/bin/cargo install cargo-sweep --version ^0.7
# Ansible
Generated
+16
View File
@@ -33,6 +33,21 @@
"type": "indirect"
}
},
"nixpkgs-claude": {
"locked": {
"lastModified": 1751271578,
"narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1736320768,
@@ -53,6 +68,7 @@
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-claude": "nixpkgs-claude",
"rust-overlay": "rust-overlay"
}
},
+11 -2
View File
@@ -3,9 +3,11 @@
nixpkgs.url = "nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
# Use separate channel for claude code. It always needs to be latest
nixpkgs-claude.url = "nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
outputs = { self, nixpkgs, nixpkgs-claude, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
@@ -13,6 +15,10 @@
config.allowUnfree = true;
overlays = [ (import rust-overlay) ];
};
claude-code = (import nixpkgs-claude {
inherit system;
config.allowUnfree = true;
}).claude-code;
lib = pkgs.lib;
stdenv = pkgs.stdenv;
rust = pkgs.rust-bin.stable.latest.default.override {
@@ -95,7 +101,10 @@
};
devShells.default = pkgs.mkShell {
buildInputs = buildInputs ++ (with pkgs; [
buildInputs = buildInputs ++ [
# To update run: `nix flake update nixpkgs-claude`
claude-code
] ++ (with pkgs; [
# Essentials
rust
cargo-watch
+1
View File
@@ -4,6 +4,7 @@ node_modules
/.svelte-kit
/package
/src/lib/gen
.vite.temp.*.js
CaddyfileRemoteRuben
tests-out/
storageState.json
+23 -12
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.501.3",
"version": "1.502.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.501.3",
"version": "1.502.5",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -46,6 +46,7 @@
"hash-sum": "^2.0.0",
"highlight.js": "^11.8.0",
"idb": "^8.0.2",
"lru-cache": "^11.1.0",
"lucide-svelte": "^0.399.0",
"minimatch": "^10.0.1",
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~16.1.1",
@@ -150,7 +151,7 @@
"fsevents": "^2.3.3"
},
"peerDependencies": {
"svelte": "^4.0.0"
"svelte": "^5.0.0"
}
},
"node_modules/@alloc/quick-lru": {
@@ -7050,6 +7051,20 @@
"node": ">=10"
}
},
"node_modules/hosted-git-info/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@@ -7822,16 +7837,12 @@
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"license": "ISC",
"engines": {
"node": ">=10"
"node": "20 || >=22"
}
},
"node_modules/ltgt": {
+46 -30
View File
@@ -1,11 +1,11 @@
{
"name": "windmill-components",
"version": "1.501.3",
"version": "1.502.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"postinstall": "node scripts/untar_ui_builder.js && node scripts/patch_files.js",
"postinstall": "if [ -f ./scripts/untar_ui_builder.js ]; then node ./scripts/untar_ui_builder.js && node ./scripts/patch_files.js; fi",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --threshold warning",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
@@ -91,9 +91,10 @@
"@redocly/json-to-json-schema": "^0.0.1",
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@xyflow/svelte": "^1.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^1.0.0",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -112,6 +113,7 @@
"hash-sum": "^2.0.0",
"highlight.js": "^11.8.0",
"idb": "^8.0.2",
"lru-cache": "^11.1.0",
"lucide-svelte": "^0.399.0",
"minimatch": "^10.0.1",
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~16.1.1",
@@ -133,7 +135,6 @@
"svelte-exmarkdown": "^5.0.0",
"svelte-infinite-loading": "^1.4.0",
"svelte-tiny-virtual-list": "^2.0.5",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"tailwind-merge": "^1.13.2",
"vscode": "npm:@codingame/monaco-vscode-extension-api@~16.1.1",
"vscode-languageclient": "~9.0.1",
@@ -160,7 +161,7 @@
"zod-to-json-schema": "^3.24.5"
},
"peerDependencies": {
"svelte": "^4.0.0"
"svelte": "^5.0.0"
},
"exports": {
"./package.json": "./package.json",
@@ -168,7 +169,16 @@
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
},
"./scripts/untar_ui_builder.js": "./scripts/untar_ui_builder.js",
"./assets/app.css": "./package/assets/app.css",
"./components/custom_ui": {
"types": "./package/components/custom_ui.d.ts",
"default": "./package/components/custom_ui.js"
},
"./components/scriptBuilder": {
"types": "./package/components/scriptBuilder.d.ts",
"default": "./package/components/scriptBuilder.js"
},
"./components/TestJobLoader.svelte": {
"types": "./package/components/TestJobLoader.svelte.d.ts",
"svelte": "./package/components/TestJobLoader.svelte",
@@ -254,20 +264,20 @@
"svelte": "./package/components/FlowStatusViewer.svelte",
"default": "./package/components/FlowStatusViewer.svelte"
},
"./components/FlowBuilder.svelte": {
"types": "./package/components/FlowBuilder.svelte.d.ts",
"svelte": "./package/components/FlowBuilder.svelte",
"default": "./package/components/FlowBuilder.svelte"
"./components/FlowWrapper.svelte": {
"types": "./package/components/FlowWrapper.svelte.d.ts",
"svelte": "./package/components/FlowWrapper.svelte",
"default": "./package/components/FlowWrapper.svelte"
},
"./components/AppEditor.svelte": {
"types": "./package/components/apps/editor/AppEditor.svelte.d.ts",
"svelte": "./package/components/apps/editor/AppEditor.svelte",
"default": "./package/components/apps/editor/AppEditor.svelte"
"./components/AppWrapper.svelte": {
"types": "./package/components/AppWrapper.svelte.d.ts",
"svelte": "./package/components/AppWrapper.svelte",
"default": "./package/components/AppWrapper.svelte"
},
"./components/ScriptBuilder.svelte": {
"types": "./package/components/ScriptBuilder.svelte.d.ts",
"svelte": "./package/components/ScriptBuilder.svelte",
"default": "./package/components/ScriptBuilder.svelte"
"./components/ScriptWrapper.svelte": {
"types": "./package/components/ScriptWrapper.svelte.d.ts",
"svelte": "./package/components/ScriptWrapper.svelte",
"default": "./package/components/ScriptWrapper.svelte"
},
"./components/FlowEditor.svelte": {
"types": "./package/components/flows/FlowEditor.svelte.d.ts",
@@ -284,10 +294,10 @@
"svelte": "./package/components/SchemaForm.svelte",
"default": "./package/components/SchemaForm.svelte"
},
"./components/EditableSchemaWrapper.svelte": {
"types": "./package/components/schema/EditableSchemaWrapper.svelte.d.ts",
"svelte": "./package/components/schema/EditableSchemaWrapper.svelte",
"default": "./package/components/schema/EditableSchemaWrapper.svelte"
"./components/EditableSchemaSdkWrapper.svelte": {
"types": "./package/components/schema/EditableSchemaSdkWrapper.svelte.d.ts",
"svelte": "./package/components/schema/EditableSchemaSdkWrapper.svelte",
"default": "./package/components/schema/EditableSchemaSdkWrapper.svelte"
},
"./components/ResourceEditor.svelte": {
"types": "./package/components/ResourceEditor.svelte.d.ts",
@@ -450,14 +460,14 @@
"components/FlowBuilder.svelte": [
"./package/components/FlowBuilder.svelte.d.ts"
],
"components/AppEditor.svelte": [
"./package/components/apps/editor/AppEditor.svelte.d.ts"
"components/AppWrapper.svelte": [
"./package/components/AppWrapper.svelte.d.ts"
],
"components/ScriptBuilder.svelte": [
"./package/components/ScriptBuilder.svelte.d.ts"
"components/ScriptWrapper.svelte": [
"./package/components/ScriptWrapper.svelte.d.ts"
],
"components/FlowEditor.svelte": [
"./package/components/flows/FlowEditor.svelte.d.ts"
"components/FlowWrapper.svelte": [
"./package/components/FlowWrapper.svelte.d.ts"
],
"components/SchemaViewer.svelte": [
"./package/components/SchemaViewer.svelte.d.ts"
@@ -465,8 +475,8 @@
"components/SchemaEditor.svelte": [
"./package/components/SchemaEditor.svelte.d.ts"
],
"components/EditableSchemaWrapper.svelte": [
"./package/components/schema/EditableSchemaWrapper.svelte.d.ts"
"components/EditableSchemaSdkWrapper.svelte": [
"./package/components/schema/EditableSchemaSdkWrapper.svelte.d.ts"
],
"components/flows/FlowHistoryInner.svelte": [
"./package/components/flows/FlowHistoryInner.svelte.d.ts"
@@ -518,6 +528,12 @@
],
"tailwindUtils": [
"./package/components/apps/editor/componentsPanel/tailwindUtils.d.ts"
],
"components/scriptBuilder": [
"./package/components/scriptBuilder.d.ts"
],
"components/custom_ui": [
"./package/components/custom_ui.d.ts"
]
}
},
@@ -525,4 +541,4 @@
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
"fsevents": "^2.3.3"
}
}
}
+17 -10
View File
@@ -1,6 +1,23 @@
import path from 'path'
import fs from 'fs'
// Check if we're in node_modules (installed as dependency)
if (process.cwd().includes('node_modules')) {
console.log('Skipping postinstall - running as dependency');
process.exit(0);
}
// Check if we're in the root project
if (process.env.INIT_CWD && process.env.INIT_CWD !== process.cwd()) {
console.log('Skipping postinstall - not root project');
process.exit(0);
}
// Your actual postinstall logic here
console.log('Running postinstall for root project');
import { x } from 'tar'
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-d44b577.tar.gz'
@@ -18,16 +35,6 @@ const response = await fetch(tarUrl)
const buffer = await response.arrayBuffer()
await fs.promises.writeFile(outputTarPath, Buffer.from(buffer))
// Check if this script is being run from the package root
const isRootInstall = process.cwd() + '/scripts' === __dirname
if (isRootInstall) {
console.log('Running postinstall: direct install')
// Your postinstall logic here
} else {
console.log('Skipping postinstall: installed as dependency')
process.exit(0)
}
// Create extract directory if it doesn't exist
try {
@@ -1,4 +1,4 @@
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
import { triggerablesByAi } from '$lib/components/copilot/chat/sharedChatState.svelte'
export interface TriggerableByAIOptions {
id?: string
@@ -60,15 +60,15 @@ export function triggerableByAI(element: HTMLElement, options: TriggerableByAIOp
// register the triggerable
const currentData = { description, onTrigger: handleTrigger }
aiChatManager.triggerablesByAI[id] = currentData
triggerablesByAi[id] = currentData
}
function unregister() {
if (isDisabled || !id) return
// unregister the triggerable
if (aiChatManager.triggerablesByAI[id]) {
delete aiChatManager.triggerablesByAI[id]
if (triggerablesByAi[id]) {
delete triggerablesByAi[id]
}
}
@@ -77,7 +77,7 @@ export function triggerableByAI(element: HTMLElement, options: TriggerableByAIOp
return {
update(newOptions: TriggerableByAIOptions) {
;({ id, description, callback, showAnimation = true } = newOptions)
; ({ id, description, callback, showAnimation = true } = newOptions)
register()
},
destroy() {
+17 -503
View File
@@ -1,29 +1,18 @@
<script lang="ts">
import {
OauthService,
GitSyncService,
type ResourceType,
type GetGlobalConnectedRepositoriesResponse
} from '$lib/gen'
import {
workspaceStore,
enterpriseLicense,
userWorkspaces,
userStore,
workspaceColor
} from '$lib/stores'
import { base } from '$lib/base'
import { OauthService, type ResourceType } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { emptySchema, emptyString } from '$lib/utils'
import SchemaForm from './SchemaForm.svelte'
import type SimpleEditor from './SimpleEditor.svelte'
import Toggle from './Toggle.svelte'
import TestConnection from './TestConnection.svelte'
import SupabaseIcon from './icons/SupabaseIcon.svelte'
import { sendUserToast } from '$lib/toast'
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte'
import { onDestroy, untrack } from 'svelte'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
import { base } from '$lib/base'
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
interface Props {
resourceType: string
@@ -50,109 +39,6 @@
let supabaseWizard = $state(false)
let loadingGithubInstallations = $state(false)
let githubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let selectedGHAppAccountId: string | undefined = $state(undefined)
let selectedGHAppRepository: string | undefined = $state(undefined)
let githubInstallationUrl: string | undefined = $state(undefined)
let installationCheckInterval: number | undefined = undefined
let isCheckingInstallation = $state(false)
let importJwt = $state('')
let githubAppPopover: { open: () => void; close: () => void } | null = $state(null)
async function loadGithubInstallations() {
if (!$enterpriseLicense) return
try {
loadingGithubInstallations = true
// Reset for reactivity
githubInstallations = []
workspaceGithubInstallations = []
const installations = await GitSyncService.getGlobalConnectedRepositories()
githubInstallations = installations
workspaceGithubInstallations = githubInstallations.filter(
(_) => _.workspace_id === $workspaceStore
)
const state = encodeURIComponent(
JSON.stringify({
workspace_id: $workspaceStore,
base_url: window.location.origin + base
})
)
githubInstallationUrl = `https://github.com/apps/windmill-sync-helper/installations/new?state=${state}`
} catch (err) {
console.error(err)
sendUserToast('Failed to load GitHub installations', true)
githubInstallations = []
workspaceGithubInstallations = []
} finally {
loadingGithubInstallations = false
}
}
function startInstallationCheck() {
isCheckingInstallation = true
installationCheckInterval = window.setInterval(async () => {
const installations = await GitSyncService.getGlobalConnectedRepositories()
if (installations.length > 0) {
stopInstallationCheck()
githubInstallations = installations
workspaceGithubInstallations = githubInstallations.filter(
(_) => _.workspace_id === $workspaceStore
)
// Open the popover with a small delay as otherwise it doesn't open
setTimeout(() => {
githubAppPopover?.open()
}, 100)
}
}, 2000)
}
function stopInstallationCheck() {
if (installationCheckInterval) {
clearInterval(installationCheckInterval)
installationCheckInterval = undefined
}
isCheckingInstallation = false
}
// Clean up interval when component is destroyed
onDestroy(() => {
stopInstallationCheck()
})
function getRepositories(accountId: string) {
return githubInstallations.find((_) => _.account_id === accountId)?.repositories || []
}
async function addInstallationToWorkspace(
installation_id: number | undefined,
workspaceId: string | undefined
) {
if (!installation_id || !workspaceId || !$workspaceStore) {
sendUserToast('Installation or workspace invalid', true)
return
}
try {
await GitSyncService.installFromWorkspace({
workspace: $workspaceStore,
requestBody: {
source_workspace_id: workspaceId,
installation_id: installation_id
}
})
sendUserToast('Successfully added installation to workspace', false)
await loadGithubInstallations()
} catch (err) {
console.error(err)
sendUserToast('Failed to add installation to workspace', true)
}
}
async function isSupabaseAvailable() {
try {
supabaseWizard =
@@ -227,22 +113,6 @@
}
}
function applyRepositoryURL(close: (_: any) => void) {
if (!selectedGHAppRepository) return
rawCode = JSON.stringify(
{
...args,
url: selectedGHAppRepository,
is_github_app: true
},
null,
2
)
description = `Repository ${selectedGHAppRepository} with permissions fetched using Windmill Github App. ${description ?? ''}`
rawCodeEditor?.setCode(rawCode)
close(null)
}
let rawCodeEditor: SimpleEditor | undefined = $state(undefined)
let textFileContent: string | undefined = $state(undefined)
@@ -251,79 +121,6 @@
content: textFileContent
}
}
async function deleteInstallation(installation_id: number) {
if (!$workspaceStore) {
sendUserToast('Failed to delete installation', true)
return
}
try {
await GitSyncService.deleteFromWorkspace({
workspace: $workspaceStore,
installationId: installation_id
})
sendUserToast('Successfully deleted installation', false)
await loadGithubInstallations()
} catch (err) {
console.error(err)
sendUserToast('Failed to delete installation', true)
}
}
async function exportInstallation(installationId: number) {
if (!$workspaceStore) {
sendUserToast('Failed to export installation', true)
return
}
try {
const response = await GitSyncService.exportInstallation({
workspace: $workspaceStore,
installationId: installationId
})
if (!response.jwt_token) {
sendUserToast('Failed to export installation', true)
return
}
// Copy to clipboard
await navigator.clipboard.writeText(response.jwt_token)
sendUserToast(
'JWT token copied to clipboard. This token is sensitive and should be kept secret!',
false,
undefined,
undefined,
10000
)
} catch (error) {
console.error(error)
sendUserToast('Failed to export installation', true)
}
}
async function importInstallation(jwt: string) {
if (!$workspaceStore) {
sendUserToast('Failed to import installation', true)
return
}
try {
await GitSyncService.importInstallation({
workspace: $workspaceStore,
requestBody: { jwt_token: jwt }
})
importJwt = ''
sendUserToast('Installation imported successfully', false)
await loadGithubInstallations()
} catch (error) {
sendUserToast('Failed to import installation', true)
}
}
function handleInstallClick() {
if (githubInstallations.length === 0) {
if (!isCheckingInstallation) {
startInstallationCheck()
}
}
}
$effect(() => {
$workspaceStore && untrack(() => loadSchema())
})
@@ -339,19 +136,6 @@
$effect(() => {
resourceType == 'postgresql' && untrack(() => isSupabaseAvailable())
})
$effect(() => {
resourceType == 'git_repository' &&
$userStore?.is_admin &&
untrack(() => loadGithubInstallations())
})
let githubInstallationsNotInWorkspace = $derived(
githubInstallations.filter((installation) => {
return !workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
})
)
</script>
{#if !notFound}
@@ -420,287 +204,17 @@
<div class="text-[#11181C] dark:text-[#EDEDED] font-semibold">Connect Supabase</div>
</a>
{/if}
{#if resourceType == 'git_repository' && $workspaceStore && $userStore?.is_admin}
{#if !loadingGithubInstallations}
<Button
color="light"
variant="contained"
size="xs"
on:click={loadGithubInstallations}
disabled={!$enterpriseLicense}
startIcon={{ icon: RotateCw }}
/>
{:else}
<Loader2 class="animate-spin w-10 h-4" />
{/if}
{#if githubInstallations.length > 0}
<Popover
documentationLink="https://www.windmill.dev/docs/integrations/git_repository#github-app"
bind:this={githubAppPopover}
floatingConfig={{
placement: 'bottom'
}}
disabled={!$enterpriseLicense || loadingGithubInstallations}
>
{#snippet trigger()}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || loadingGithubInstallations}
startIcon={{
icon: loadingGithubInstallations ? Loader2 : Github,
classes: loadingGithubInstallations ? 'animate-spin' : ''
}}
nonCaptureEvent
>
{$enterpriseLicense ? 'GitHub App' : 'GitHub App (ee only)'}
</Button>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="flex flex-col gap-4 w-[600px]">
{#if workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Select Repository</p>
<div class="flex flex-row gap-2 w-full">
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Github Account ID</p>
<select bind:value={selectedGHAppAccountId}>
<option value="" disabled>Select GitHub Account ID</option>
{#each workspaceGithubInstallations as installation}
<option value={installation.account_id}
>{installation.account_id}</option
>
{/each}
</select>
</div>
{#if selectedGHAppAccountId}
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Repository</p>
<div class="flex flex-row gap-2">
<select bind:value={selectedGHAppRepository}>
<option value="" disabled selected>Select Repository</option>
{#each getRepositories(selectedGHAppAccountId) as repository}
<option value={repository.url}>{repository.name}</option>
{/each}
</select>
</div>
</div>
{/if}
<div class="pt-[26px]">
<Button
size="xs"
color="blue"
buttonType="button"
disabled={!selectedGHAppRepository}
on:click={() => {
applyRepositoryURL(close)
}}
>
Apply
</Button>
</div>
</div>
</div>
{/if}
<div
class={`${
workspaceGithubInstallations.length > 0
? 'border-t border-gray-200 dark:border-gray-700'
: ''
} pt-4`}
>
<div class="flex flex-col gap-4">
<div class="flex">
<Button
color="none"
variant="border"
size="xs"
href={githubInstallationUrl}
startIcon={{ icon: Plus }}
target="_blank"
>
Add new installation
</Button>
</div>
{#if workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Current installations:</p>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each workspaceGithubInstallations as installation}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
{#if $workspaceColor}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
style="background-color: {$workspaceColor}20; color: {$workspaceColor}"
>
{installation.workspace_id}
</span>
{:else}
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
{/if}
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="py-2 text-right">
<div class="flex justify-end gap-1">
<Button
size="xs2"
color="blue"
title="Export installation to other instance"
startIcon={{ icon: Download }}
on:click={() =>
exportInstallation(installation.installation_id)}
>
Export
</Button>
<Button
size="xs2"
color="red"
title="Remove installation from workspace"
startIcon={{ icon: Minus }}
on:click={() =>
deleteInstallation(installation.installation_id)}
>
Remove
</Button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if githubInstallationsNotInWorkspace.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Installations in other workspaces:</p
>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each githubInstallationsNotInWorkspace as installation}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
{#if $userWorkspaces.find((w) => w.id === installation.workspace_id)?.color}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
style="background-color: {$userWorkspaces.find(
(w) => w.id === installation.workspace_id
)?.color}20; color: {$userWorkspaces.find(
(w) => w.id === installation.workspace_id
)?.color}"
>
{installation.workspace_id}
</span>
{:else}
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
{/if}
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="pl-8 py-2 text-right">
<Button
size="xs2"
color="blue"
title="Add installation to workspace"
startIcon={{ icon: Plus }}
on:click={() =>
addInstallationToWorkspace(
installation.installation_id,
installation.workspace_id
)}
>
Add to workspace
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</div>
</div>
<div class="mt-4 flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Import installation from other instance:</p
>
<div class="flex gap-2">
<input
type="text"
placeholder="Paste JWT token here"
bind:value={importJwt}
class="flex-1"
/>
<Button
color="blue"
on:click={() => importInstallation(importJwt)}
disabled={!importJwt}
>
Import
</Button>
</div>
</div>
</div>
</div>
{/snippet}
</Popover>
{:else}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || loadingGithubInstallations}
startIcon={{
icon: loadingGithubInstallations || isCheckingInstallation ? Loader2 : Github,
classes: loadingGithubInstallations || isCheckingInstallation ? 'animate-spin' : ''
}}
href={githubInstallationUrl}
target="_blank"
on:click={handleInstallClick}
>
{$enterpriseLicense
? isCheckingInstallation
? 'Waiting for installation...'
: 'Install GitHub App'
: 'GitHub App (ee only)'}
</Button>
{/if}
{/if}
<GitHubAppIntegration
{resourceType}
{args}
{description}
onArgsUpdate={(newArgs) => {
args = newArgs
rawCode = JSON.stringify(args, null, 2)
rawCodeEditor?.setCode(rawCode)
}}
onDescriptionUpdate={(newDescription) => (description = newDescription)}
/>
</div>
{:else}
<p class="italic text-tertiary text-xs mb-4"
@@ -0,0 +1,10 @@
<script lang="ts">
import AppEditor from './apps/editor/AppEditor.svelte'
import type { AppEditorProps } from './apps/types'
let { app: oldApp, ...props }: AppEditorProps = $props()
let app = $state(oldApp)
</script>
<AppEditor {app} {...props} />
+1 -1
View File
@@ -846,7 +846,7 @@
format={format ?? ''}
bind:value
bind:editor
on:clear={() => {
onClear={() => {
defaultValue = null
}}
{showSchemaExplorer}
@@ -0,0 +1,8 @@
<script lang="ts">
import ComponentInner from './apps/editor/component/ComponentInner.svelte'
// import AppPreview from './apps/editor/AppPreview.svelte'
// AppPreview
ComponentInner
</script>
@@ -4,9 +4,13 @@
import { autoPlacement } from '@floating-ui/core'
import ChangeInstanceUsernameInner from './ChangeInstanceUsernameInner.svelte'
export let email: string
export let username: string
export let isConflict = false
interface Props {
email: string
username: string
isConflict?: boolean
}
let { email, username, isConflict = false }: Props = $props()
</script>
<Popover
@@ -19,12 +23,12 @@
}}
closeButton
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color={isConflict ? 'red' : 'light'} size="xs" spacingSize="xs2" nonCaptureEvent={true}
>{isConflict ? 'Fix username conflict' : 'Change username'}</Button
>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<ChangeInstanceUsernameInner
{email}
{username}
@@ -32,5 +36,5 @@
on:close={() => close()}
on:renamed
/>
</svelte:fragment>
{/snippet}
</Popover>
@@ -1,9 +1,8 @@
<script lang="ts">
import { page } from '$app/stores'
import { workspaceStore } from '$lib/stores'
import ClipboardPanel from './details/ClipboardPanel.svelte'
$: url = `${$page.url.protocol}//${$page.url.hostname}/`
let url = $derived(`${window.location.protocol}//${window.location.hostname}/`)
</script>
<div>
+10 -5
View File
@@ -3,19 +3,24 @@
import { Button } from './common'
import { Clock } from 'lucide-svelte'
import Popover from './meltComponents/Popover.svelte'
interface Props {
children?: import('svelte').Snippet<[any]>
}
let { children }: Props = $props()
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }} closeButton>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: Clock }}>
Use simplified builder
</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<Section label="CRON Builder" wrapperClass="p-4">
<div class="flex flex-col w-72">
<slot {close} />
{@render children?.({ close })}
</div>
</Section>
</svelte:fragment>
{/snippet}
</Popover>
+209 -193
View File
@@ -10,23 +10,35 @@
import Select from './select/Select.svelte'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
import { untrack } from 'svelte'
export let schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
export let timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
export let disabled = false
export let validCRON = true
export let cronVersion: string = 'v2'
interface Props {
schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
disabled?: boolean
validCRON?: boolean
cronVersion?: string
}
let preview: string[] = []
let {
schedule = $bindable(),
timezone = $bindable(),
disabled = false,
validCRON = $bindable(true),
cronVersion = $bindable('v2')
}: Props = $props()
let preview: string[] = $state([])
// If the user has already entered a cron string, switching to the basic tab will override it.
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' = 'minute'
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' =
$state('minute')
let seconds = 30
let minutes = 30
let hours = 1
let seconds = $state(30)
let minutes = $state(30)
let hours = $state(1)
const daysOfMonthOptions: number[] = Array.from(Array(31).keys()).map((i) => i + 1)
let daysOfMonth: number[] = []
let daysOfMonth: number[] = $state([])
// let lastDayOfMonth = false
const monthsOfYearOptions: string[] = [
'January',
@@ -42,7 +54,7 @@
'November',
'December'
]
let monthsOfYear: string[] = []
let monthsOfYear: string[] = $state([])
const daysOfWeekOptions: string[] = [
'Sunday',
'Monday',
@@ -52,10 +64,8 @@
'Friday',
'Saturday'
]
let daysOfWeek: string[] = []
let UTCTime: string = ''
$: !emptyString(schedule) && handleScheduleInput(schedule, timezone)
let daysOfWeek: string[] = $state([])
let UTCTime: string = $state('')
async function handleScheduleInput(input: string, timezone: string): Promise<void> {
try {
@@ -73,9 +83,60 @@
}
}
let nschedule = ''
let nschedule = $state('')
$: {
function formatDate(timezone) {
try {
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
} catch (ee) {
sendUserToast(
`Invalid timezone: ${timezone}. Update your browser's timezone preference`,
true
)
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'Europe/Paris',
timeZoneName: 'short'
}).format
}
}
const items = Object.keys(timezones)
.map((key) => {
return Object.keys(timezones[key])
.map((subKey) => {
return {
value: subKey,
label: subKey,
group: timezones[key][subKey][1] as string
}
})
.flat()
})
.flat()
$effect(() => {
schedule
untrack(() => {
!emptyString(schedule) && handleScheduleInput(schedule, timezone)
})
})
$effect(() => {
// CRON string format
// sec min hour day of month month day of week year
// 0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2
@@ -133,65 +194,18 @@
} else if (executeEvery === 'day-week') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} * * ${s_daysOfWeek}`
}
}
$: dateFormatter = formatDate(timezone)
function formatDate(timezone) {
try {
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
} catch (ee) {
sendUserToast(
`Invalid timezone: ${timezone}. Update your browser's timezone preference`,
true
)
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'Europe/Paris',
timeZoneName: 'short'
}).format
}
}
const items = Object.keys(timezones)
.map((key) => {
return Object.keys(timezones[key])
.map((subKey) => {
return {
value: subKey,
label: subKey,
group: timezones[key][subKey][1] as string
}
})
.flat()
})
.flat()
})
let dateFormatter = $derived(formatDate(timezone))
</script>
<div class="w-full flex space-x-8">
<div class="w-full flex flex-col gap-4">
<Label label="Cron" class="font-semibold" primary={true}>
<svelte:fragment slot="error">
{#snippet error()}
{#if !validCRON}
<div class="text-red-600 text-xs"> Invalid cron syntax </div>
{/if}
</svelte:fragment>
{/snippet}
<div class="flex flex-row-reverse text-2xs text-tertiary -mt-1 hover:underline">
<a
class="text-tertiary"
@@ -231,143 +245,145 @@
{#if !disabled}
<div class="flex flex-row gap-2 mb-2">
<CronBuilder let:close>
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Execute schedule every</div>
<CronBuilder>
{#snippet children({ close })}
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Execute schedule every</div>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1 mb-2">
<select
{disabled}
name="execute_every"
id="execute_every"
bind:value={executeEvery}
>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1 mb-2">
<select
{disabled}
name="execute_every"
id="execute_every"
bind:value={executeEvery}
>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input {disabled} type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input {disabled} type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input {disabled} type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
</div>
</div>
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={monthsOfYear}
items={safeSelectItems(monthsOfYearOptions)}
placeholder="Every month"
/>
</div>
{/if}
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfWeek}
items={safeSelectItems(daysOfWeekOptions)}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfMonth}
items={safeSelectItems(daysOfMonthOptions)}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input {disabled} type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input {disabled} type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input {disabled} type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
{/if} -->
</div>
</div>
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={monthsOfYear}
items={safeSelectItems(monthsOfYearOptions)}
placeholder="Every month"
/>
</div>
<small>Schedule will only execute on valid calendar days</small>
{/if}
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfWeek}
items={safeSelectItems(daysOfWeekOptions)}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfMonth}
items={safeSelectItems(daysOfMonthOptions)}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div>
{/if} -->
</div>
<small>Schedule will only execute on valid calendar days</small>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At Time</small>
<input
{disabled}
type="time"
name="atUTCTime"
id="atUTCTime"
bind:value={UTCTime}
/>
</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Preview New Cron</div>
<div class="flex p-2 px-4 rounded-md bg-surface-secondary">
<span>{nschedule}</span>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At Time</small>
<input
{disabled}
type="time"
name="atUTCTime"
id="atUTCTime"
bind:value={UTCTime}
/>
</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Preview New Cron</div>
<div class="flex p-2 px-4 rounded-md bg-surface-secondary">
<span>{nschedule}</span>
</div>
</div>
</div>
<div class="mt-4">
<Button
color="dark"
size="xs"
on:click={() => {
schedule = nschedule
close()
}}
>
Set cron schedule
</Button>
</div>
<div class="mt-4">
<Button
color="dark"
size="xs"
on:click={() => {
schedule = nschedule
close()
}}
>
Set cron schedule
</Button>
</div>
{/snippet}
</CronBuilder>
<CronGen bind:schedule bind:cronVersion />
</div>
+3 -1
View File
@@ -187,7 +187,9 @@
<div class="flex gap-1 w-full justify-between items-center text-xs text-primary p-2">
<div>
<Popover>
<svelte:fragment slot="text">Download</svelte:fragment>
{#snippet text()}
Download
{/snippet}
<Button
startIcon={{ icon: Download }}
color="light"
+13 -6
View File
@@ -6,14 +6,21 @@
import DefaultTagsInner from './DefaultTagsInner.svelte'
export let defaultTagPerWorkspace: boolean | undefined = undefined
export let defaultTagWorkspaces: string[] = []
interface Props {
defaultTagPerWorkspace?: boolean | undefined
defaultTagWorkspaces?: string[]
}
let {
defaultTagPerWorkspace = $bindable(undefined),
defaultTagWorkspaces = $bindable([])
}: Props = $props()
let placement: 'bottom-end' | 'top-end' = 'bottom-end'
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: placement }} contentClasses="p-4">
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true}>
<div class="flex flex-row gap-1 items-center"
><Pen size={14} /> Default tags&nbsp;<Tooltip light
@@ -22,8 +29,8 @@
></div
>
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<DefaultTagsInner bind:defaultTagPerWorkspace bind:defaultTagWorkspaces />
</svelte:fragment>
{/snippet}
</Popover>
@@ -51,10 +51,12 @@
}
}}
{hideDropdown}
on:dropdownOpen={({ detail }) => (dropdownOpen = detail)}
on:dropdownOpen={({ detail }) => {
dropdownOpen = detail
}}
>
Deploy
<svelte:fragment slot="tooltip">
{#snippet tooltip()}
<div class="flex flex-row gap-2 w-80 p-4 bg-surface rounded-lg shadow-lg border z-[5001]">
<input
type="text"
@@ -76,5 +78,5 @@
Deploy
</Button>
</div>
</svelte:fragment>
{/snippet}
</Button>
+4 -5
View File
@@ -524,22 +524,21 @@
let workspace = $derived($page.url.searchParams.get('workspace') ?? undefined)
let themeDarkRaw = $derived($page.url.searchParams.get('activeColorTheme'))
let themeDark = $derived(themeDarkRaw == '2' || themeDarkRaw == '4')
$effect(() => {
$effect.pre(() => {
if (token) {
OpenAPI.WITH_CREDENTIALS = true
OpenAPI.TOKEN = token
untrack(() => loadUser())
}
})
$effect(() => {
$effect.pre(() => {
if (workspace) {
$workspaceStore = workspace
untrack(() => setupCopilotInfo())
}
})
$effect(() => {
$effect.pre(() => {
if (workspace && token) {
untrack(() => loadUser())
untrack(() => setupCopilotInfo())
}
})
$effect(() => {
@@ -13,7 +13,8 @@
Table2,
Braces,
Highlighter,
ArrowDownFromLine
ArrowDownFromLine,
Loader2
} from 'lucide-svelte'
import Portal from '$lib/components/Portal.svelte'
import DisplayResultControlBar from './DisplayResultControlBar.svelte'
@@ -34,7 +35,6 @@
import { convertJsonToCsv } from './table/tableUtils'
import Tooltip from './Tooltip.svelte'
import HighlightTheme from './HighlightTheme.svelte'
import PdfViewer from './display/PdfViewer.svelte'
import type { DisplayResultUi } from './custom_ui'
import { getContext, hasContext, createEventDispatcher, onDestroy } from 'svelte'
import { toJsonStr } from '$lib/utils'
@@ -634,10 +634,14 @@
</div>
{:else if !forceJson && resultKind === 'pdf'}
<div class="h-96 mt-2 border">
<PdfViewer
allowFullscreen
source="data:application/pdf;base64,{contentOrRootString(result.pdf)}"
/>
{#await import('$lib/components/display/PdfViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
allowFullscreen
source="data:application/pdf;base64,{contentOrRootString(result.pdf)}"
/>
{/await}
</div>
{:else if !forceJson && resultKind === 'plain'}<div class="h-full text-2xs"
><pre class="whitespace-pre-wrap"
@@ -809,18 +813,22 @@
</div>
{:else if result?.s3?.endsWith('.pdf')}
<div class="h-96 mt-2 border">
<PdfViewer
allowFullscreen
source="{`/api/w/${workspaceId}/${
appPath
? 'apps_u/download_s3_file/' + appPath
: 'job_helpers/load_image_preview'
}?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(result.s3)}` +
(result.storage ? `&storage=${result.storage}` : '')}{appPath &&
result.presigned
? `&${result.presigned}`
: ''}"
/>
{#await import('$lib/components/display/PdfViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
allowFullscreen
source="{`/api/w/${workspaceId}/${
appPath
? 'apps_u/download_s3_file/' + appPath
: 'job_helpers/load_image_preview'
}?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(result.s3)}` +
(result.storage ? `&storage=${result.storage}` : '')}{appPath &&
result.presigned
? `&${result.presigned}`
: ''}"
/>
{/await}
</div>
{/if}
{/if}
@@ -893,12 +901,16 @@
{/if}
{:else if s3object?.s3?.endsWith('.pdf')}
<div class="h-96 mt-2 border" data-interactive>
<PdfViewer
allowFullscreen
source={`/api/w/${workspaceId}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
s3object.s3
)}` + (s3object.storage ? `&storage=${s3object.storage}` : '')}
/>
{#await import('$lib/components/display/PdfViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
allowFullscreen
source={`/api/w/${workspaceId}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
s3object.s3
)}` + (s3object.storage ? `&storage=${s3object.storage}` : '')}
/>
{/await}
</div>
{/if}
{/each}
@@ -6,14 +6,27 @@
import type { DisplayResultUi } from './custom_ui'
import { createEventDispatcher } from 'svelte'
export let customUi: DisplayResultUi | undefined = undefined
export let filename: string | undefined = undefined
export let workspaceId: string | undefined = undefined
export let jobId: string | undefined = undefined
export let nodeId: string | undefined = undefined
export let base: string
export let result: any
export let disableTooltips: boolean = false
interface Props {
customUi?: DisplayResultUi | undefined
filename?: string | undefined
workspaceId?: string | undefined
jobId?: string | undefined
nodeId?: string | undefined
base: string
result: any
disableTooltips?: boolean
}
let {
customUi = undefined,
filename = undefined,
workspaceId = undefined,
jobId = undefined,
nodeId = undefined,
base,
result,
disableTooltips = false
}: Props = $props()
const dispatch = createEventDispatcher()
@@ -42,19 +55,19 @@
{/if}
{#if disableTooltips !== true}
<Popover documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering">
<svelte:fragment slot="text">
{#snippet text()}
The result renderer in Windmill supports rich display rendering, allowing you to customize
the display format of your results.
</svelte:fragment>
{/snippet}
<div>
<InfoIcon size={14} />
</div>
</Popover>
{/if}
<button on:click={() => copyToClipboard(toJsonStr(result))}>
<button onclick={() => copyToClipboard(toJsonStr(result))}>
<ClipboardCopy size={14} />
</button>
<button on:click={() => dispatch('open-drawer')}>
<button onclick={() => dispatch('open-drawer')}>
<Expand size={14} />
</button>
</div>
+57 -28
View File
@@ -1,4 +1,4 @@
<script context="module" lang="ts">
<script module lang="ts">
import { writable } from 'svelte/store'
const activeDropdown = writable<{ id: string | null; close: (() => void) | null }>({
id: null,
@@ -16,25 +16,49 @@
import ResolveOpen from '$lib/components/common/menu/ResolveOpen.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import { triggerableByAI } from '$lib/actions/triggerableByAI'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import { untrack } from 'svelte'
export let aiId: string | undefined = undefined
export let aiDescription: string | undefined = undefined
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
export let disabled = false
export let placement: Placement = 'bottom-end'
export let usePointerDownOutside = false
export let closeOnOtherDropdownOpen = true
export let fixedHeight = true
export let hidePopup = false
export let open = false
export let customWidth: number | undefined = undefined
export let customMenu = false
interface Props {
aiId?: string | undefined
aiDescription?: string | undefined
items?: Item[] | (() => Item[]) | (() => Promise<Item[]>)
disabled?: boolean
placement?: Placement
usePointerDownOutside?: boolean
closeOnOtherDropdownOpen?: boolean
fixedHeight?: boolean
hidePopup?: boolean
open?: boolean
customWidth?: number | undefined
customMenu?: boolean
class?: string | undefined
buttonReplacement?: import('svelte').Snippet
menu?: import('svelte').Snippet
}
let buttonEl: HTMLButtonElement | undefined = undefined
let {
aiId = undefined,
aiDescription = undefined,
items = [],
disabled = false,
placement = 'bottom-end',
usePointerDownOutside = false,
closeOnOtherDropdownOpen = true,
fixedHeight = true,
hidePopup = false,
open = $bindable(false),
customWidth = undefined,
customMenu = false,
class: classNames = undefined,
buttonReplacement,
menu
}: Props = $props()
let buttonEl: HTMLButtonElement | undefined = $state(undefined)
const {
elements: { menu, item, trigger },
elements: { menu: menuEl, item, trigger },
states,
ids: { menu: dropdownId }
} = createDropdownMenu({
@@ -60,7 +84,12 @@
})
const sync = createSync(states)
$: sync.open(open, (v) => (open = Boolean(v)))
$effect(() => {
open
untrack(() => {
sync.open(open, (v) => (open = Boolean(v)))
})
})
export function close() {
open = false
@@ -88,25 +117,25 @@
description: aiDescription,
callback: () => buttonEl?.click()
}}
class={twMerge('w-full flex items-center justify-end', fixedHeight && 'h-8', $$props.class)}
class={twMerge('w-full flex items-center justify-end', fixedHeight && 'h-8', classNames)}
use:melt={$trigger}
{disabled}
on:click={(e) => e.stopPropagation()}
onclick={(e) => e.stopPropagation()}
use:pointerDownOutside={{
capture: true,
stopPropagation: false,
exclude: getMenuElements,
customEventName: 'pointerdown_menu'
}}
on:pointerdown_outside={() => {
if (usePointerDownOutside) {
close()
customEventName: 'pointerdown_menu',
onClickOutside: () => {
if (usePointerDownOutside) {
close()
}
}
}}
data-menu
>
{#if $$slots.buttonReplacement}
<slot name="buttonReplacement" />
{#if buttonReplacement}
{@render buttonReplacement?.()}
{:else}
<Button
nonCaptureEvent
@@ -119,9 +148,9 @@
</button>
{#if open && !hidePopup}
<div use:melt={$menu} data-menu class="z-[6000] transition-all duration-100">
<div use:melt={$menuEl} data-menu class="z-[6000] transition-all duration-100">
{#if customMenu}
<slot name="menu" />
{@render menu?.()}
{:else}
<div
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
@@ -1,5 +1,5 @@
<script lang="ts">
import { MenuItem } from '$lib/components/meltComponents'
import MenuItem from '$lib/components/meltComponents/MenuItem.svelte'
import { Loader2 } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import type { MenubarMenuElements } from '@melt-ui/svelte'
@@ -29,8 +29,9 @@
<div class="flex flex-col">
{#each computedItems ?? [] as item}
<MenuItem
on:click={(e) => item?.action?.(e)}
onClick={(e) => item?.action?.(e)}
href={item?.href}
target={item?.hrefTarget}
disabled={item?.disabled}
class={twMerge(
'px-4 py-2 text-primary font-semibold hover:bg-surface-hover cursor-pointer text-xs transition-all w-full',
+16 -70
View File
@@ -105,15 +105,10 @@
vimMode
} from '$lib/stores'
import {
createHash as randomHash,
editorConfig,
langToExt,
updateOptions,
extToLang
} from '$lib/editorUtils'
import { editorConfig, updateOptions } from '$lib/editorUtils'
import { createHash as randomHash } from '$lib/editorLangUtils'
import { workspaceStore } from '$lib/stores'
import { type Preview, ResourceService, UserService } from '$lib/gen'
import { type Preview, ResourceService, type ScriptLang, UserService } from '$lib/gen'
import type { Text } from 'yjs'
import { initializeVscode, keepModelAroundToAvoidDisposalOfWorkers } from '$lib/components/vscode'
@@ -149,13 +144,14 @@
import * as htmllang from '$lib/svelteMonarch'
import { conf, language } from '$lib/vueMonarch'
import { Autocompletor } from './copilot/autocomplete/monaco-adapter'
import { Autocompletor } from './copilot/autocomplete/Autocompletor'
import { AIChatEditorHandler } from './copilot/chat/monaco-adapter'
import GlobalReviewButtons from './copilot/chat/GlobalReviewButtons.svelte'
import { writable } from 'svelte/store'
import { formatResourceTypes } from './copilot/chat/script/core'
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
import { editorPositionMap } from '$lib/utils'
import { extToLang, langToExt } from '$lib/editorLangUtils'
// import EditorTheme from './EditorTheme.svelte'
let divEl: HTMLDivElement | null = null
@@ -650,79 +646,29 @@
}
}
$: $reviewingChanges && autocompletor?.reject()
let completorDisposable: IDisposable | undefined = undefined
let autocompletor: Autocompletor | undefined = undefined
function addSuperCompletor(editor: meditor.IStandaloneCodeEditor) {
try {
if (completorDisposable) {
completorDisposable.dispose()
}
if (!scriptLang) {
throw new Error('No script lang')
}
autocompletor = new Autocompletor(editor, lang, scriptLang)
// last user events (currently disabled):
// let lastTs = Date.now()
// editor.onDidChangeModelContent((e) => {
// const thisTs = Date.now()
// lastTs = thisTs
// setTimeout(() => {
// if (thisTs === lastTs) {
// autocompletor?.savePatch()
// }
// }, 150)
// })
completorDisposable = editor.onDidChangeCursorPosition((e) => {
autocompletor?.reject()
if ($reviewingChanges) {
return
}
const position = editor.getPosition()
if (!position) {
return
}
const upToText = editor.getModel()?.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 0,
endLineNumber: position.lineNumber,
endColumn: position.column
})
const lastChar = upToText ? upToText[upToText.length - 1] : ''
if (lastChar && lastChar.match(/[\(\{\s:="',]/)) {
autocompletor?.predict()
}
})
editor.onKeyDown((e) => {
if (e.keyCode === KeyCode.Escape) {
autocompletor?.reject()
} else if (e.keyCode === KeyCode.Tab && autocompletor?.hasChanges()) {
e.preventDefault()
e.stopPropagation()
autocompletor?.accept()
autocompletor?.predict()
}
})
} catch (err) {
console.error('Could not add supercompletor', err)
function addAutoCompletor(
editor: meditor.IStandaloneCodeEditor,
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
) {
if (autocompletor) {
autocompletor.dispose()
}
autocompletor = new Autocompletor(editor, scriptLang)
}
$: $copilotInfo.enabled &&
$copilotInfo.codeCompletionModel &&
$codeCompletionSessionEnabled &&
Autocompletor.isProviderModelSupported($copilotInfo.codeCompletionModel) &&
initialized &&
editor &&
scriptLang &&
addSuperCompletor(editor)
addAutoCompletor(editor, scriptLang)
$: $copilotInfo.enabled && initialized && editor && addChatHandler(editor)
$: !$codeCompletionSessionEnabled && (completorDisposable?.dispose(), autocompletor?.reject())
$: !$codeCompletionSessionEnabled && autocompletor?.dispose()
const outputChannel = {
name: 'Language Server Client',
@@ -1525,7 +1471,7 @@
disposeMethod && disposeMethod()
websocketInterval && clearInterval(websocketInterval)
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
completorDisposable && completorDisposable.dispose()
autocompletor && autocompletor.dispose()
sqlTypeCompletor && sqlTypeCompletor.dispose()
timeoutModel && clearTimeout(timeoutModel)
loadTimeout && clearTimeout(loadTimeout)
+1 -1
View File
@@ -719,7 +719,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
{/if}
{#if customUi?.assistants != false}
{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash' || lang == 'nu'}
{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash'}
<Button
aiId="editor-bar-reload-assistants"
aiDescription="Reload assistants"
@@ -478,7 +478,7 @@
>{teams_team_name}</Badge
>
</p>
<Tooltip text={teams_team_name}>
<Tooltip>
Each workspace can only be connected to one Microsoft Teams team. You can configure it under <a
target="_blank"
href="{base}/workspace_settings?tab=teams">workspace settings</a
@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, getContext, untrack } from 'svelte'
import { getFirstStepSchema } from '$lib/components/flows/flowStore.svelte'
import { getFirstStepSchema } from '$lib/components/flows/flowStore'
import type { FlowEditorContext } from '$lib/components/flows/types'
import { twMerge } from 'tailwind-merge'
import { Alert } from '$lib/components/common'
+56 -63
View File
@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import {
FlowService,
type Flow,
@@ -26,7 +24,6 @@
orderedJsonStringify,
readFieldsRecursively,
replaceFalseWithUndefined,
type StateStore,
type Value
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
@@ -35,29 +32,34 @@
import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte'
import { onMount, setContext, untrack, type ComponentType } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import { writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Badge, Button, UndoRedo } from './common'
import FlowEditor from './flows/FlowEditor.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import type { FlowState } from './flows/flowState'
import { dfs as dfsApply } from './flows/dfs'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
import { cleanInputs } from './flows/utils'
import { Calendar, Pen, Save, DiffIcon, HistoryIcon, FileJson, type Icon } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import {
Calendar,
Pen,
Save,
DiffIcon,
HistoryIcon,
FileJson,
type Icon,
Settings
} from 'lucide-svelte'
import Awareness from './Awareness.svelte'
import { getAllModules } from './flows/flowExplorer'
import { type FlowCopilotContext } from './copilot/flow'
import FlowAIButton from './copilot/chat/flow/FlowAIButton.svelte'
import { loadFlowModuleState } from './flows/flowStateUtils.svelte'
import FlowBuilderTutorials from './FlowBuilderTutorials.svelte'
import Dropdown from '$lib/components/DropdownV2.svelte'
import FlowTutorials from './FlowTutorials.svelte'
import { ignoredTutorials } from './tutorials/ignoredTutorials'
import type DiffDrawer from './DiffDrawer.svelte'
import FlowHistory from './flows/FlowHistory.svelte'
import Summary from './Summary.svelte'
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
@@ -80,33 +82,7 @@
StepHistoryLoader,
type stepState
} from './stepHistoryLoader.svelte'
interface Props {
initialPath?: string
pathStoreInit?: string | undefined
newFlow: boolean
selectedId: string | undefined
initialArgs?: Record<string, any>
loading?: boolean
flowStore: StateStore<OpenFlow>
flowStateStore: Writable<FlowState>
savedFlow?: FlowWithDraftAndDraftTriggers | undefined
diffDrawer?: DiffDrawer | undefined
customUi?: FlowBuilderWhitelabelCustomUi
disableAi?: boolean
disabledFlowInputs?: boolean
savedPrimarySchedule?: ScheduleTrigger | undefined // used to set the primary schedule in the legacy primaryScheduleStore
version?: number | undefined
setSavedraftCb?: ((cb: () => void) => void) | undefined
draftTriggersFromUrl?: Trigger[] | undefined
selectedTriggerIndexFromUrl?: number | undefined
children?: import('svelte').Snippet
loadedFromHistoryFromUrl?: {
flowJobInitial: boolean | undefined
stepsState: Record<string, stepState>
}
noInitial?: boolean
}
import type { FlowBuilderProps } from './flow_builder'
let {
initialPath = $bindable(''),
@@ -129,8 +105,16 @@
selectedTriggerIndexFromUrl = undefined,
children,
loadedFromHistoryFromUrl,
noInitial = false
}: Props = $props()
noInitial = false,
onSaveInitial,
onSaveDraft,
onDeploy,
onDeployError,
onDetails,
onSaveDraftError,
onSaveDraftOnlyAtNewPath,
onHistoryRestore
}: FlowBuilderProps = $props()
let initialPathStore = writable(initialPath)
@@ -139,7 +123,7 @@
'u/' +
($userStore?.username?.includes('@')
? $userStore!.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
: $userStore!.username!) +
: $userStore?.username) +
'/' +
generateRandomString(12)
@@ -211,8 +195,6 @@
}
}
const dispatch = createEventDispatcher()
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // kept for legacy reasons
const triggersCount = writable<TriggersCount | undefined>(undefined)
const simplifiedPoll = writable(false)
@@ -343,18 +325,18 @@
let savedAtNewPath = false
if (newFlow) {
dispatch('saveInitial', $pathStore)
onSaveInitial?.({ path: $pathStore, id: getSelectedId() })
} else if (savedFlow?.draft_only && $pathStore !== initialPath) {
savedAtNewPath = true
initialPath = $pathStore
onSaveDraftOnlyAtNewPath?.({ path: $pathStore, selectedId: getSelectedId() })
// this is so we can use the flow builder outside of sveltekit
dispatch('saveDraftOnlyAtNewPath', { path: $pathStore, selectedId: getSelectedId() })
}
dispatch('saveDraft', { path: $pathStore, savedAtNewPath, newFlow })
onSaveDraft?.({ path: $pathStore, savedAtNewPath, newFlow })
sendUserToast('Saved as draft')
} catch (error) {
sendUserToast(`Error while saving the flow as a draft: ${error.body || error.message}`, true)
dispatch('saveDraftError', error)
onSaveDraftError?.({ error })
}
loadingDraft = false
}
@@ -524,9 +506,10 @@
} as Flow
setDraftTriggers([])
loadingSave = false
dispatch('deploy', $pathStore)
onDeploy?.({ path: $pathStore })
} catch (err) {
dispatch('deployError', err)
onDeployError?.({ error: err })
// this is so we can use the flow builder outside of sveltekit
sendUserToast(`The flow could not be saved: ${err.body ?? err}`, true)
loadingSave = false
}
@@ -720,7 +703,7 @@
if (savedFlow?.draft_only === false || savedFlow?.draft_only === undefined) {
dropdownItems.push({
label: 'Exit & see details',
onClick: () => dispatch('details', $pathStore)
onClick: () => onDetails?.({ path: $pathStore })
})
}
@@ -789,7 +772,7 @@
}
]
: []),
...(customUi?.topBar?.history != false
...(customUi?.topBar?.export != false
? [
{
displayName: 'Export',
@@ -803,6 +786,17 @@
disabled: hasAiDiff
}
]
: []),
...(customUi?.topBar?.settings != false
? [
{
displayName: 'Flow settings',
icon: Settings,
action: () => {
select('settings-metadata')
}
}
]
: [])
]
}
@@ -824,28 +818,28 @@
let forceTestTab: Record<string, boolean> = $state({})
let highlightArg: Record<string, string | undefined> = $state({})
run(() => {
$effect.pre(() => {
initialPathStore.set(initialPath)
})
run(() => {
$effect.pre(() => {
setContext('customUi', customUi)
})
run(() => {
$effect.pre(() => {
if (flowStore.val || $selectedIdStore) {
readFieldsRecursively(flowStore.val)
untrack(() => saveSessionDraft())
}
})
run(() => {
$effect.pre(() => {
initialPath && ($pathStore = initialPath)
})
run(() => {
$effect.pre(() => {
selectedId && untrack(() => select(selectedId))
})
run(() => {
$effect.pre(() => {
initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers())
})
run(() => {
$effect.pre(() => {
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
customUi && untrack(() => onCustomUiChange(customUi, hasAiDiff))
})
@@ -890,8 +884,8 @@
{@render children?.()}
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
{deployedBy}
{confirmCallback}
bind:open
{diffDrawer}
bind:deployedValue
@@ -913,7 +907,7 @@
{#key renderCount}
{#if !$userStore?.operator}
{#if $pathStore}
<FlowHistory bind:this={flowHistory} path={$pathStore} on:historyRestore />
<FlowHistory bind:this={flowHistory} path={$pathStore} {onHistoryRestore} />
{/if}
<FlowYamlEditor bind:drawer={yamlEditorDrawer} />
<FlowImportExportMenu bind:drawer={jsonViewerDrawer} />
@@ -1068,9 +1062,6 @@
</div>
</Button>
{/if}
{#if !disableAi && customUi?.topBar?.aiBuilder != false && !aiChatManager.open}
<FlowAIButton openPanel={() => aiChatManager.openChat()} />
{/if}
<FlowPreviewButtons
on:openTriggers={(e) => {
select('triggers')
@@ -1103,7 +1094,6 @@
/>
</div>
</div>
<!-- metadata -->
{#if $flowStateStore}
<FlowEditor
@@ -1143,6 +1133,9 @@
}}
{forceTestTab}
{highlightArg}
aiChatOpen={aiChatManager.open}
showFlowAiButton={!disableAi && customUi?.topBar?.aiBuilder != false}
toggleAiChat={() => aiChatManager.toggleOpen()}
onRunPreview={() => {
flowPreviewButtons?.openPreview(true)
}}
@@ -7,8 +7,8 @@
import FlowTutorials from './FlowTutorials.svelte'
import { tutorialsToDo } from '$lib/stores'
let targetTutorial: string | undefined = undefined
let flowTutorials: FlowTutorials | undefined = undefined
let targetTutorial: string | undefined = $state(undefined)
let flowTutorials: FlowTutorials | undefined = $state(undefined)
async function getTutorialItems() {
const tutorials = [
@@ -42,7 +42,7 @@
{#key $tutorialsToDo}
<Dropdown items={getTutorialItems} class="w-fit">
<svelte:fragment slot="buttonReplacement">
{#snippet buttonReplacement()}
<Button
nonCaptureEvent
size="xs"
@@ -51,7 +51,7 @@
id="tutorials-button"
startIcon={{ icon: BookOpen }}
/>
</svelte:fragment>
{/snippet}
</Dropdown>
{/key}
@@ -11,10 +11,11 @@
import { base } from '$lib/base'
import FlowJobResult from './FlowJobResult.svelte'
import FlowPreviewStatus from './preview/FlowPreviewStatus.svelte'
import DisplayResult from './DisplayResult.svelte'
import { createEventDispatcher, getContext, tick } from 'svelte'
import { onDestroy } from 'svelte'
import { Badge, Button, Skeleton, Tab } from './common'
import DisplayResult from './DisplayResult.svelte'
import Tabs from './common/tabs/Tabs.svelte'
import { type DurationStatus, type FlowStatusViewerContext, type GraphModuleState } from './graph'
import ModuleStatus from './ModuleStatus.svelte'
+33 -21
View File
@@ -1,40 +1,52 @@
<script lang="ts">
import { debounce, displayDate, msToSec } from '$lib/utils'
import { onDestroy } from 'svelte'
import { onDestroy, untrack } from 'svelte'
import { getDbClockNow } from '$lib/forLater'
import { Loader2 } from 'lucide-svelte'
import TimelineBar from './TimelineBar.svelte'
import type { Writable } from 'svelte/store'
import WaitTimeWarning from './common/waitTimeWarning/WaitTimeWarning.svelte'
export let selfWaitTime: number | undefined = undefined
export let aggregateWaitTime: number | undefined = undefined
export let flowModules: string[]
export let durationStatuses: Writable<
Record<
string,
{
byJob: Record<string, { created_at?: number; started_at?: number; duration_ms?: number }>
iteration_from?: number
iteration_total?: number
}
interface Props {
selfWaitTime?: number | undefined
aggregateWaitTime?: number | undefined
flowModules: string[]
durationStatuses: Writable<
Record<
string,
{
byJob: Record<string, { created_at?: number; started_at?: number; duration_ms?: number }>
iteration_from?: number
iteration_total?: number
}
>
>
>
export let flowDone = false
flowDone?: boolean
}
let min: undefined | number = undefined
let max: undefined | number = undefined
let total: number | undefined = undefined
let {
selfWaitTime = undefined,
aggregateWaitTime = undefined,
flowModules,
durationStatuses,
flowDone = false
}: Props = $props()
let min: undefined | number = $state(undefined)
let max: undefined | number = $state(undefined)
let total: number | undefined = $state(undefined)
let items:
| Record<
string,
Array<{ created_at?: number; started_at?: number; duration_ms?: number; id: string }>
>
| undefined = undefined
| undefined = $state(undefined)
let { debounced, clearDebounce } = debounce(() => computeItems($durationStatuses), 30)
$: flowDone != undefined && $durationStatuses && debounced()
$effect(() => {
flowDone != undefined && $durationStatuses && untrack(() => debounced())
})
export function reset() {
min = undefined
@@ -104,7 +116,7 @@
}
}
let now = getDbClockNow().getTime()
let now = $state(getDbClockNow().getTime())
let interval = setInterval((x) => {
if (!max) {
@@ -165,7 +177,7 @@
<div class="w-full flex flex-row-reverse sticky top-0">
<button
class="!text-secondary underline mr-2 text-2xs text-right whitespace-nowrap"
on:click={() => {
onclick={() => {
let r = $durationStatuses[k]
if (r.iteration_from) {
r.iteration_from -= 20
@@ -6,11 +6,12 @@
import FlowBuilderTutorialForLoop from './tutorials/FlowBuilderTutorialForLoop.svelte'
import FlowBuilderTutorialErrorHandler from './tutorials/FlowBuilderTutorialErrorHandler.svelte'
let flowBuilderTutorialSimpleFlow: FlowBuilderTutorialSimpleFlow | undefined = undefined
let flowBuilderTutorialForLoop: FlowBuilderTutorialForLoop | undefined = undefined
let flowBuilderTutorialBranchOne: FlowBuilderTutorialBranchOne | undefined = undefined
let flowBuilderTutorialBranchAll: FlowBuilderTutorialBranchAll | undefined = undefined
let flowBuilderTutorialErrorHandler: FlowBuilderTutorialErrorHandler | undefined = undefined
let flowBuilderTutorialSimpleFlow: FlowBuilderTutorialSimpleFlow | undefined = $state(undefined)
let flowBuilderTutorialForLoop: FlowBuilderTutorialForLoop | undefined = $state(undefined)
let flowBuilderTutorialBranchOne: FlowBuilderTutorialBranchOne | undefined = $state(undefined)
let flowBuilderTutorialBranchAll: FlowBuilderTutorialBranchAll | undefined = $state(undefined)
let flowBuilderTutorialErrorHandler: FlowBuilderTutorialErrorHandler | undefined =
$state(undefined)
export function runTutorialById(id: string, indexToInsertAt?: number | undefined) {
if (id === 'forloop') {
@@ -0,0 +1,13 @@
<script lang="ts">
import AiChatLayout from './copilot/chat/AiChatLayout.svelte'
import type { FlowBuilderProps } from './flow_builder'
import FlowBuilder from './FlowBuilder.svelte'
let { flowStore: oldFlowStore, disableAi, ...props }: FlowBuilderProps = $props()
let flowStore = $state(oldFlowStore)
</script>
<AiChatLayout noPadding={true} {disableAi}>
<FlowBuilder {flowStore} {disableAi} {...props} />
</AiChatLayout>
+127 -118
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import {
type Folder,
@@ -21,18 +23,22 @@
import Select from './select/Select.svelte'
import { safeSelectItems } from './select/utils.svelte'
export let name: string
let can_write = false
interface Props {
name: string
}
let { name }: Props = $props()
let can_write = $state(false)
type Role = 'viewer' | 'writer' | 'admin'
let folder: Folder | undefined
let perms: { owner_name: string; role: Role }[] | undefined = undefined
let usernames: string[] = []
let groups: string[] = []
let ownerItem: string = ''
let perms: { owner_name: string; role: Role }[] | undefined = $state(undefined)
let usernames: string[] = $state([])
let groups: string[] = $state([])
let ownerItem: string = $state('')
let newGroup: Drawer
let viewGroup: Drawer
let newGroup: Drawer | undefined = $state(undefined)
let viewGroup: Drawer | undefined = $state(undefined)
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
@@ -42,12 +48,6 @@
groups = await GroupService.listGroupNames({ workspace: $workspaceStore! })
}
$: {
if ($workspaceStore && $userStore) {
load()
}
}
async function load() {
loadUsernames()
loadGroups()
@@ -67,7 +67,7 @@
loadFolder()
}
let folderNotFound: boolean | undefined = undefined
let folderNotFound: boolean | undefined = $state(undefined)
async function loadFolder(): Promise<void> {
try {
@@ -111,10 +111,10 @@
}
}
let ownerKind: 'user' | 'group' = 'user'
let groupCreated: string | undefined = undefined
let newGroupName: string = ''
let summary: string = ''
let ownerKind: 'user' | 'group' = $state('user')
let groupCreated: string | undefined = $state(undefined)
let newGroupName: string = $state('')
let summary: string = $state('')
async function addGroup() {
await GroupService.createGroup({
@@ -139,13 +139,18 @@
dispatch('update')
loadFolder()
}
run(() => {
if ($workspaceStore && $userStore) {
load()
}
})
</script>
<Drawer bind:this={newGroup}>
<DrawerContent
title="New Group"
on:close={() => {
newGroup.closeDrawer()
newGroup?.closeDrawer()
groupCreated = undefined
}}
>
@@ -180,7 +185,7 @@
<Section label={`Permissions (${perms?.length ?? 0})`}>
<div class="flex flex-col gap-6">
{#if can_write}
<Alert role="info" title="New permissions may take up to 60s to apply">
<Alert type="info" title="New permissions may take up to 60s to apply">
<span class="text-xs text-tertiary">Due to permissions cache invalidation </span>
</Alert>
<div class="flex items-center gap-1">
@@ -260,116 +265,120 @@
{/if}
{#if perms}
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user/group</th>
<th></th>
<th></th>
</tr>
<tbody slot="body">
{#each perms as { owner_name, role }}<tr>
<td>{owner_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username && !$userStore?.is_admin}
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
// const inAcl = (
// folder?.extra_perms ? Object.keys(folder?.extra_perms) : []
// ).includes(folder)
if (role == 'admin') {
await FolderService.addOwnerToFolder({
{#snippet body()}
<tbody>
{#each perms ?? [] as { owner_name, role }}<tr>
<td>{owner_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username &&
!$userStore?.is_admin}
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
// const inAcl = (
// folder?.extra_perms ? Object.keys(folder?.extra_perms) : []
// ).includes(folder)
if (role == 'admin') {
await FolderService.addOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name
}
})
} else if (role == 'writer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: true
}
})
} else if (role == 'viewer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: false
}
})
}
loadFolder()
}}
>
{#snippet children({ item })}
<ToggleButton
value="viewer"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="center"
value="writer"
label="Writer"
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="right"
value="admin"
label="Admin"
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write && (owner_name != 'u/' + $userStore?.username || $userStore?.is_admin)}
<button
class="ml-2 text-red-500"
onclick={async () => {
await Promise.all([
FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: { owner: owner_name }
}),
GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
} else if (role == 'writer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: true
}
})
} else if (role == 'viewer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: false
}
})
}
])
loadFolder()
}}
}}>remove</button
>
{#snippet children({ item })}
<ToggleButton
value="viewer"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="center"
value="writer"
label="Writer"
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="right"
value="admin"
label="Admin"
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write && (owner_name != 'u/' + $userStore?.username || $userStore?.is_admin)}
<button
class="ml-2 text-red-500"
on:click={async () => {
await Promise.all([
FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: { owner: owner_name }
}),
GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
])
loadFolder()
}}>remove</button
>
{:else}
<span class="text-tertiary text-xs">cannot remove yourself</span>
{/if}</td
>
</tr>{/each}
</tbody>
{:else}
<span class="text-tertiary text-xs">cannot remove yourself</span>
{/if}</td
>
</tr>{/each}
</tbody>
{/snippet}
</TableCustom>
<!-- <h2 class="mt-10"
>Folders managing this folder <Tooltip
@@ -1,7 +1,11 @@
<script lang="ts">
import Popover from './Popover.svelte'
export let members: string[]
interface Props {
members: string[]
}
let { members }: Props = $props()
</script>
<Popover>
@@ -11,5 +15,7 @@
<span class="text-tertiary text-xs">{members?.join(', ')} </span>
</div>
</div>
<span slot="text">{members?.join(', ')} </span>
{#snippet text()}
<span>{members?.join(', ')} </span>
{/snippet}
</Popover>
@@ -0,0 +1,440 @@
<script lang="ts">
import { workspaceStore, enterpriseLicense, userStore } from '$lib/stores'
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte'
import { onDestroy } from 'svelte'
import {
createGitHubAppState,
loadGithubInstallations,
startInstallationCheck,
stopInstallationCheck,
getRepositories,
addInstallationToWorkspace,
deleteInstallation,
exportInstallation,
importInstallation,
applyRepositoryURL,
handleInstallClick,
type GitHubAppState
} from '$lib/githubApp'
interface Props {
resourceType: string
args?: Record<string, any>
description?: string
onArgsUpdate?: (args: Record<string, any>) => void
onDescriptionUpdate?: (description: string) => void
}
let {
resourceType,
args = {},
description = '',
onArgsUpdate,
onDescriptionUpdate
}: Props = $props()
// GitHub App state using the service utilities
let githubState: GitHubAppState = $state(createGitHubAppState())
let githubAppPopover: { open: () => void; close: () => void } | null = $state(null)
// Filter and deduplicate installations not in current workspace
let githubInstallationsNotInWorkspace = $derived(
githubState.githubInstallations
.filter(
(installation) =>
!githubState.workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
)
.filter(
(installation, index, array) =>
array.findIndex((item) => item.installation_id === installation.installation_id) === index
)
)
let showGitHubApp = $derived(
resourceType === 'git_repository' && $workspaceStore && $userStore?.is_admin
)
// Load GitHub installations when conditions are met
$effect(() => {
if (showGitHubApp && $enterpriseLicense && $workspaceStore) {
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
console.error('Failed to load GitHub installations:', error)
})
}
})
// Clean up interval when component is destroyed
onDestroy(() => {
stopInstallationCheck(githubState)
})
// Extracted event handlers for better maintainability
function handleApplyRepositoryURL(close: (_: any) => void) {
try {
applyRepositoryURL(
githubState,
args,
description,
(newArgs) => {
if (onArgsUpdate) {
onArgsUpdate(newArgs)
}
},
(newDescription) => {
if (onDescriptionUpdate) {
onDescriptionUpdate(newDescription)
}
}
)
close(null)
} catch (error) {
console.error('Failed to apply repository URL:', error)
}
}
async function handleDeleteInstallation(installationId: number) {
if (!$workspaceStore) return
try {
await deleteInstallation($workspaceStore, installationId, () =>
loadGithubInstallations(githubState, $workspaceStore!)
)
} catch (error) {
console.error('Failed to delete installation:', error)
}
}
async function handleAddInstallation(installationId: number, workspaceId: string) {
if (!$workspaceStore) return
try {
await addInstallationToWorkspace($workspaceStore, installationId, workspaceId, () =>
loadGithubInstallations(githubState, $workspaceStore!)
)
} catch (error) {
console.error('Failed to add installation:', error)
}
}
async function handleExportInstallation(installationId: number) {
if (!$workspaceStore) return
try {
await exportInstallation($workspaceStore, installationId)
} catch (error) {
console.error('Failed to export installation:', error)
}
}
async function handleImportInstallation() {
if (!$workspaceStore) return
try {
await importInstallation($workspaceStore, githubState.importJwt, () => {
githubState.importJwt = ''
loadGithubInstallations(githubState, $workspaceStore!)
})
} catch (error) {
console.error('Failed to import installation:', error)
}
}
function handleRefreshInstallations() {
if (!$workspaceStore) return
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
console.error('Failed to refresh installations:', error)
})
}
function handleInstallClickWithPopover() {
if (!$workspaceStore) return
handleInstallClick(githubState, $workspaceStore, () => {
githubAppPopover?.open()
})
}
</script>
{#if showGitHubApp}
{#if !githubState.loadingGithubInstallations}
<Button
color="light"
variant="contained"
size="xs"
on:click={handleRefreshInstallations}
disabled={!$enterpriseLicense}
startIcon={{ icon: RotateCw }}
/>
{:else}
<Loader2 class="animate-spin w-10 h-4" />
{/if}
{#if showGitHubApp}
<Popover
documentationLink="https://www.windmill.dev/docs/integrations/git_repository#github-app"
bind:this={githubAppPopover}
floatingConfig={{
placement: 'bottom'
}}
disabled={!$enterpriseLicense || githubState.loadingGithubInstallations}
>
{#snippet trigger()}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || githubState.loadingGithubInstallations}
startIcon={{
icon: githubState.loadingGithubInstallations ? Loader2 : Github,
classes: githubState.loadingGithubInstallations ? 'animate-spin' : ''
}}
nonCaptureEvent
>
{$enterpriseLicense ? 'GitHub App' : 'GitHub App (ee only)'}
</Button>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="flex flex-col gap-4 w-[600px]">
{#if githubState.workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Select Repository</p>
<div class="flex flex-row gap-2 w-full">
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">GitHub Account ID</p>
<select bind:value={githubState.selectedGHAppAccountId}>
<option value="" disabled>Select GitHub Account ID</option>
{#each githubState.workspaceGithubInstallations as installation (`select-${installation.installation_id}-${installation.workspace_id}`)}
<option value={installation.account_id}>{installation.account_id}</option>
{/each}
</select>
</div>
{#if githubState.selectedGHAppAccountId}
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Repository</p>
<div class="flex flex-row gap-2">
<select bind:value={githubState.selectedGHAppRepository}>
<option value="" disabled selected>Select Repository</option>
{#each getRepositories(githubState, githubState.selectedGHAppAccountId) as repository (repository.url)}
<option value={repository.url}>{repository.name}</option>
{/each}
</select>
</div>
</div>
{/if}
<div class="pt-[26px]">
<Button
size="xs"
color="blue"
buttonType="button"
disabled={!githubState.selectedGHAppRepository}
on:click={() => handleApplyRepositoryURL(close)}
>
Apply
</Button>
</div>
</div>
</div>
{/if}
<div
class={`${
githubState.workspaceGithubInstallations.length > 0
? 'border-t border-gray-200 dark:border-gray-700'
: ''
} pt-4`}
>
<div class="flex flex-col gap-4">
<div class="flex">
<Button
color="none"
variant="border"
size="xs"
href={githubState.githubInstallationUrl}
startIcon={{
icon: githubState.isCheckingInstallation ? Loader2 : Plus,
classes: githubState.isCheckingInstallation ? 'animate-spin' : ''
}}
target="_blank"
disabled={githubState.isCheckingInstallation}
on:click={() => {
if ($workspaceStore) {
startInstallationCheck(githubState, $workspaceStore, () =>
loadGithubInstallations(githubState, $workspaceStore!)
)
}
}}
>
{githubState.isCheckingInstallation
? 'Checking for new installations...'
: 'Add new installation'}
</Button>
</div>
{#if githubState.workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Current installations:</p>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each githubState.workspaceGithubInstallations as installation (`current-${installation.installation_id}-${installation.workspace_id}`)}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="py-2 text-right">
<div class="flex justify-end gap-1">
<Button
size="xs2"
color="blue"
title="Export installation to other instance"
startIcon={{ icon: Download }}
on:click={() =>
handleExportInstallation(installation.installation_id)}
>
Export
</Button>
<Button
size="xs2"
color="red"
title="Remove installation from workspace"
startIcon={{ icon: Minus }}
on:click={() =>
handleDeleteInstallation(installation.installation_id)}
>
Remove
</Button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if githubInstallationsNotInWorkspace.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Installations in other workspaces:</p
>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each githubInstallationsNotInWorkspace as installation (`other-${installation.installation_id}-${installation.workspace_id}`)}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="pl-8 py-2 text-right">
<Button
size="xs2"
color="blue"
title="Add installation to workspace"
startIcon={{ icon: Plus }}
on:click={() => {
if (installation.workspace_id) {
handleAddInstallation(
installation.installation_id,
installation.workspace_id
)
}
}}
>
Add to workspace
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</div>
</div>
<div class="mt-4 flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Import installation from other instance:</p
>
<div class="flex gap-2">
<input
type="text"
placeholder="Paste JWT token here"
bind:value={githubState.importJwt}
class="flex-1"
/>
<Button
color="blue"
on:click={handleImportInstallation}
disabled={!githubState.importJwt}
>
Import
</Button>
</div>
</div>
</div>
</div>
{/snippet}
</Popover>
{:else}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || githubState.loadingGithubInstallations}
startIcon={{
icon:
githubState.loadingGithubInstallations || githubState.isCheckingInstallation
? Loader2
: Github,
classes:
githubState.loadingGithubInstallations || githubState.isCheckingInstallation
? 'animate-spin'
: ''
}}
href={githubState.githubInstallationUrl}
target="_blank"
on:click={handleInstallClickWithPopover}
>
{$enterpriseLicense
? githubState.isCheckingInstallation
? 'Waiting for installation...'
: 'Install GitHub App'
: 'GitHub App (ee only)'}
</Button>
{/if}
{/if}
+12 -4
View File
@@ -2,16 +2,22 @@
import { GroupService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Popover from './Popover.svelte'
import { untrack } from 'svelte'
export let name: string
interface Props {
name: string
}
$: $workspaceStore && loadMembers()
let { name }: Props = $props()
let members: string[] | undefined = []
let members: string[] | undefined = $state([])
async function loadMembers() {
members = (await GroupService.getGroup({ workspace: $workspaceStore!, name })).members
}
$effect(() => {
$workspaceStore && untrack(() => loadMembers())
})
</script>
{#if members}
@@ -22,6 +28,8 @@
><span class="text-tertiary text-xs">{members?.join(', ')}</span></div
></div
>
<span slot="text">{members?.join(', ')}</span></Popover
{#snippet text()}
<span>{members?.join(', ')}</span>
{/snippet}</Popover
>
{/if}
@@ -649,7 +649,7 @@
<Button
variant="border"
color="light"
size="md"
size="xs"
btnClasses="mt-1"
on:click={() => {
if ($values[setting.key] == undefined || !Array.isArray($values[setting.key])) {
@@ -668,7 +668,7 @@
disabled={!$enterpriseLicense}
variant="border"
color="light"
size="md"
size="xs"
on:click={async () => {
try {
await SettingService.testCriticalChannels({
+27 -13
View File
@@ -12,18 +12,30 @@
import { classNames, emptyString, parseQueryParams } from '$lib/utils'
import { base } from '$lib/base'
import { getUserExt } from '$lib/user'
import { Button, Skeleton } from '$lib/components/common'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
import { refreshSuperadmin } from '$lib/refreshUser'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Button from './common/button/Button.svelte'
export let rd: string | undefined = undefined
export let email: string | undefined = undefined
export let password: string | undefined = undefined
export let error: string | undefined = undefined
export let popup: boolean = false
export let firstTime: boolean = false
interface Props {
rd?: string | undefined
email?: string | undefined
password?: string | undefined
error?: string | undefined
popup?: boolean
firstTime?: boolean
}
let {
rd = undefined,
email = $bindable(undefined),
password = $bindable(undefined),
error = undefined,
popup = false,
firstTime = false
}: Props = $props()
const providers = [
{
@@ -60,9 +72,9 @@
const providersType = providers.map((p) => p.type as string)
let showPassword = false
let logins: OAuthLogin[] | undefined = undefined
let saml: string | undefined = undefined
let showPassword = $state(false)
let logins: OAuthLogin[] | undefined = $state(undefined)
let saml: string | undefined = $state(undefined)
type OAuthLogin = {
type: string
@@ -251,7 +263,9 @@
}
}
$: error && sendUserToast(error, true)
$effect(() => {
error && sendUserToast(error, true)
})
</script>
<div class="bg-surface px-4 py-8 shadow md:border sm:rounded-lg sm:px-10">
@@ -352,7 +366,7 @@
</label>
<div>
<input
on:keyup={handleKeyUp}
onkeyup={handleKeyUp}
bind:value={password}
id="password"
type="password"
@@ -364,7 +378,7 @@
<div class="pt-2">
<button
on:click={login}
onclick={login}
disabled={!email || !password}
class="flex w-full justify-center rounded-md bg-frost-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-frost-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-frost-600"
>
@@ -24,7 +24,7 @@
disableMock?: boolean
disableHistory?: boolean
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
loadingHistory?: boolean
loadingJob?: boolean
}
let {
@@ -40,27 +40,18 @@
disableMock = false,
disableHistory = false,
onUpdateMock,
loadingHistory = false
loadingJob = false
}: Props = $props()
const { testSteps } = getContext<FlowEditorContext>('FlowEditorContext')
let selectedJob: Job | undefined = $state(undefined)
let fetchingLastJob = false
let preview: 'mock' | 'job' | undefined = $state(undefined)
let jobProgressReset: () => void = $state(() => {})
let nlastJob = $derived.by(() => {
if (testJob && testJob.type === 'CompletedJob') {
return { ...testJob, preview: true }
}
if (lastJob) {
return { ...lastJob, preview: false }
}
return undefined
})
let forceJson = $state(false)
const logJob = $derived(testJob ?? selectedJob)
</script>
<Splitpanes horizontal>
@@ -75,7 +66,8 @@
{/if}
<OutputPickerInner
lastJob={nlastJob}
{lastJob}
{testJob}
fullResult
moduleId={mod.id}
closeOnOutsideClick={true}
@@ -84,7 +76,7 @@
mock={mod.mock}
bind:forceJson
bind:selectedJob
isLoading={(testIsLoading && !scriptProgress) || fetchingLastJob || loadingHistory}
isLoading={testIsLoading || loadingJob}
bind:preview
path={`path` in mod.value ? mod.value.path : ''}
{loopStatus}
@@ -110,14 +102,12 @@
{:else}
<LogViewer
small
jobId={selectedJob?.id}
duration={selectedJob?.['duration_ms']}
mem={selectedJob?.['mem_peak']}
content={selectedJob?.logs}
isLoading={(testIsLoading && selectedJob?.['running'] == false) ||
fetchingLastJob ||
loadingHistory}
tag={selectedJob?.tag}
jobId={logJob?.id}
duration={logJob?.['duration_ms']}
mem={logJob?.['mem_peak']}
content={logJob?.logs}
isLoading={(testIsLoading && logJob?.['running'] == false) || loadingJob}
tag={logJob?.tag}
/>
{/if}
</Pane>
+15 -17
View File
@@ -1,8 +1,7 @@
<script lang="ts" module>
type testModuleState = {
loading: boolean
instances: number
cancel?: () => void
cancel?: () => Promise<void>
}
let testModulesState = $state<Record<string, testModuleState>>({})
@@ -53,7 +52,10 @@
// Not defined if JobProgressBar not loaded
if (jobProgressReset) jobProgressReset()
testModulesState[mod.id].cancel = testJobLoader?.cancelJob
testModulesState[mod.id].cancel = async () => {
await testJobLoader?.cancelJob()
testJob = undefined
}
const val = mod.value
// let jobId: string | undefined = undefined
@@ -111,14 +113,7 @@
const modId = mod.id
testModulesState[modId] = {
...(testModulesState[modId] ?? { loading: false, instances: 0 }),
loading: testIsLoading,
instances: testModulesState[modId]!.instances + 1
}
return () => {
testModulesState[modId].instances -= 1
if (testModulesState[modId].instances < 1) {
delete testModulesState[modId]
}
loading: testIsLoading
}
})
</script>
@@ -130,12 +125,15 @@
bind:this={testJobLoader}
bind:isLoading={
() => testModulesState[mod.id]?.loading ?? false,
(v) =>
(testModulesState[mod.id] = {
...testModulesState[mod.id],
loading: v ?? false,
instances: testModulesState[mod.id]?.instances ?? 0
})
(v) => {
let newLoading = v ?? false
if (testModulesState[mod.id]?.loading !== newLoading) {
testModulesState[mod.id] = {
...(testModulesState[mod.id] ?? {}),
loading: newLoading
}
}
}
}
bind:job={testJob}
/>
@@ -4,6 +4,8 @@
</script>
<Popover notClickable>
<svelte:fragment slot="text">The script has no main function exported</svelte:fragment>
{#snippet text()}
The script has no main function exported
{/snippet}
<Badge small color="yellow" baseClass="border border-indigo-200">No main</Badge>
</Popover>
@@ -17,6 +17,7 @@
defaultValue: any
editor?: SimpleEditor | undefined
path?: string
onClear?: () => void
}
let {
@@ -26,7 +27,8 @@
showSchemaExplorer = false,
selectFirst = false,
defaultValue,
editor = $bindable(undefined)
editor = $bindable(undefined),
onClear = undefined
}: Props = $props()
function isResource() {
@@ -48,7 +50,7 @@
<ResourcePicker
{selectFirst}
{disablePortal}
on:clear
{onClear}
bind:value={
() => valueToPath(),
(v) => {
@@ -6,6 +6,7 @@
import { sendUserToast } from '$lib/toast'
import TestConnection from './TestConnection.svelte'
import { enterpriseLicense } from '$lib/stores'
import SimpleEditor from './SimpleEditor.svelte'
type S3Config = {
type: 'S3'
@@ -35,7 +36,14 @@
roleArn: string
}
export let bucket_config: S3Config | AzureConfig | AwsOidcConfig | undefined = undefined
type GcsConfig = {
type: 'Gcs'
bucket: string
serviceAccountKey: Record<string, string>
}
export let bucket_config: S3Config | AzureConfig | AwsOidcConfig | GcsConfig | undefined =
undefined
$: bucket_config?.type == 'S3' &&
bucket_config.allow_http == undefined &&
@@ -105,10 +113,11 @@
buttonTextOverride="Test from a worker"
/>
</div>
<Tabs
bind:selected={bucket_config.type}
selected={bucket_config?.type ?? 'S3'}
on:selected={(e) => {
if (e.detail === 'S3') {
if (e.detail === 'S3' && bucket_config?.type !== 'S3') {
bucket_config = {
type: 'S3',
bucket: '',
@@ -117,7 +126,7 @@
secret_key: '',
endpoint: ''
}
} else if (e.detail === 'Azure') {
} else if (e.detail === 'Azure' && bucket_config?.type !== 'Azure') {
bucket_config = {
type: 'Azure',
accountName: '',
@@ -127,12 +136,26 @@
clientId: '',
accessKey: ''
}
} else if (e.detail === 'Gcs' && bucket_config?.type !== 'Gcs') {
bucket_config = {
type: 'Gcs',
bucket: '',
serviceAccountKey: {}
}
} else if (e.detail === 'AwsOidc' && bucket_config?.type !== 'AwsOidc') {
bucket_config = {
type: 'AwsOidc',
bucket: '',
region: '',
roleArn: ''
}
}
}}
>
<Tab size="sm" value="S3">S3</Tab>
<Tab size="sm" value="Azure">Azure Blob</Tab>
<Tab size="sm" value="AwsOidc">AWS OIDC</Tab>
<Tab size="sm" value="Gcs">Google Cloud Storage</Tab>
</Tabs>
<div class="flex flex-col gap-2 mt-2 p-2 border rounded-md">
{#if bucket_config.type === 'S3'}
@@ -235,6 +258,37 @@
bind:value={bucket_config.roleArn}
/>
</label>
{:else if bucket_config.type === 'Gcs'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Bucket</span>
<input type="text" placeholder="bucket-name" bind:value={bucket_config.bucket} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Service Account Key</span>
<span class="text-tertiary text-2xs">JSON content of the service account key file</span>
<SimpleEditor
lang="json"
bind:code={
() => {
if (bucket_config?.type === 'Gcs') {
return JSON.stringify(bucket_config.serviceAccountKey)
} else {
return '{}'
}
},
(v) => {
if (bucket_config?.type === 'Gcs') {
try {
bucket_config.serviceAccountKey = JSON.parse(v ?? '{}')
} catch (_) {
bucket_config.serviceAccountKey = {}
}
}
}
}
class="h-80"
/>
</label>
{:else}
<div>Unknown bucket type {bucket_config['type']}</div>
{/if}
+49 -23
View File
@@ -5,16 +5,39 @@
import { twMerge } from 'tailwind-merge'
import { ExternalLink } from 'lucide-svelte'
import { untrack } from 'svelte'
export let placement: PopoverPlacement = 'bottom-end'
export let notClickable = false
export let popupClass = ''
export let disablePopup = false
export let disappearTimeout = 100
export let appearTimeout = 300
export let documentationLink: string | undefined = undefined
export let style: string | undefined = undefined
export let forceOpen = false
interface Props {
placement?: PopoverPlacement
notClickable?: boolean
popupClass?: string
disablePopup?: boolean
disappearTimeout?: number
appearTimeout?: number
documentationLink?: string | undefined
style?: string | undefined
forceOpen?: boolean
class?: string
children?: import('svelte').Snippet
text?: import('svelte').Snippet
onClick?: () => void
}
let {
placement = 'bottom-end',
notClickable = false,
popupClass = '',
disablePopup = false,
disappearTimeout = 100,
appearTimeout = 300,
documentationLink = undefined,
style = undefined,
forceOpen = false,
class: classNames = '',
children,
text,
onClick
}: Props = $props()
const [popperRef, popperContent] = createPopperActions({ placement })
@@ -32,7 +55,7 @@
]
}
let showTooltip = false
let showTooltip = $state(false)
let timeout: NodeJS.Timeout | undefined = undefined
let inTimeout: NodeJS.Timeout | undefined = undefined
@@ -50,40 +73,43 @@
timeout = setTimeout(() => (showTooltip = false), disappearTimeout)
}
$: forceOpen ? open() : close()
$effect(() => {
;[forceOpen]
untrack(() => (forceOpen ? open() : close()))
})
</script>
{#if notClickable}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span {style} use:popperRef on:mouseenter={open} on:mouseleave={close} class={$$props.class}>
<slot />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span {style} use:popperRef onmouseenter={open} onmouseleave={close} class={classNames}>
{@render children?.()}
</span>
{:else}
<button
{style}
use:popperRef
on:mouseenter={open}
on:mouseleave={close}
on:click
class={$$props.class}
onmouseenter={open}
onmouseleave={close}
onclick={onClick}
class={classNames}
>
<slot />
{@render children?.()}
</button>
{/if}
{#if showTooltip && !disablePopup}
<Portal name="popover">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
use:popperContent={popperOptions}
on:mouseenter={open}
on:mouseleave={close}
onmouseenter={open}
onmouseleave={close}
class={twMerge(
'z-[5001] py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800 whitespace-normal text-left',
popupClass
)}
>
<div class="max-w-sm break-words">
<slot name="text" />
{@render text?.()}
{#if documentationLink}
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
<div class="flex flex-row gap-2 mt-4">
@@ -20,7 +20,7 @@
import autosize from '$lib/autosize'
import GfmMarkdown from './GfmMarkdown.svelte'
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
interface Props {
canSave?: boolean
@@ -28,7 +28,7 @@
path?: string
newResource?: boolean
hidePath?: boolean
watchChanges?: boolean
onChange?: (args: { path: string; args: Record<string, any>; description: string }) => void
defaultValues?: Record<string, any> | undefined
}
@@ -38,7 +38,7 @@
path = $bindable(''),
newResource = false,
hidePath = false,
watchChanges = false,
onChange,
defaultValues = undefined
}: Props = $props()
@@ -61,7 +61,6 @@
let viewJsonSchema = $state(false)
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
let rawCode: string | undefined = $state(undefined)
@@ -180,8 +179,8 @@
run(() => {
canSave = can_write && isValid && jsonError == ''
})
run(() => {
watchChanges && dispatchIfMounted('change', { path, args, description })
$effect(() => {
onChange && onChange({ path, args, description })
})
run(() => {
rawCode && untrack(() => parseJson())
@@ -247,19 +246,33 @@
<GfmMarkdown md={description} />
{/if}
<div class="flex w-full justify-between items-center mt-4">
<div></div>
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
{:else}
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
{/if}
<div class="w-full flex gap-4 flex-row-reverse items-center mt-4">
<Toggle
on:change={(e) => switchTab(e.detail)}
options={{
right: 'As JSON'
}}
/>
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
{:else}
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
{/if}
{#if resource_type === 'git_repository' && $workspaceStore && $userStore?.is_admin}
<GitHubAppIntegration
resourceType={resource_type}
{args}
{description}
onArgsUpdate={(newArgs) => {
args = newArgs
// Update rawCode if in JSON view mode
if (viewJsonSchema) {
rawCode = JSON.stringify(args, null, 2)
}
}}
onDescriptionUpdate={(newDescription) => (description = newDescription)}
/>
{/if}
</div>
<div>
{#if loadingSchema}
@@ -1,7 +1,7 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher, onMount, untrack } from 'svelte'
import { onMount, untrack } from 'svelte'
import AppConnect from './AppConnectDrawer.svelte'
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
@@ -11,10 +11,6 @@
import { sendUserToast } from '$lib/toast'
import { isDbType } from './apps/components/display/dbtable/utils'
import Select from './select/Select.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
interface Props {
initialValue?: string | undefined
@@ -28,6 +24,7 @@
expressOAuthSetup?: boolean
defaultValues?: Record<string, any> | undefined
placeholder?: string | undefined
onClear?: () => void
}
let {
@@ -41,7 +38,8 @@
selectFirst = false,
expressOAuthSetup = false,
defaultValues = undefined,
placeholder = undefined
placeholder = undefined,
onClear = undefined
}: Props = $props()
if (initialValue && value == undefined) {
@@ -140,10 +138,6 @@
)
})
$effect(() => {
dispatchIfMounted('change', value)
})
let appConnect: AppConnect | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
</script>
@@ -186,7 +180,7 @@
initialValue = undefined
value = undefined
valueType = undefined
dispatch('clear')
onClear?.()
}}
items={collection}
clearable
@@ -1,21 +1,31 @@
<script lang="ts">
import { JobService, type Preview } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy, tick } from 'svelte'
import { onDestroy, tick, untrack } from 'svelte'
import { createEventDispatcher } from 'svelte'
import type { SupportedLanguage } from '$lib/common'
export let isLoading = false
export let job: { completed: boolean; result: any; id: string; success?: boolean } | undefined =
undefined
export let workspaceOverride: string | undefined = undefined
export let notfound = false
export let isEditor = false
export let allowConcurentRequests = false
interface Props {
isLoading?: boolean
job?: { completed: boolean; result: any; id: string; success?: boolean } | undefined
workspaceOverride?: string | undefined
notfound?: boolean
isEditor?: boolean
allowConcurentRequests?: boolean
}
let {
isLoading = $bindable(false),
job = $bindable(undefined),
workspaceOverride = undefined,
notfound = $bindable(false),
isEditor = false,
allowConcurentRequests = false
}: Props = $props()
const dispatch = createEventDispatcher()
$: workspace = workspaceOverride ?? $workspaceStore!
let workspace = $derived(workspaceOverride ?? $workspaceStore!)
let syncIteration: number = 0
let errorIteration = 0
@@ -24,9 +34,16 @@
let ITERATIONS_BEFORE_SUPER_SLOW_REFRESH = 100
let lastStartedAt: number = Date.now()
let currentId: string | undefined = undefined
let currentId: string | undefined = $state(undefined)
$: isLoading = currentId !== undefined
$effect(() => {
let newIsLoading = currentId !== undefined
untrack(() => {
if (isLoading !== newIsLoading) {
isLoading = newIsLoading
}
})
})
type Callbacks = { done: (x: any) => void; cancel: () => void; error: (err: Error) => void }
+1 -1
View File
@@ -18,7 +18,7 @@
import { page } from '$app/stores'
import { replaceState } from '$app/navigation'
import JsonInputs from '$lib/components/JsonInputs.svelte'
import { triggerableByAI } from '$lib/actions/triggerableByAI'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import { untrack } from 'svelte'
@@ -25,7 +25,6 @@
import TableSimple from './TableSimple.svelte'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import FileUploadModal from './common/fileUpload/FileUploadModal.svelte'
import PdfViewer from './display/PdfViewer.svelte'
import { twMerge } from 'tailwind-merge'
let deletionModalOpen = $state(false)
@@ -722,11 +721,15 @@
</div>
{:else if fileMetadata?.fileKey.endsWith('.pdf')}
<div class="w-full h-[950px] border">
<PdfViewer
source={`/api/w/${$workspaceStore}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
fileMetadata.fileKey
)}` + (storage ? `&storage=${storage}` : '')}
/>
{#await import('$lib/components/display/PdfViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
source={`/api/w/${$workspaceStore}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
fileMetadata.fileKey
)}` + (storage ? `&storage=${storage}` : '')}
/>
{/await}
</div>
{:else if filePreviewLoading}
<div class="flex h-6 items-center text-tertiary mb-4">
@@ -173,7 +173,10 @@
.forEach((x) => {
n[x] = schema.properties[x]
})
if (!deepEqual(schema.properties, n) || !deepEqual(schema.properties, Object.keys(n))) {
if (
!deepEqual(schema.properties, n) ||
!deepEqual(Object.keys(schema.properties), Object.keys(n))
) {
schema.properties = n
}
}
+108 -89
View File
@@ -11,7 +11,8 @@
type TriggersCount,
PostgresTriggerService,
CaptureService,
type ScriptLang
type ScriptLang,
WorkerService
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { initialCode } from '$lib/script_helpers'
@@ -21,6 +22,7 @@
enterpriseLicense,
usedTriggerKinds,
userStore,
workerTags,
workspaceStore
} from '$lib/stores'
import {
@@ -65,16 +67,15 @@
import ScriptSchema from './ScriptSchema.svelte'
import Section from './Section.svelte'
import Label from './Label.svelte'
import type DiffDrawer from './DiffDrawer.svelte'
import type Editor from './Editor.svelte'
import WorkerTagPicker from './WorkerTagPicker.svelte'
import MetadataGen from './copilot/MetadataGen.svelte'
import { writable } from 'svelte/store'
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import DefaultScripts from './DefaultScripts.svelte'
import { createEventDispatcher, onMount, setContext, untrack } from 'svelte'
import { onMount, setContext, untrack } from 'svelte'
import Summary from './Summary.svelte'
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import TriggersEditor from './triggers/TriggersEditor.svelte'
import type { ScheduleTrigger, TriggerContext } from './triggers'
@@ -86,7 +87,6 @@
} from '$lib/script_helpers'
import CaptureTable from './triggers/CaptureTable.svelte'
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
import DeployButton from './DeployButton.svelte'
import {
type NewScriptWithDraftAndDraftTriggers,
@@ -97,29 +97,12 @@
} from './triggers/utils'
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
import { Triggers } from './triggers/triggers.svelte'
interface Props {
script: NewScript & { draft_triggers?: Trigger[] }
fullyLoaded?: boolean
initialPath?: string
template?: 'docker' | 'bunnative' | 'script'
initialArgs?: Record<string, any>
lockedLanguage?: boolean
showMeta?: boolean
neverShowMeta?: boolean
diffDrawer?: DiffDrawer | undefined
savedScript?: NewScriptWithDraftAndDraftTriggers | undefined
searchParams?: URLSearchParams
disableHistoryChange?: boolean
replaceStateFn?: (url: string) => void
customUi?: ScriptBuilderWhitelabelCustomUi
savedPrimarySchedule?: ScheduleTrigger | undefined
functionExports?: ((exports: ScriptBuilderFunctionExports) => void) | undefined
children?: import('svelte').Snippet
}
import type { ScriptBuilderProps } from './script_builder'
import type { DiffDrawerI } from './diff_drawer'
import WorkerTagSelect from './WorkerTagSelect.svelte'
let {
script = $bindable(),
script,
fullyLoaded = true,
initialPath = $bindable(''),
template = $bindable('script'),
@@ -135,8 +118,15 @@
customUi = {},
savedPrimarySchedule = undefined,
functionExports = undefined,
children
}: Props = $props()
children,
onDeploy,
onDeployError,
onSaveInitial,
onSeeDetails,
onSaveDraftError,
onSaveDraft,
disableAi
}: ScriptBuilderProps = $props()
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
@@ -153,7 +143,7 @@
'u/' +
($userStore?.username?.includes('@')
? $userStore?.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
: $userStore?.username!) +
: $userStore?.username) +
'/' +
generateRandomString(12)
@@ -210,8 +200,6 @@
loadTriggers()
}
const dispatch = createEventDispatcher()
onMount(() => {
if (functionExports) {
console.log('functionExports set')
@@ -573,10 +561,10 @@
script.parent_hash = newHash
sendUserToast('Deployed')
} else {
dispatch('deploy', newHash)
onDeploy?.({ path: script.path, hash: newHash })
}
} catch (error) {
dispatch('deployError', error)
onDeployError?.({ path: script.path, error })
sendUserToast(`Error while saving the script: ${error.body || error.message}`, true)
}
loadingSave = false
@@ -629,7 +617,7 @@
} catch (error) {
sendUserToast(`Could not parse code, are you sure it is valid?`, true)
}
let newHash = ''
if (initialPath == '' || savedScript?.draft_only) {
if (savedScript?.draft_only) {
await ScriptService.deleteScriptByPath({
@@ -649,7 +637,7 @@
runnableKind: 'script'
})
}
await ScriptService.createScript({
newHash = await ScriptService.createScript({
workspace: $workspaceStore!,
requestBody: {
path: script.path,
@@ -709,9 +697,9 @@
if (initialPath == '' || (savedScript?.draft_only && script.path !== initialPath)) {
savedAtNewPath = true
initialPath = script.path
dispatch('saveInitial', script.path)
onSaveInitial?.({ path: script.path, hash: newHash })
}
dispatch('saveDraft', { path: script.path, savedAtNewPath, script })
onSaveDraft?.({ path: script.path, savedAtNewPath, script })
sendUserToast('Saved as draft')
} catch (error) {
@@ -719,7 +707,7 @@
`Error while saving the script as a draft: ${error.body || error.message}`,
true
)
dispatch('saveDraftError', error)
onSaveDraftError?.({ path: script.path, error })
}
loadingDraft = false
}
@@ -727,7 +715,7 @@
function computeDropdownItems(
initialPath: string,
savedScript: NewScriptWithDraftAndDraftTriggers | undefined,
diffDrawer: DiffDrawer | undefined
diffDrawer: DiffDrawerI | undefined
) {
let dropdownItems: { label: string; onClick: () => void }[] =
initialPath != '' && customUi?.topBar?.extraDeployOptions != false
@@ -777,7 +765,7 @@
{
label: 'Exit & See details',
onClick: () => {
dispatch('seeDetails', initialPath)
onSeeDetails?.({ path: initialPath })
}
}
]
@@ -945,14 +933,21 @@
$effect(() => {
!disableHistoryChange && untrack(() => encodeScriptState(script))
})
loadWorkerTags()
async function loadWorkerTags() {
if (!$workerTags) {
$workerTags = await WorkerService.getCustomTags({ workspace: $workspaceStore })
}
}
</script>
<svelte:window onkeydown={onKeyDown} />
{@render children?.()}
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
{deployedBy}
{confirmCallback}
bind:open
{diffDrawer}
bind:deployedValue
@@ -1081,7 +1076,7 @@
}}
/>
</Label>
{#if script.schema}
{#if script.schema && !disableAi && !customUi?.settingsPanel?.metadata?.disableAiFilling}
<div class="mt-3">
<AIFormSettings
bind:prompt={script.schema.prompt_for_ai as string | undefined}
@@ -1091,52 +1086,52 @@
{/if}
</div>
</Section>
<Section label="Language">
{#snippet action()}
<DefaultScripts />
{/snippet}
{#if lockedLanguage}
<div class="text-sm text-tertiary italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class=" grid grid-cols-3 gap-2">
{#each langs as [label, lang] (lang)}
{@const isPicked =
(lang == script.language && template == 'script') ||
(template == 'bunnative' && lang == 'bunnative') ||
(template == 'docker' && lang == 'docker')}
<Popover
disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}
>
<Button
aiId={`create-script-language-button-${lang}`}
aiDescription={`Choose ${lang} as the language of the script`}
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
on:click={() => onScriptLanguageTrigger(lang)}
disabled={lockedLanguage ||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
{#if !customUi?.settingsPanel?.metadata?.languages || customUi?.settingsPanel?.metadata?.languages?.length > 1}
<Section label="Language">
{#snippet action()}
<DefaultScripts />
{/snippet}
{#if lockedLanguage}
<div class="text-sm text-tertiary italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class=" grid grid-cols-3 gap-2">
{#each langs as [label, lang] (lang)}
{@const isPicked =
(lang == script.language && template == 'script') ||
(template == 'bunnative' && lang == 'bunnative') ||
(template == 'docker' && lang == 'docker')}
<Popover
disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
{#if lang === 'nu'}
<span class="text-tertiary !text-xs"> BETA </span>
{/if}
</Button>
{#snippet text()}
{label} is only available with an enterprise license
{/snippet}
</Popover>
{/each}
</div>
</Section>
<Button
aiId={`create-script-language-button-${lang}`}
aiDescription={`Choose ${lang} as the language of the script`}
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
on:click={() => onScriptLanguageTrigger(lang)}
disabled={lockedLanguage ||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
{#if lang === 'nu'}
<span class="text-tertiary !text-xs"> BETA </span>
{/if}
</Button>
{#snippet text()}
{label} is only available with an enterprise license
{/snippet}
</Popover>
{/each}
</div>
</Section>
{/if}
{#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
<Section label="Script kind">
{#snippet header()}
@@ -1172,6 +1167,14 @@
</ToggleButtonGroup>
</Section>
{/if}
{#if customUi?.settingsPanel?.disableRuntime}
<Section label="Worker group tag (queue)">
<WorkerTagPicker
bind:tag={script.tag}
placeholder={customUi?.tagSelectPlaceholder}
/>
</Section>
{/if}
</div>
</TabContent>
<TabContent value="runtime">
@@ -1239,7 +1242,10 @@
group tag (queue). For instance, you could setup an "highmem", or "gpu" tag.
</Tooltip>
{/snippet}
<WorkerTagPicker bind:tag={script.tag} />
<WorkerTagPicker
bind:tag={script.tag}
placeholder={customUi?.tagSelectPlaceholder}
/>
</Section>
<Section label="Cache">
{#snippet header()}
@@ -1693,6 +1699,18 @@
{/if}
<div class="flex flex-row gap-x-1 lg:gap-x-2">
{#if $workerTags}
{#if $workerTags?.length ?? 0 > 0}
<div class="max-w-[200px] pr-8">
<WorkerTagSelect
inputClass="text-sm text-secondary !placeholder-secondary"
nullTag={script.language}
placeholder={customUi?.tagSelectPlaceholder}
bind:tag={script.tag}
/>
</div>
{/if}
{/if}
{#if customUi?.topBar?.settings != false}
<Button
aiId="script-builder-settings"
@@ -1733,6 +1751,7 @@
</div>
<ScriptEditor
{disableAi}
bind:selectedTab={selectedInputTab}
{customUi}
collabMode

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