mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-17 00:02:31 +00:00
Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
065b0efa85 | ||
|
|
83e5bfbc17 | ||
|
|
835f1d2ec9 | ||
|
|
d933648d36 | ||
|
|
2039c93d4c | ||
|
|
18ee03a323 | ||
|
|
4f6ad58e41 | ||
|
|
395f1ff8ba | ||
|
|
7f02e8020a | ||
|
|
7a8c6d9dbb | ||
|
|
44457c72cf | ||
|
|
91a5a549c3 | ||
|
|
900c8edd7b | ||
|
|
dc5e764d9d | ||
|
|
3f23198385 | ||
|
|
d24eea2fde | ||
|
|
3daf79ffbc | ||
|
|
b21a8da6c6 | ||
|
|
17872edb99 | ||
|
|
4ab7f2919e | ||
|
|
f3f0b3d01a | ||
|
|
db000508ec | ||
|
|
1bdd00a3e4 | ||
|
|
29719ac504 | ||
|
|
517b61e196 | ||
|
|
29f6fab60c | ||
|
|
e03246eadb | ||
|
|
7f58a1cb47 | ||
|
|
5722014651 | ||
|
|
3a1b43e8bc | ||
|
|
a7bba4674b | ||
|
|
d6a0c026d4 | ||
|
|
27e12a1527 | ||
|
|
18cb8324ed | ||
|
|
8ac16ca94b | ||
|
|
6f3cb5eabb | ||
|
|
6ac004ece5 | ||
|
|
5b5a64e6c2 | ||
|
|
172af24ead | ||
|
|
607c23dcfd | ||
|
|
d1c33ab974 | ||
|
|
bcac9f1844 | ||
|
|
9c2f6a757f | ||
|
|
1c85bbb05a | ||
|
|
1b1bee5b53 | ||
|
|
f70b6f3052 | ||
|
|
9466830810 | ||
|
|
f32159f412 | ||
|
|
67e6bce9b2 | ||
|
|
419defe05c | ||
|
|
f3ecbe1792 | ||
|
|
99e18aedea | ||
|
|
af6b724f0b | ||
|
|
88ab1a5136 | ||
|
|
3e82282351 | ||
|
|
b3b6c53430 | ||
|
|
934ae4fe57 | ||
|
|
86eb9074cc | ||
|
|
06e61ee958 | ||
|
|
b9e668b489 | ||
|
|
f2425362f9 | ||
|
|
4ae5928788 | ||
|
|
9462d56be7 | ||
|
|
c4adaeeabd | ||
|
|
e4255e6276 | ||
|
|
fa8d1b47db | ||
|
|
054f2c134a | ||
|
|
530a72ba83 | ||
|
|
2a334421e8 | ||
|
|
c7c2efbbe5 | ||
|
|
b1c4f8b29d | ||
|
|
14005fe4c1 | ||
|
|
4326bb8dc9 | ||
|
|
18d12525d2 | ||
|
|
47d1ef0f1c | ||
|
|
86b5fab4dc |
@@ -45,7 +45,7 @@ jobs:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.1.43
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
- uses: astral-sh/setup-uv@v6.2.1
|
||||
with:
|
||||
version: "0.6.2"
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
@@ -9,7 +9,14 @@ jobs:
|
||||
runs-on: ubicloud
|
||||
container: node:18
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: app
|
||||
with:
|
||||
app-id: ${{ vars.INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.INTERNAL_APP_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app.outputs.token }}
|
||||
- run: git config --system --add safe.directory /__w/windmill/windmill
|
||||
- name: Change versions
|
||||
run: ./.github/change-versions.sh "$(cat version.txt)"
|
||||
@@ -21,3 +28,8 @@ jobs:
|
||||
cd backend
|
||||
cargo generate-lockfile
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_user_name: windmill-internal-app[bot]
|
||||
commit_user_email: windmill-internal-app[bot]@users.noreply.github.com
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app.outputs.token }}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Check Organization Membership
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commenter:
|
||||
required: true
|
||||
type: string
|
||||
description: 'The username to check for organization membership'
|
||||
organization:
|
||||
required: false
|
||||
type: string
|
||||
default: 'windmill-labs'
|
||||
description: 'The organization to check membership for'
|
||||
trusted_bot:
|
||||
required: false
|
||||
type: string
|
||||
default: 'windmill-internal-app[bot]'
|
||||
description: 'The trusted bot username to allow'
|
||||
secrets:
|
||||
access_token:
|
||||
required: true
|
||||
description: 'The access token to use for org membership check'
|
||||
outputs:
|
||||
is_member:
|
||||
description: 'Whether the user is an organization member or trusted bot'
|
||||
value: ${{ jobs.check-membership.outputs.is_member }}
|
||||
|
||||
jobs:
|
||||
check-membership:
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
is_member: ${{ steps.check-membership.outputs.is_member }}
|
||||
steps:
|
||||
- name: Check organization membership
|
||||
id: check-membership
|
||||
env:
|
||||
ORG_ACCESS_TOKEN: ${{ secrets.access_token }}
|
||||
COMMENTER: ${{ inputs.commenter }}
|
||||
ORG: ${{ inputs.organization }}
|
||||
TRUSTED_BOT: ${{ inputs.trusted_bot }}
|
||||
run: |
|
||||
# 1. Allow the trusted bot straight away
|
||||
if [[ "$COMMENTER" == "$TRUSTED_BOT" ]]; then
|
||||
echo "is_member=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2. Otherwise fall back to the org-membership check
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token $ORG_ACCESS_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
|
||||
|
||||
if [ "$STATUS" -eq 204 ]; then
|
||||
echo "is_member=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_member=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -11,45 +11,40 @@ on:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
check-membership:
|
||||
determine-commenter:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai') && !contains(github.event.review.user.login, '[bot]')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai') && !contains(github.event.issue.user.login, '[bot]'))
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai'))
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
is_member: ${{ steps.check-membership.outputs.is_member }}
|
||||
commenter: ${{ steps.determine-commenter.outputs.commenter }}
|
||||
steps:
|
||||
- name: Check organization membership
|
||||
id: check-membership
|
||||
env:
|
||||
ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
|
||||
- name: Determine commenter
|
||||
id: determine-commenter
|
||||
run: |
|
||||
ORG="windmill-labs"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "issue_comment" || "${{ github.event_name }}" == "pull_request_review_comment" ]]; then
|
||||
# Work out who wrote the comment / review
|
||||
if [[ "${{ github.event_name }}" == "issue_comment" || \
|
||||
"${{ github.event_name }}" == "pull_request_review_comment" ]]; then
|
||||
COMMENTER="${{ github.event.comment.user.login }}"
|
||||
elif [[ "${{ github.event_name }}" == "pull_request_review" ]]; then
|
||||
COMMENTER="${{ github.event.review.user.login }}"
|
||||
else
|
||||
COMMENTER="${{ github.event.issue.user.login }}"
|
||||
fi
|
||||
echo "commenter=$COMMENTER" >> $GITHUB_OUTPUT
|
||||
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token $ORG_ACCESS_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
|
||||
|
||||
if [ "$STATUS" -eq 204 ]; then
|
||||
echo "is_member=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_member=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
check-membership:
|
||||
needs: determine-commenter
|
||||
uses: ./.github/workflows/check-org-membership.yml
|
||||
with:
|
||||
commenter: ${{ needs.determine-commenter.outputs.commenter }}
|
||||
secrets:
|
||||
access_token: ${{ secrets.ORG_ACCESS_TOKEN }}
|
||||
|
||||
claude-code-action:
|
||||
needs: check-membership
|
||||
needs: [determine-commenter, check-membership]
|
||||
if: |
|
||||
needs.check-membership.outputs.is_member == 'true'
|
||||
runs-on: ubicloud-standard-8
|
||||
@@ -64,21 +59,69 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Run npm install and generate-backend-client
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
# add a build directory for cargo check
|
||||
mkdir -p build
|
||||
npm install
|
||||
npm run generate-backend-client
|
||||
|
||||
- name: install xmlsec1
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-dev libxmlsec1-dev
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.85.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
|
||||
- name: cargo check
|
||||
working-directory: ./backend
|
||||
timeout-minutes: 16
|
||||
run: |
|
||||
SQLX_OFFLINE=true cargo check --features $(./all_features_oss.sh)
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@beta
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash(npm:*),Bash(cargo:*)"
|
||||
custom_instructions: "IMPORTANT INSTRUCTIONS:
|
||||
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main, with the title starting with [Claude PR].
|
||||
- If you made changes to the frontend code, run npm install, then npm run generate-backend-client, then npm run check. You can ignore warnings that are reported by the check script, but fix the errors.
|
||||
- If you made changes to the backend code, run cargo check. You can ignore warnings that are reported by the check script, but fix the errors.
|
||||
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
|
||||
AVAILABLE TOOLS:
|
||||
- mcp__github__create_pull_request: Create a PR from a branch to main
|
||||
- Bash(npm run check): Run the check script. You should run this tool after making changes to the frontend code.
|
||||
- Bash(npm install): Install dependencies. You need this to run npm run check.
|
||||
- Bash(npm run generate-backend-client): Generate the backend client. You need this to run npm run check.
|
||||
- Bash(cargo check): Run the cargo check script. You should run this tool after making changes to the backend code.
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash"
|
||||
custom_instructions: |
|
||||
## IMPORTANT INSTRUCTIONS
|
||||
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.
|
||||
|
||||
## Code Quality Requirements
|
||||
|
||||
After making any code changes, you MUST run the appropriate validation commands:
|
||||
|
||||
**Frontend Changes:**
|
||||
- Run: `npm run check` in the frontend directory
|
||||
- Fix all warnings and errors before proceeding
|
||||
|
||||
**Backend Changes:**
|
||||
- Run: `cargo check --features $(./all_features_oss.sh)` in the backend directory
|
||||
- Fix all warnings and errors before proceeding
|
||||
|
||||
**Pull Request Creation:**
|
||||
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
|
||||
|
||||
## Available Tools
|
||||
- mcp__github__create_pull_request: Create PRs from branches
|
||||
- Bash: Full access to run validation commands and git operations
|
||||
trigger_phrase: "/ai"
|
||||
|
||||
@@ -4,38 +4,36 @@ on:
|
||||
|
||||
jobs:
|
||||
check-membership:
|
||||
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && github.event.comment.user.type != 'Bot' }}
|
||||
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') }}
|
||||
uses: ./.github/workflows/check-org-membership.yml
|
||||
with:
|
||||
commenter: ${{ github.event.comment.user.login }}
|
||||
secrets:
|
||||
access_token: ${{ secrets.ORG_ACCESS_TOKEN }}
|
||||
|
||||
generate-token:
|
||||
needs: check-membership
|
||||
if: ${{ needs.check-membership.outputs.is_member == 'true' }}
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
is_member: ${{ steps.check-membership.outputs.is_member }}
|
||||
app_token: ${{ steps.app.outputs.token }}
|
||||
steps:
|
||||
- name: Check organization membership
|
||||
id: check-membership
|
||||
env:
|
||||
ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
|
||||
COMMENTER: ${{ github.event.comment.user.login }}
|
||||
run: |
|
||||
ORG="windmill-labs"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token $ORG_ACCESS_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
|
||||
|
||||
if [ "$STATUS" -eq 204 ]; then
|
||||
echo "is_member=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_member=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Generate an installation token
|
||||
id: app
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.INTERNAL_APP_KEY }}
|
||||
owner: windmill-labs
|
||||
|
||||
trigger-docs:
|
||||
needs: check-membership
|
||||
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && needs.check-membership.outputs.is_member == 'true' }}
|
||||
needs: [generate-token, check-membership]
|
||||
if: ${{ needs.check-membership.outputs.is_member == 'true' }}
|
||||
uses: windmill-labs/windmilldocs/.github/workflows/create-docs.yml@main
|
||||
with:
|
||||
pr_number: ${{ github.event.issue.number }}
|
||||
repo: ${{ github.event.repository.name }}
|
||||
comment_text: ${{ github.event.comment.body }}
|
||||
secrets:
|
||||
DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}
|
||||
DOCS_TOKEN: ${{ needs.generate-token.outputs.app_token }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
|
||||
|
||||
merge_success_emoji:
|
||||
if: github.event.pull_request.merged == true
|
||||
if: github.event.action == 'closed'
|
||||
uses: ./.github/workflows/shareable-discord-notification.yml
|
||||
with:
|
||||
PR_STATUS: "merged"
|
||||
|
||||
@@ -9,11 +9,19 @@ jobs:
|
||||
runs-on: ubicloud-standard-2
|
||||
|
||||
steps:
|
||||
- name: Generate an installation token
|
||||
id: app
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.INTERNAL_APP_KEY }}
|
||||
owner: windmill-labs
|
||||
|
||||
- name: Checkout on helm repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: windmill-labs/windmill-helm-charts
|
||||
token: ${{ secrets.HELM_CHART_TOKEN }}
|
||||
token: ${{ steps.app.outputs.token }}
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
@@ -49,6 +57,23 @@ jobs:
|
||||
APP_VERSION=${APP_VERSION%/}
|
||||
sed -i "s/appVersion: .*/appVersion: $APP_VERSION/" ./charts/windmill/Chart.yaml
|
||||
|
||||
- name: Close existing bump-helm PRs
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app.outputs.token }}
|
||||
run: |
|
||||
# List open PR numbers whose title starts with the prefix
|
||||
prs=$(gh pr list \
|
||||
--state open \
|
||||
--search '"helm: bump version to" in:title' \
|
||||
--json number \
|
||||
-q '.[].number')
|
||||
|
||||
for pr in $prs; do
|
||||
echo "Closing outdated bump PR #$pr"
|
||||
gh pr close "$pr" \
|
||||
--comment "Closed automatically – superseded by a newer Helm-chart bump PR."
|
||||
done
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
git add .
|
||||
@@ -57,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Create PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HELM_CHART_TOKEN }}
|
||||
GH_TOKEN: ${{ steps.app.outputs.token }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--title "helm: bump version to ${{ env.VERSION }}" \
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Auto Comment on PR Ready for Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
add-review-comment:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubicloud-standard-2
|
||||
steps:
|
||||
- name: Add review comment
|
||||
uses: actions/github-script@v7
|
||||
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'
|
||||
});
|
||||
+2
-1
@@ -11,4 +11,5 @@ CaddyfileRemoteMalo
|
||||
.dev-docker-wrapper*
|
||||
backend/.minio-data
|
||||
.aider*
|
||||
!.aiderignore
|
||||
!.aiderignore
|
||||
rust-client/Cargo.toml
|
||||
|
||||
@@ -1,5 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
## [1.501.3](https://github.com/windmill-labs/windmill/compare/v1.501.2...v1.501.3) (2025-06-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** return correct content-type for openapi spec ([#6045](https://github.com/windmill-labs/windmill/issues/6045)) ([44457c7](https://github.com/windmill-labs/windmill/commit/44457c72cf75c969de97c39bb23f57acad268e10))
|
||||
* **frontend:** load all flow jobs on page load ([#6029](https://github.com/windmill-labs/windmill/issues/6029)) ([dc5e764](https://github.com/windmill-labs/windmill/commit/dc5e764d9db9251dc356094d6ac47c45fdf72c74))
|
||||
* ignore type only imports when computing ts lockfiles ([900c8ed](https://github.com/windmill-labs/windmill/commit/900c8edd7b35802e23a1359029da8ddbfb783753))
|
||||
* improve ordering of forms for non complete ordering + array schema fix ([18ee03a](https://github.com/windmill-labs/windmill/commit/18ee03a32371885f5e608cb306b5ccbccc31dac5))
|
||||
* missing static_asset_config from api call ([#6058](https://github.com/windmill-labs/windmill/issues/6058)) ([395f1ff](https://github.com/windmill-labs/windmill/commit/395f1ff8ba05020d72d1d8b34bd6bb32517b7aec))
|
||||
|
||||
## [1.501.2](https://github.com/windmill-labs/windmill/compare/v1.501.1...v1.501.2) (2025-06-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve schema form handling of inconsistent order and properties ([3daf79f](https://github.com/windmill-labs/windmill/commit/3daf79ffbc45ca32ff443e5521a67d62528665db))
|
||||
|
||||
## [1.501.1](https://github.com/windmill-labs/windmill/compare/v1.501.0...v1.501.1) (2025-06-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* optimize jobs list run incremental refresh performance ([1bdd00a](https://github.com/windmill-labs/windmill/commit/1bdd00a3e4a94ecb23efb9614c341c64a67ac389))
|
||||
* pwsh skip already installed modules outside of cache ([#6037](https://github.com/windmill-labs/windmill/issues/6037)) ([29f6fab](https://github.com/windmill-labs/windmill/commit/29f6fab60c6f8cf251182a56c09bac7692868bae))
|
||||
|
||||
## [1.501.0](https://github.com/windmill-labs/windmill/compare/v1.500.3...v1.501.0) (2025-06-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ai flow chat prompt and UX improvements ([#5942](https://github.com/windmill-labs/windmill/issues/5942)) ([5722014](https://github.com/windmill-labs/windmill/commit/57220146513444436faff95f58c1b36481d1fa1d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve reactivity of apps ([27e12a1](https://github.com/windmill-labs/windmill/commit/27e12a1527c41ac801042038b707a94897e718f8))
|
||||
|
||||
## [1.500.3](https://github.com/windmill-labs/windmill/compare/v1.500.2...v1.500.3) (2025-06-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix conditional wrappre ([6f3cb5e](https://github.com/windmill-labs/windmill/commit/6f3cb5eabb7b2224d04ec10f151f67c0955a5cfd))
|
||||
|
||||
## [1.500.2](https://github.com/windmill-labs/windmill/compare/v1.500.1...v1.500.2) (2025-06-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* consistency of root job propagation fixing cases where runFlow in scripts would fail ([9c2f6a7](https://github.com/windmill-labs/windmill/commit/9c2f6a757fb168c7305c991c9fdbf78acd856a1c))
|
||||
|
||||
## [1.500.1](https://github.com/windmill-labs/windmill/compare/v1.500.0...v1.500.1) (2025-06-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* git repository resource picker effect loop ([#6017](https://github.com/windmill-labs/windmill/issues/6017)) ([1b1bee5](https://github.com/windmill-labs/windmill/commit/1b1bee5b53d78e4407b684b567d0fddd2b5283f3))
|
||||
|
||||
## [1.500.0](https://github.com/windmill-labs/windmill/compare/v1.499.0...v1.500.0) (2025-06-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add typescript client context to ai chat system prompt ([#6004](https://github.com/windmill-labs/windmill/issues/6004)) ([3e82282](https://github.com/windmill-labs/windmill/commit/3e822823519d1d5c22e422e4bd1ad4d37b6428b6))
|
||||
* blacklist remote agent worker token ([#5985](https://github.com/windmill-labs/windmill/issues/5985)) ([86eb907](https://github.com/windmill-labs/windmill/commit/86eb9074cc94f309f17ea72e9cecd0d502ffd2be))
|
||||
* **frontend:** run steps from graph ([#5915](https://github.com/windmill-labs/windmill/issues/5915)) ([67e6bce](https://github.com/windmill-labs/windmill/commit/67e6bce9b2eba1653450921afab3eabbd41fc715))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ai button in inline script editor to open AI chat in flow builder ([#5989](https://github.com/windmill-labs/windmill/issues/5989)) ([4ae5928](https://github.com/windmill-labs/windmill/commit/4ae5928788831196672e212b32ca410afab640e0))
|
||||
* improve piptar upload - sequential uploads via background task queue ([#5994](https://github.com/windmill-labs/windmill/issues/5994)) ([c4adaee](https://github.com/windmill-labs/windmill/commit/c4adaeeabd287ca1c4f3522bcd8bcea30b00fe6d))
|
||||
* new MultiSelect component ([#5979](https://github.com/windmill-labs/windmill/issues/5979)) ([fa8d1b4](https://github.com/windmill-labs/windmill/commit/fa8d1b47db19e15fe854e01f9987c8f97cb45b44))
|
||||
* replace worker tags to listen multiselect ([#5997](https://github.com/windmill-labs/windmill/issues/5997)) ([e4255e6](https://github.com/windmill-labs/windmill/commit/e4255e6276565c4a45b1f45a5d627bcfb5369270))
|
||||
|
||||
## [1.499.0](https://github.com/windmill-labs/windmill/compare/v1.498.0...v1.499.0) (2025-06-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* devOps role can edit worker groups ([#5984](https://github.com/windmill-labs/windmill/issues/5984)) ([b1c4f8b](https://github.com/windmill-labs/windmill/commit/b1c4f8b29d0fb4cad76853110b84a87892b54661))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent keypress events from bubbling in decision tree drawer ([#5993](https://github.com/windmill-labs/windmill/issues/5993)) ([2a33442](https://github.com/windmill-labs/windmill/commit/2a334421e85abf046784aab57522582439ef2901))
|
||||
|
||||
## [1.498.0](https://github.com/windmill-labs/windmill/compare/v1.497.2...v1.498.0) (2025-06-17)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
To have an overview of what this app does, see @.cursor/rules/windmill-overview.mdc
|
||||
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc. You also have access to a summarized version of the database schema here @backend/summarized_schema.txt
|
||||
For frontend modifications, follow the rules mentioned here @.cursor/rules/svelte5-best-practices.mdc
|
||||
# Windmill Development Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details.
|
||||
|
||||
## Language-Specific Guides
|
||||
|
||||
- Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt
|
||||
- Frontend (Svelte 5): @frontend/svelte5-best-practices.mdc
|
||||
|
||||
+4
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n id As \"id!\",\n flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json<Uuid>\"\n FROM v2_as_queue\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id AND workspace_id = $2",
|
||||
"query": "SELECT\n id As \"id!\",\n flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json<Uuid>\"\n FROM v2_job_status\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -16,14 +16,13 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3c0b2a840102b12864c5d721b8e0142602ab37f3e1a95d39b3c7cbd7ff34d0b2"
|
||||
"hash": "019100d178129340a7c35d60ab61f983c8a9cb810db4369554bf26c6b0d6003d"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM app WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "08c827d9b2de0b77ce0ea2653760751615112c501b35e931ed817dbefd7c6bdb"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n ORDER BY blacklisted_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "blacklisted_at",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "blacklisted_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1c5d3556fc8436ddd294f39c5431e1f501a821d6143c5d8aece20814237a6b86"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "CREATE INDEX CONCURRENTLY idx_audit_recent_login_activities \nON audit (timestamp, username) \nWHERE operation IN ('users.login', 'oauth.login', 'users.token.refresh');",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "222e29b89d10f3840d4e9b9ab63207df3cbab63c83d4a6374e72a11893841653"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM agent_token_blacklist WHERE token = $1 AND expires_at > $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Timestamp"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2bf99d540365c228e1776ee5d2ba01ebe289183526afab19c1390bbf5082f019"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM token WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2f30274b0fe89aa1579b252b990876e5035ca5b31a68fcf08701102a6457e5c4"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM raw_app WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3b5295a7c4b99aefa52c9a8ae1e0dd12bf4a0be1bf755caf7a1fa863e7950562"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM flow WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "52032730f2eeaaeab55305f72bea5481d1c50c2eaa92a97a078239430f0d6c13"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM agent_token_blacklist WHERE token = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "54fee31b61d62598c89cf7d0729079ac1721fe7bd1844f339236379211defc78"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM variable WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a31b32659a0ac6a6ad0e122a4d475787240d6714ddadf16296d2b7bd5fdcb52"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM resource WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "7c765f50c67b0ef751bafc1bf9279c4cb8a851dfab406ba7611f77773663e9f3"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM agent_token_blacklist WHERE expires_at <= now() RETURNING token",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "995b194da28092d5aa053df936e7a9ee4b80cf3ade038a032c57ecff8fa3c6cf"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM workspace WHERE owner = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9d488c5ba4b9f5203692721d76ec831f5954861a5576e0d8c1c42a9eca90927f"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT leaf_jobs->$1::text AS \"leaf_jobs: Json<Box<RawValue>>\", parent_job\n FROM v2_as_queue\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = id AND workspace_id = $3",
|
||||
"query": "SELECT flow_leaf_jobs->$1::text AS \"leaf_jobs: Json<Box<RawValue>>\", v2_job.parent_job\n FROM v2_job_status\n LEFT JOIN v2_job ON v2_job.id = v2_job_status.id AND v2_job.workspace_id = $3\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = v2_job_status.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -26,5 +26,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cf12a70e7b75ae471a0944de34502384be156cf25129f9c52bda34b240cf469a"
|
||||
"hash": "b46a0fbebdc8e5e9852a06444b0aeaa4eaf67959e68b69eb2f0896ebe9244691"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -97,10 +97,11 @@
|
||||
"Bool",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"TextArray"
|
||||
"TextArray",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cccdcb7fe7968eadfc04d8957a8e98b2f2d92a6d7f687a9dd5a70edb3d5a63e6"
|
||||
"hash": "b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO agent_token_blacklist (token, expires_at, blacklisted_by) \n VALUES ($1, $2, $3) \n ON CONFLICT (token) DO UPDATE SET \n expires_at = EXCLUDED.expires_at,\n blacklisted_at = NOW(),\n blacklisted_by = EXCLUDED.blacklisted_by",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Timestamp",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM script WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "cb8bde4d92a020278cbae79c5c01a766c198392aceb38fb27e57b73de8f7f279"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n WHERE expires_at > $1 \n ORDER BY blacklisted_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "blacklisted_at",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "blacklisted_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamp"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d56722c25877222af9affd5da5bb83b28fa8cbb528a2cfc90684cb10a69e4375"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Backend Development (Rust)
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Follow @rust-best-practices.mdc for detailed guidelines
|
||||
- Database schema reference: @summarized_schema.txt
|
||||
- The API routes prefixes are all listed in windmill-api/src/lib.rs
|
||||
|
||||
## Adding New Features
|
||||
|
||||
1. Update database schema with migration if necessary
|
||||
2. Update backend/windmill-api/openapi.yaml after modifying API endpoints
|
||||
Generated
+249
-188
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.498.0"
|
||||
version = "1.501.3"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.498.0"
|
||||
version = "1.501.3"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -329,7 +329,7 @@ jsonwebtoken = "8.3.0"
|
||||
pem = "3.0.1"
|
||||
nix = { version = "0.27.1", features = ["process", "signal"] }
|
||||
tinyvector = { git = "https://github.com/windmill-labs/tinyvector", rev = "20823b94c20f2b9093f318badd24026cf54dcc85" }
|
||||
hf-hub = "0.3.2"
|
||||
hf-hub = "0.4.3"
|
||||
tokenizers = "0.14.1"
|
||||
candle-core = "0.9.1"
|
||||
candle-transformers = "0.9.1"
|
||||
|
||||
@@ -1 +1 @@
|
||||
67e727c618cf673850a0887931c803241abfcfe8
|
||||
835a91c7c31ea749759cd8af0922ad837049ea2a
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Remove agent token blacklist table
|
||||
DROP TABLE IF EXISTS agent_token_blacklist;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Add agent token blacklist table
|
||||
CREATE TABLE agent_token_blacklist (
|
||||
token VARCHAR PRIMARY KEY,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
blacklisted_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
blacklisted_by VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
-- Add index for efficient expiry cleanup
|
||||
CREATE INDEX idx_agent_token_blacklist_expires_at ON agent_token_blacklist(expires_at);
|
||||
|
||||
-- Grant permissions to windmill users
|
||||
GRANT ALL ON agent_token_blacklist TO windmill_user;
|
||||
GRANT ALL ON agent_token_blacklist TO windmill_admin;
|
||||
@@ -30,8 +30,6 @@ use windmill_common::{
|
||||
worker::PythonAnnotations,
|
||||
};
|
||||
|
||||
const DEF_MAIN: &str = "def main(";
|
||||
|
||||
fn replace_import(x: String) -> String {
|
||||
SHORT_IMPORTS_MAP
|
||||
.get(&x)
|
||||
@@ -48,6 +46,9 @@ lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)\s*$").unwrap();
|
||||
static ref PIN_RE: Regex = Regex::new(r"(?:\s*#\s*(pin|repin):\s*)(\S*)").unwrap();
|
||||
static ref PKG_RE: Regex = Regex::new(r"^([^!=<>]+)(?:[!=<>]|$)").unwrap();
|
||||
// Regex to properly match main function definition at line start,
|
||||
// capturing both sync and async variants
|
||||
static ref DEF_MAIN_RE: Regex = Regex::new(r"(?m)^(async\s+)?def\s+main\s*\(").unwrap();
|
||||
}
|
||||
|
||||
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<NImport> {
|
||||
@@ -143,7 +144,12 @@ struct ImportPin {
|
||||
}
|
||||
|
||||
fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<NImport>> {
|
||||
let mut code = code.split(DEF_MAIN).next().unwrap_or("").to_string();
|
||||
// Use regex to safely find the main function definition
|
||||
let mut code = DEF_MAIN_RE
|
||||
.split(code)
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
// remove main function decorator from end of file if it exists
|
||||
if code
|
||||
@@ -160,10 +166,17 @@ fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<NImport>>
|
||||
+ "\n";
|
||||
}
|
||||
|
||||
let ast = Suite::parse(&code, "main.py").map_err(|e| {
|
||||
// Add a fake main function to ensure the parser can process the code correctly
|
||||
// This is needed because we've split off the real main function above
|
||||
let code_with_fake_main = format!("{}\n\ndef main(): pass", code);
|
||||
|
||||
let ast = Suite::parse(&code_with_fake_main, "main.py").map_err(|e| {
|
||||
error::Error::ExecutionErr(format!("Error parsing code for imports: {}", e.to_string()))
|
||||
})?;
|
||||
|
||||
// Note: We're still using the original code for finding pins,
|
||||
// as the TextRange values from the parsed AST would be based on code_with_fake_main
|
||||
// but we want to match against the original code
|
||||
let find_pin = |range: TextRange, key: String| {
|
||||
let hs = code
|
||||
.chars()
|
||||
@@ -378,7 +391,7 @@ async fn parse_python_imports_inner(
|
||||
})
|
||||
.join("\n")
|
||||
.parse::<toml::Table>()
|
||||
.map_err(to_anyhow)?;
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
{
|
||||
if let Some(v) = metadata.get("requires-python").and_then(|v| v.as_str()) {
|
||||
|
||||
@@ -232,7 +232,14 @@ fn parse_typ(id: &str) -> Typ {
|
||||
x @ _ if x.starts_with("DynSelect_") => {
|
||||
Typ::DynSelect(x.strip_prefix("DynSelect_").unwrap().to_string())
|
||||
}
|
||||
_ => Typ::Resource(id.to_string()),
|
||||
_ => Typ::Resource(map_resource_name(id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_resource_name(x: &str) -> String {
|
||||
match x {
|
||||
"S3Object" => "s3_object".to_string(),
|
||||
_ => x.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,7 +475,7 @@ def main(test1: str,
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "s3o".to_string(),
|
||||
typ: Typ::Resource("S3Object".to_string()),
|
||||
typ: Typ::Resource("s3_object".to_string()),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
|
||||
@@ -29,12 +29,36 @@ use wasm_bindgen::prelude::*;
|
||||
|
||||
struct ImportsFinder {
|
||||
imports: HashSet<String>,
|
||||
skip_type_only: bool,
|
||||
}
|
||||
|
||||
impl Visit for ImportsFinder {
|
||||
noop_visit_type!();
|
||||
|
||||
fn visit_import_decl(&mut self, n: &swc_ecma_ast::ImportDecl) {
|
||||
if self.skip_type_only {
|
||||
if n.type_only {
|
||||
return;
|
||||
}
|
||||
if n.specifiers.len() > 0 {
|
||||
let mut is_type_only = true;
|
||||
|
||||
for specifier in n.specifiers.iter() {
|
||||
match specifier {
|
||||
swc_ecma_ast::ImportSpecifier::Named(
|
||||
swc_ecma_ast::ImportNamedSpecifier { is_type_only, .. },
|
||||
) if *is_type_only => (),
|
||||
_ => {
|
||||
is_type_only = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_type_only {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ref s) = n.src.raw {
|
||||
let s = s.to_string();
|
||||
if s.starts_with("'") && s.ends_with("'") {
|
||||
@@ -46,7 +70,7 @@ impl Visit for ImportsFinder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
|
||||
pub fn parse_expr_for_imports(code: &str, skip_type_only: bool) -> anyhow::Result<Vec<String>> {
|
||||
let cm: Lrc<SourceMap> = Default::default();
|
||||
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.into());
|
||||
let mut tss = TsSyntax::default();
|
||||
@@ -72,7 +96,7 @@ pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
|
||||
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
|
||||
})?;
|
||||
|
||||
let mut visitor = ImportsFinder { imports: HashSet::new() };
|
||||
let mut visitor = ImportsFinder { imports: HashSet::new(), skip_type_only };
|
||||
visitor.visit_module(&expr);
|
||||
|
||||
let mut imports: Vec<_> = visitor.imports.into_iter().collect();
|
||||
@@ -318,7 +342,7 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
pub fn remove_pinned_imports(code: &str) -> anyhow::Result<String> {
|
||||
let mut imports = parse_expr_for_imports(code)?;
|
||||
let mut imports = parse_expr_for_imports(code, false)?;
|
||||
imports.sort_by_key(|f| 0 - (f.len() as i32));
|
||||
let mut content = code.to_string();
|
||||
for import in imports {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
mod tests {
|
||||
use windmill_parser_ts::parse_expr_for_imports;
|
||||
|
||||
#[test]
|
||||
fn test_imports() {
|
||||
let code = r#"
|
||||
import { foo } from "bar";
|
||||
import type { foo } from "bar2";
|
||||
import { type foo, bar } from "bar3";
|
||||
import { bar, type foo } from "bar7";
|
||||
|
||||
import { type foo, type bar } from "bar4";
|
||||
import * as foo from "bar5";
|
||||
import foo from "bar6";
|
||||
"#;
|
||||
let imports = parse_expr_for_imports(code, true).unwrap();
|
||||
assert_eq!(imports, vec!["bar", "bar3", "bar5", "bar6", "bar7"]);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ pub fn parse_outputs(code: &str) -> String {
|
||||
#[cfg(feature = "ts-parser")]
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_ts_imports(code: &str) -> String {
|
||||
let parsed = parse_expr_for_imports(code);
|
||||
let parsed = parse_expr_for_imports(code, false);
|
||||
let r = if let Ok(parsed) = parsed {
|
||||
json!({ "imports": parsed })
|
||||
} else {
|
||||
|
||||
@@ -339,7 +339,7 @@ fn test_parse_imports() -> anyhow::Result<()> {
|
||||
import { bar } from \"bar/foo/d\";
|
||||
import { bar as baroof } from \"bar\";
|
||||
";
|
||||
let mut l = parse_expr_for_imports(code)?;
|
||||
let mut l = parse_expr_for_imports(code, false)?;
|
||||
l.sort();
|
||||
assert_eq!(
|
||||
l,
|
||||
@@ -360,7 +360,7 @@ fn test_parse_imports_dts() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
export type foo = number
|
||||
";
|
||||
let mut l = parse_expr_for_imports(code)?;
|
||||
let mut l = parse_expr_for_imports(code, false)?;
|
||||
l.sort();
|
||||
assert_eq!(l, vec![] as Vec<String>);
|
||||
|
||||
|
||||
@@ -92,6 +92,12 @@ mod test {
|
||||
fn test_snake_case() {
|
||||
assert_eq!("s3", to_snake_case("S3"));
|
||||
assert_eq!("s3", to_snake_case("s3"));
|
||||
assert_eq!("s3_object", to_snake_case("S3Object"));
|
||||
assert_eq!("s3_object", to_snake_case("S3object"));
|
||||
assert_eq!("s3_object", to_snake_case("s3object"));
|
||||
assert_eq!("abc", to_snake_case("ABC"));
|
||||
assert_eq!("aa_bc", to_snake_case("AaBC"));
|
||||
assert_eq!("a_b_c", to_snake_case("A_B_C"));
|
||||
assert_eq!("s_3", to_snake_case("S_3"));
|
||||
assert_eq!("type_name_here", to_snake_case("typeNameHere"));
|
||||
}
|
||||
|
||||
@@ -1102,6 +1102,9 @@ Windmill Community Edition {GIT_VERSION}
|
||||
_ = tokio::time::sleep(Duration::from_secs(12 * 60 * 60)) => {
|
||||
tracing::info!("Reloading config after 12 hours");
|
||||
initial_load(&conn, tx.clone(), worker_mode, server_mode, #[cfg(feature = "parquet")] disable_s3_store).await;
|
||||
if let Err(e) = reload_license_key(&conn).await {
|
||||
tracing::error!("Failed to reload license key on agent: {e:#}");
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
ee_oss::verify_license_key().await;
|
||||
}
|
||||
|
||||
@@ -840,6 +840,24 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
tracing::error!("Error deleting audit log on CE: {:?}", e);
|
||||
}
|
||||
|
||||
match sqlx::query_scalar!(
|
||||
"DELETE FROM agent_token_blacklist WHERE expires_at <= now() RETURNING token",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
{
|
||||
Ok(deleted_tokens) => {
|
||||
if deleted_tokens.len() > 0 {
|
||||
tracing::info!(
|
||||
"deleted {} expired blacklisted agent tokens: {:?}",
|
||||
deleted_tokens.len(),
|
||||
deleted_tokens
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting expired blacklisted agent tokens: {:?}", e),
|
||||
}
|
||||
|
||||
let job_retention_secs = *JOB_RETENTION_SECS.read().await;
|
||||
if job_retention_secs > 0 {
|
||||
match db.begin().await {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.498.0
|
||||
version: 1.501.3
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -11240,6 +11240,98 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/agent_workers/blacklist_token:
|
||||
post:
|
||||
summary: blacklist agent token (requires super admin)
|
||||
operationId: blacklistAgentToken
|
||||
tags:
|
||||
- agent_workers
|
||||
requestBody:
|
||||
description: token to blacklist
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The agent token to blacklist
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Optional expiration date for the blacklist entry
|
||||
required:
|
||||
- token
|
||||
responses:
|
||||
"200":
|
||||
description: token blacklisted successfully
|
||||
|
||||
/agent_workers/remove_blacklist_token:
|
||||
post:
|
||||
summary: remove agent token from blacklist (requires super admin)
|
||||
operationId: removeBlacklistAgentToken
|
||||
tags:
|
||||
- agent_workers
|
||||
requestBody:
|
||||
description: token to remove from blacklist
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The agent token to remove from blacklist
|
||||
required:
|
||||
- token
|
||||
responses:
|
||||
"200":
|
||||
description: token removed from blacklist successfully
|
||||
|
||||
/agent_workers/list_blacklisted_tokens:
|
||||
get:
|
||||
summary: list blacklisted agent tokens (requires super admin)
|
||||
operationId: listBlacklistedAgentTokens
|
||||
tags:
|
||||
- agent_workers
|
||||
parameters:
|
||||
- name: include_expired
|
||||
in: query
|
||||
description: Whether to include expired blacklisted tokens
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
"200":
|
||||
description: list of blacklisted tokens
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The blacklisted token (without prefix)
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the blacklist entry expires
|
||||
blacklisted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the token was blacklisted
|
||||
blacklisted_by:
|
||||
type: string
|
||||
description: Email of the user who blacklisted the token
|
||||
required:
|
||||
- token
|
||||
- expires_at
|
||||
- blacklisted_at
|
||||
- blacklisted_by
|
||||
|
||||
/w/{workspace}/acls/get/{kind}/{path}:
|
||||
get:
|
||||
|
||||
@@ -16,9 +16,6 @@ use crate::db::DB;
|
||||
#[cfg(not(feature = "private"))]
|
||||
use axum::Router;
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -44,15 +41,6 @@ pub fn workspaced_service(
|
||||
(router, vec![], Some(job_completed_tx))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub struct AgentAuth {
|
||||
pub worker_group: String,
|
||||
pub suffix: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub exp: Option<usize>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub struct AgentCache {}
|
||||
|
||||
|
||||
@@ -917,6 +917,23 @@ async fn create_app_internal<'a>(
|
||||
raw_app: bool,
|
||||
mut app: CreateApp,
|
||||
) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> {
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_apps =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM app WHERE workspace_id = $1", &w_id)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_apps.unwrap_or(0) >= 1000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of apps (1000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if app.summary.len() > 300 {
|
||||
return Err(Error::BadRequest(
|
||||
"Summary must be less than 300 characters on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
|
||||
app.policy.on_behalf_of_email = Some(authed.email.clone());
|
||||
@@ -2011,7 +2028,7 @@ async fn upload_s3_file_from_app(
|
||||
|
||||
if !has_unnamed_policy {
|
||||
return Err(Error::BadRequest(
|
||||
"no policy found for unnamed s3 file uplooad".to_string(),
|
||||
"no policy found for unnamed s3 file upload".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use windmill_common::{
|
||||
DB,
|
||||
};
|
||||
|
||||
use crate::{db::ApiAuthed, utils::require_super_admin};
|
||||
use crate::{db::ApiAuthed, utils::{require_devops_role}};
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -103,7 +103,7 @@ async fn get_config(
|
||||
Path(name): Path<String>,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> error::JsonResult<Option<serde_json::Value>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
let config = sqlx::query_as!(Config, "SELECT * FROM config WHERE name = $1", name)
|
||||
.fetch_optional(&db)
|
||||
@@ -119,7 +119,7 @@ async fn update_config(
|
||||
authed: ApiAuthed,
|
||||
Json(config): Json<serde_json::Value>,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if name.starts_with("worker__") {
|
||||
@@ -157,7 +157,7 @@ async fn delete_config(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
@@ -232,7 +232,7 @@ async fn list_configs(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> error::JsonResult<Vec<Config>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
let configs = sqlx::query_as!(Config, "SELECT name, config FROM config")
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
@@ -812,6 +812,16 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
|
||||
run_windmill_migration!("audit_recent_login_activities", db, |tx| {
|
||||
sqlx::query!(
|
||||
"CREATE INDEX CONCURRENTLY idx_audit_recent_login_activities
|
||||
ON audit (timestamp, username)
|
||||
WHERE operation IN ('users.login', 'oauth.login', 'users.token.refresh');"
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use candle_nn::VarBuilder;
|
||||
#[cfg(feature = "embedding")]
|
||||
use candle_transformers::models::bert::{BertModel, Config, DTYPE};
|
||||
#[cfg(feature = "embedding")]
|
||||
use hf_hub::{api::sync::Api, Cache, Repo};
|
||||
use hf_hub::api::tokio::Api;
|
||||
#[cfg(feature = "embedding")]
|
||||
use serde::Deserialize;
|
||||
#[cfg(feature = "embedding")]
|
||||
@@ -158,63 +158,22 @@ pub struct ModelInstance {
|
||||
#[cfg(feature = "embedding")]
|
||||
impl ModelInstance {
|
||||
pub async fn load_model_files() -> Result<(PathBuf, PathBuf, PathBuf)> {
|
||||
let repo = Repo::model("thenlper/gte-small".to_string());
|
||||
|
||||
let cache = Cache::default().repo(repo.clone());
|
||||
|
||||
let api = Api::new()?;
|
||||
let api = api.repo(repo);
|
||||
let repo_api = api.model("thenlper/gte-small".to_string());
|
||||
|
||||
let (config_filename, tokenizer_filename, weights_filename) = (
|
||||
cache
|
||||
.get("config.json")
|
||||
.or_else(|| {
|
||||
api.get("config.json")
|
||||
.or_else(|e| {
|
||||
tracing::error!("Failed to get config.json from hugging face: {}", e);
|
||||
return Err(e);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.ok_or(Error::msg("could not get config.json"))?,
|
||||
cache
|
||||
.get("tokenizer.json")
|
||||
.or_else(|| {
|
||||
api.get("tokenizer.json")
|
||||
.or_else(|e| {
|
||||
tracing::error!(
|
||||
"Failed to get tokenizer.json from hugging face: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.ok_or(Error::msg("could not get tokenizer.json"))?,
|
||||
cache
|
||||
.get("model.safetensors")
|
||||
.and_then(|p| {
|
||||
tracing::info!("Found embedding model in cache");
|
||||
Some(p)
|
||||
})
|
||||
.or_else(|| {
|
||||
tracing::info!("Downloading embedding model...");
|
||||
api.get("model.safetensors")
|
||||
.or_else(|e| {
|
||||
tracing::error!(
|
||||
"Failed to get model.safetensors from hugging face: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
})
|
||||
.ok()
|
||||
.and_then(|p| {
|
||||
tracing::info!("Downloaded embedding model");
|
||||
Some(p)
|
||||
})
|
||||
})
|
||||
.ok_or(Error::msg("could not get model.safetensors"))?,
|
||||
);
|
||||
let (config_filename, tokenizer_filename, weights_filename) =
|
||||
(
|
||||
repo_api
|
||||
.get("config.json")
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to get config.json from hugging face: {}", e))?,
|
||||
repo_api.get("tokenizer.json").await.map_err(|e| {
|
||||
anyhow!("Failed to get tokenizer.json from hugging face: {}", e)
|
||||
})?,
|
||||
repo_api.get("model.safetensors").await.map_err(|e| {
|
||||
anyhow!("Failed to get model.safetensors from hugging face: {}", e)
|
||||
})?,
|
||||
);
|
||||
|
||||
Ok((config_filename, tokenizer_filename, weights_filename))
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ use sqlx::{FromRow, Postgres, Transaction};
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::utils::query_elems_from_hub;
|
||||
use windmill_common::worker::to_raw_value;
|
||||
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
|
||||
use windmill_common::HUB_BASE_URL;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
@@ -358,6 +358,32 @@ async fn create_flow(
|
||||
Path(w_id): Path<String>,
|
||||
Json(nf): Json<NewFlow>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_flows =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM flow WHERE workspace_id = $1", &w_id)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_flows.unwrap_or(0) >= 1000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of flows (1000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if nf.summary.len() > 300 {
|
||||
return Err(Error::BadRequest(
|
||||
"Summary must be less than 300 characters on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
if nf
|
||||
.description
|
||||
.as_ref()
|
||||
.is_some_and(|desc| desc.len() > 3000)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
"Description must be less than 3000 characters on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if nf
|
||||
.value
|
||||
|
||||
@@ -3564,7 +3564,7 @@ pub async fn run_flow_by_path_inner(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -3657,7 +3657,7 @@ pub async fn restart_flow(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -3749,7 +3749,7 @@ pub async fn run_script_by_path_inner(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -4417,7 +4417,7 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
None,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -4557,7 +4557,7 @@ pub async fn run_wait_result_script_by_path_internal(
|
||||
None,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -4670,7 +4670,7 @@ pub async fn run_wait_result_script_by_hash(
|
||||
None,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -4784,7 +4784,7 @@ pub async fn run_wait_result_flow_by_path_internal(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -5625,7 +5625,7 @@ pub async fn run_job_by_hash_inner(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.root_job.or(run_query.parent_job),
|
||||
run_query.root_job,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
@@ -5875,6 +5875,7 @@ pub fn filter_list_completed_query(
|
||||
sqlb.and_where_le("started_at", "?".bind(&dt.to_rfc3339()));
|
||||
}
|
||||
if let Some(dt) = &lq.created_or_started_after {
|
||||
sqlb.and_where_ge("created_at", "?".bind(&dt.to_rfc3339()));
|
||||
sqlb.and_where_ge("started_at", "?".bind(&dt.to_rfc3339()));
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,10 @@ use anyhow::Context;
|
||||
use argon2::Argon2;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::{middleware::from_extractor, routing::get, routing::post, Extension, Router};
|
||||
use axum::response::Response;
|
||||
use axum::http::HeaderValue;
|
||||
use axum::body::Body;
|
||||
use db::DB;
|
||||
use http::HeaderValue;
|
||||
use reqwest::Client;
|
||||
#[cfg(feature = "oauth2")]
|
||||
use std::collections::HashMap;
|
||||
@@ -236,6 +238,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Compliance with cloud events spec.
|
||||
pub async fn add_webhook_allowed_origin(
|
||||
req: axum::extract::Request,
|
||||
@@ -258,6 +261,7 @@ pub async fn add_webhook_allowed_origin(
|
||||
next.run(req).await
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
type IndexReader = ();
|
||||
|
||||
@@ -890,12 +894,18 @@ async fn ee_license() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
async fn openapi() -> &'static str {
|
||||
include_str!("../openapi-deref.yaml")
|
||||
async fn openapi() -> Response {
|
||||
Response::builder()
|
||||
.header("content-type", "application/yaml")
|
||||
.body(Body::from(include_str!("../openapi-deref.yaml")))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn openapi_json() -> &'static str {
|
||||
include_str!("../openapi-deref.json")
|
||||
async fn openapi_json() -> Response {
|
||||
Response::builder()
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(include_str!("../openapi-deref.json")))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn migrate_db(db: &DB) -> anyhow::Result<Option<JoinHandle<()>>> {
|
||||
|
||||
@@ -387,7 +387,8 @@ impl Runner {
|
||||
item_type: &str,
|
||||
) -> Result<Vec<T>, Error> {
|
||||
let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type));
|
||||
sqlb.fields(&["o.path", "o.summary", "o.description", "o.schema"]);
|
||||
let fields = vec!["o.path", "o.summary", "o.description", "o.schema"];
|
||||
sqlb.fields(&fields);
|
||||
if scope_type == "favorites" {
|
||||
sqlb.join("favorite")
|
||||
.on("favorite.favorite_kind = ? AND favorite.workspace_id = o.workspace_id AND favorite.path = o.path AND favorite.usr = ?".bind(&item_type)
|
||||
@@ -395,16 +396,21 @@ impl Runner {
|
||||
}
|
||||
sqlb.and_where("o.workspace_id = ?".bind(&workspace_id))
|
||||
.and_where("o.archived = false")
|
||||
.and_where("o.draft_only IS NOT TRUE")
|
||||
.order_by(
|
||||
if item_type == "flow" {
|
||||
"o.edited_at"
|
||||
} else {
|
||||
"o.created_at"
|
||||
},
|
||||
false,
|
||||
)
|
||||
.limit(100);
|
||||
.and_where("o.draft_only IS NOT TRUE");
|
||||
|
||||
if item_type == "script" {
|
||||
sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)");
|
||||
}
|
||||
|
||||
sqlb.order_by(
|
||||
if item_type == "flow" {
|
||||
"o.edited_at"
|
||||
} else {
|
||||
"o.created_at"
|
||||
},
|
||||
false,
|
||||
)
|
||||
.limit(100);
|
||||
let sql = sqlb.sql().map_err(|_e| {
|
||||
tracing::error!("failed to build sql: {}", _e);
|
||||
Error::internal_error("failed to build sql", None)
|
||||
|
||||
@@ -29,6 +29,7 @@ use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult, Result},
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -149,9 +150,29 @@ async fn create_app(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(app): Json<CreateApp>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_apps = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM raw_app WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_apps.unwrap_or(0) >= 1000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of apps (1000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if app.summary.len() > 300 {
|
||||
return Err(Error::BadRequest(
|
||||
"Summary must be less than 300 characters on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
if &app.path == "" {
|
||||
return Err(Error::BadRequest("App path cannot be empty".to_string()));
|
||||
|
||||
@@ -33,6 +33,7 @@ use windmill_common::{
|
||||
error::{Error, JsonResult, Result},
|
||||
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
variables,
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -583,7 +584,6 @@ pub async fn transform_json_value<'c>(
|
||||
job.schedule_path.clone(),
|
||||
job.flow_step_id.clone(),
|
||||
job.root_job.map(|x| x.to_string()),
|
||||
None,
|
||||
Some(job.scheduled_for.clone()),
|
||||
)
|
||||
.await;
|
||||
@@ -657,6 +657,20 @@ async fn create_resource(
|
||||
Query(q): Query<CreateResourceQuery>,
|
||||
Json(resource): Json<CreateResource>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_resources = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM resource WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_resources.unwrap_or(0) >= 10000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of resources (10000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let authed = maybe_refresh_folders(&resource.path, &w_id, authed, &db).await;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
@@ -42,7 +42,7 @@ use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_worker::process_relative_imports;
|
||||
|
||||
use windmill_common::error::to_anyhow;
|
||||
use windmill_common::{error::to_anyhow, worker::CLOUD_HOSTED};
|
||||
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
@@ -520,6 +520,29 @@ async fn create_script_internal<'c>(
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_scripts =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM script WHERE workspace_id = $1", &w_id)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_scripts.unwrap_or(0) >= 5000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of scripts (5000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if ns.summary.len() > 300 {
|
||||
return Err(Error::BadRequest(
|
||||
"Summary must be less than 300 characters on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
if ns.description.len() > 3000 {
|
||||
return Err(Error::BadRequest(
|
||||
"Description must be less than 3000 characters on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let script_path = ns.path.clone();
|
||||
let hash = ScriptHash(hash_script(&ns));
|
||||
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
|
||||
|
||||
@@ -10,6 +10,8 @@ use axum::{body::Body, extract::OriginalUri, http::Response, response::IntoRespo
|
||||
|
||||
#[cfg(feature = "static_frontend")]
|
||||
use axum::http::header;
|
||||
#[cfg(feature = "static_frontend")]
|
||||
use http::HeaderValue;
|
||||
|
||||
use hyper::Uri;
|
||||
#[cfg(feature = "static_frontend")]
|
||||
@@ -17,6 +19,12 @@ use mime_guess::mime;
|
||||
#[cfg(feature = "static_frontend")]
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
// Content Security Policy configuration
|
||||
#[cfg(feature = "static_frontend")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref CSP_POLICY: String = std::env::var("CSP_POLICY").unwrap_or_default();
|
||||
}
|
||||
|
||||
// static_handler is a handler that serves static files from the
|
||||
pub async fn static_handler(OriginalUri(original_uri): OriginalUri) -> StaticFile {
|
||||
StaticFile(original_uri)
|
||||
@@ -51,6 +59,13 @@ fn serve_path(path: &str) -> Response<Body> {
|
||||
let mut res = Response::builder()
|
||||
.header(header::CONTENT_TYPE, mime.as_ref())
|
||||
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
|
||||
// Add Content-Security-Policy header for static assets when policy is set
|
||||
if !CSP_POLICY.is_empty() {
|
||||
if let Ok(header_value) = HeaderValue::try_from(CSP_POLICY.as_str()) {
|
||||
res = res.header("Content-Security-Policy", header_value);
|
||||
}
|
||||
}
|
||||
if mime.as_ref() == mime::APPLICATION_JAVASCRIPT
|
||||
|| mime.as_ref() == mime::TEXT_JAVASCRIPT
|
||||
|| path.ends_with(".wasm")
|
||||
|
||||
@@ -1853,6 +1853,18 @@ async fn create_token(
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_tokens =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM token WHERE email = $1", &authed.email)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_tokens.unwrap_or(0) >= 10000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of tokens (10000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
sqlx::query!(
|
||||
"INSERT INTO token
|
||||
(token, email, label, expiration, super_admin, scopes, workspace_id)
|
||||
|
||||
@@ -29,6 +29,7 @@ use windmill_common::{
|
||||
variables::{
|
||||
build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable,
|
||||
},
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
@@ -74,8 +75,7 @@ async fn list_contextual_variables(
|
||||
Some("u/user/triggering_flow_path".to_string()),
|
||||
Some("c".to_string()),
|
||||
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
|
||||
Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c".to_string()),
|
||||
Some(chrono::offset::Utc::now())
|
||||
Some(chrono::offset::Utc::now()),
|
||||
)
|
||||
.await
|
||||
.to_vec(),
|
||||
@@ -314,6 +314,20 @@ async fn create_variable(
|
||||
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
|
||||
Json(variable): Json<CreateVariable>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_variables = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM variable WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_variables.unwrap_or(0) >= 10000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of variables (10000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let authed = maybe_refresh_folders(&variable.path, &w_id, authed, &db).await;
|
||||
|
||||
check_path_conflict(&db, &w_id, &variable.path).await?;
|
||||
|
||||
@@ -36,7 +36,7 @@ use windmill_common::db::UserDB;
|
||||
use windmill_common::s3_helpers::LargeFileStorage;
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_common::variables::{build_crypt, decrypt, encrypt};
|
||||
use windmill_common::worker::to_raw_value;
|
||||
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::workspaces::WorkspaceDeploymentUISettings;
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -1454,6 +1454,21 @@ async fn create_workspace(
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
_check_nb_of_workspaces(&db).await?;
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_workspaces = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM workspace WHERE owner = $1",
|
||||
authed.email
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if nb_workspaces.unwrap_or(0) >= 10 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of workspaces (10) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
check_w_id_conflict(&mut tx, &nw.id).await?;
|
||||
|
||||
@@ -180,7 +180,6 @@ pub async fn get_reserved_variables(
|
||||
schedule_path: Option<String>,
|
||||
step_id: Option<String>,
|
||||
root_flow_id: Option<String>,
|
||||
jwt_token: Option<String>,
|
||||
scheduled_for: Option<chrono::DateTime<Utc>>,
|
||||
) -> Vec<ContextualVariable> {
|
||||
let state_path = {
|
||||
@@ -334,12 +333,6 @@ pub async fn get_reserved_variables(
|
||||
description: "Script or flow step execution unique path, useful for storing results in an external service".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_OIDC_JWT".to_string(),
|
||||
value: jwt_token.unwrap_or_else(|| "".to_string()),
|
||||
description: "OIDC JWT token (EE only)".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_WORKER_GROUP".to_string(),
|
||||
value: WORKER_GROUP.clone(),
|
||||
|
||||
@@ -2721,10 +2721,9 @@ pub async fn get_result_by_id(
|
||||
"SELECT
|
||||
id As \"id!\",
|
||||
flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json<Uuid>\"
|
||||
FROM v2_as_queue
|
||||
WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id AND workspace_id = $2",
|
||||
flow_id,
|
||||
&w_id
|
||||
FROM v2_job_status
|
||||
WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id",
|
||||
flow_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
@@ -2863,9 +2862,10 @@ pub async fn get_result_by_id_from_running_flow_inner(
|
||||
node_id: &str,
|
||||
) -> error::Result<JobResult> {
|
||||
let flow_job_result = sqlx::query!(
|
||||
"SELECT leaf_jobs->$1::text AS \"leaf_jobs: Json<Box<RawValue>>\", parent_job
|
||||
FROM v2_as_queue
|
||||
WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = id AND workspace_id = $3",
|
||||
"SELECT flow_leaf_jobs->$1::text AS \"leaf_jobs: Json<Box<RawValue>>\", v2_job.parent_job
|
||||
FROM v2_job_status
|
||||
LEFT JOIN v2_job ON v2_job.id = v2_job_status.id AND v2_job.workspace_id = $3
|
||||
WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = v2_job_status.id",
|
||||
node_id,
|
||||
flow_id,
|
||||
w_id,
|
||||
@@ -2873,11 +2873,13 @@ pub async fn get_result_by_id_from_running_flow_inner(
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
// tracing::error!("flow_job_result: {:?} {:?}", flow_job_result, flow_id);
|
||||
let flow_job_result = windmill_common::utils::not_found_if_none(
|
||||
flow_job_result,
|
||||
"Root job of parent runnnig flow",
|
||||
format!("parent: {}, id: {}", flow_id, node_id),
|
||||
)?;
|
||||
// tracing::error!("flow_job_result: {:?}, {:?}", flow_job_result.leaf_jobs, flow_job_result.parent_job);
|
||||
|
||||
let job_result = flow_job_result
|
||||
.leaf_jobs
|
||||
@@ -4028,6 +4030,7 @@ pub async fn push<'c, 'd>(
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
let final_priority: Option<i16>;
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
@@ -4229,10 +4232,10 @@ pub async fn push<'c, 'd>(
|
||||
INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,
|
||||
created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,
|
||||
script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,
|
||||
flow_innermost_root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,
|
||||
flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,
|
||||
cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,
|
||||
$19, $20, $21, $22, $23, $24, $25, $26,
|
||||
$19, $20, $38, $21, $22, $23, $24, $25, $26,
|
||||
CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,
|
||||
($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)
|
||||
),
|
||||
@@ -4288,6 +4291,7 @@ pub async fn push<'c, 'd>(
|
||||
job_authed.is_operator,
|
||||
folders.as_slice(),
|
||||
job_authed.groups.as_slice(),
|
||||
root_job.or(parent_job)
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(1)
|
||||
|
||||
@@ -510,6 +510,23 @@ fn raw_to_string(x: &str) -> String {
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
const POWERSHELL_INSTALL_CODE: &str = r#"
|
||||
$availableModules = Get-Module -ListAvailable
|
||||
$path = '{path}'
|
||||
|
||||
$moduleNames = @({modules})
|
||||
|
||||
foreach ($module in $moduleNames) {
|
||||
if (-not ($availableModules | Where-Object { $_.Name -eq $module })) {
|
||||
Write-Host "Installing module $module..."
|
||||
Save-Module -Name $module -Path $path -Force
|
||||
} else {
|
||||
Write-Host "Module $module already installed"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn handle_powershell_job(
|
||||
mem_peak: &mut i32,
|
||||
@@ -573,27 +590,34 @@ pub async fn handle_powershell_job(
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut install_string: String = String::new();
|
||||
let mut modules_to_install: Vec<String> = Vec::new();
|
||||
let mut logs1 = String::new();
|
||||
for line in content.lines() {
|
||||
for cap in RE_POWERSHELL_IMPORTS.captures_iter(line) {
|
||||
let module = cap.get(1).unwrap().as_str();
|
||||
if !installed_modules.contains(&module.to_lowercase()) {
|
||||
logs1.push_str(&format!("\n{} not found in cache", module.to_string()));
|
||||
// instead of using Install-Module, we use Save-Module so that we can specify the installation path
|
||||
install_string.push_str(&format!(
|
||||
"Save-Module -Path {} -Force {};",
|
||||
POWERSHELL_CACHE_DIR, module
|
||||
));
|
||||
modules_to_install.push(module.to_string());
|
||||
} else {
|
||||
logs1.push_str(&format!("\n{} found in cache", module.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !install_string.is_empty() {
|
||||
logs1.push_str("\n\nInstalling modules...");
|
||||
if !logs1.is_empty() {
|
||||
append_logs(&job.id, &job.workspace_id, logs1, db).await;
|
||||
}
|
||||
|
||||
if !modules_to_install.is_empty() {
|
||||
let install_string = POWERSHELL_INSTALL_CODE
|
||||
.replace("{path}", POWERSHELL_CACHE_DIR)
|
||||
.replace(
|
||||
"{modules}",
|
||||
&modules_to_install
|
||||
.iter()
|
||||
.map(|x| format!("'{x}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
let child = Command::new(POWERSHELL_PATH.as_str())
|
||||
.args(&["-Command", &install_string])
|
||||
.stdout(Stdio::piped())
|
||||
|
||||
@@ -1566,7 +1566,6 @@ pub async fn start_worker(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let context_envs = build_envs_map(context.to_vec()).await;
|
||||
|
||||
@@ -434,7 +434,6 @@ pub async fn get_reserved_variables(
|
||||
job.schedule_path(),
|
||||
job.flow_step_id.clone(),
|
||||
job.flow_innermost_root_job.clone().map(|x| x.to_string()),
|
||||
None,
|
||||
Some(job.scheduled_for.clone()),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -11,8 +11,7 @@ use crate::{
|
||||
start_child_process, OccupancyMetrics,
|
||||
},
|
||||
handle_child::handle_child,
|
||||
DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, HOME_ENV, NPM_CONFIG_REGISTRY,
|
||||
PATH_ENV, TZ_ENV,
|
||||
DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, HOME_ENV, NPM_CONFIG_REGISTRY, PATH_ENV, TZ_ENV,
|
||||
};
|
||||
use windmill_common::client::AuthedClient;
|
||||
|
||||
@@ -534,7 +533,6 @@ pub async fn start_worker(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let context_envs = build_envs_map(context.to_vec()).await;
|
||||
|
||||
@@ -62,12 +62,52 @@ lazy_static::lazy_static! {
|
||||
static ref EPHEMERAL_TOKEN_CMD: Option<String> = var("EPHEMERAL_TOKEN_CMD").ok();
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref PIPTAR_UPLOAD_CHANNEL: tokio::sync::mpsc::UnboundedSender<PiptarUploadTask> = {
|
||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
// Spawn background task to handle uploads sequentially
|
||||
tokio::spawn(handle_piptar_uploads(rx));
|
||||
|
||||
tx
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
#[derive(Debug)]
|
||||
struct PiptarUploadTask {
|
||||
venv_path: String,
|
||||
cache_dir: String,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
async fn handle_piptar_uploads(mut rx: tokio::sync::mpsc::UnboundedReceiver<PiptarUploadTask>) {
|
||||
use crate::global_cache::build_tar_and_push;
|
||||
use windmill_common::s3_helpers::get_object_store;
|
||||
|
||||
while let Some(task) = rx.recv().await {
|
||||
if let Some(os) = get_object_store().await {
|
||||
match build_tar_and_push(os, task.venv_path.clone(), task.cache_dir, None, false).await {
|
||||
Ok(()) => {
|
||||
tracing::info!("Successfully uploaded piptar for {}", task.venv_path);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to upload piptar for {}: {}", task.venv_path, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("S3 object store not available for piptar upload: {}", task.venv_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
|
||||
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto");
|
||||
const RELATIVE_PYTHON_LOADER: &str = include_str!("../loader.py");
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
use crate::global_cache::{build_tar_and_push, pull_from_tar};
|
||||
use crate::global_cache::pull_from_tar;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
use windmill_common::s3_helpers::OBJECT_STORE_SETTINGS;
|
||||
@@ -284,6 +324,7 @@ pub async fn uv_pip_compile(
|
||||
child_cmd
|
||||
.env("SystemRoot", SYSTEM_ROOT.as_str())
|
||||
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
|
||||
.env("HOME", crate::USERPROFILE_ENV.as_str())
|
||||
.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
@@ -292,6 +333,29 @@ pub async fn uv_pip_compile(
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
)
|
||||
.env(
|
||||
"APPDATA",
|
||||
std::env::var("APPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Roaming", crate::USERPROFILE_ENV.as_str())),
|
||||
)
|
||||
.env(
|
||||
"ComSpec",
|
||||
std::env::var("ComSpec").unwrap_or_else(|_| String::from("C:\\Windows\\System32\\cmd.exe")),
|
||||
)
|
||||
.env(
|
||||
"PATHEXT",
|
||||
std::env::var("PATHEXT").unwrap_or_else(|_|
|
||||
String::from(".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL")
|
||||
),
|
||||
)
|
||||
.env(
|
||||
"ProgramData",
|
||||
std::env::var("ProgramData").unwrap_or_else(|_| String::from("C:\\ProgramData")),
|
||||
)
|
||||
.env(
|
||||
"ProgramFiles",
|
||||
std::env::var("ProgramFiles").unwrap_or_else(|_| String::from("C:\\Program Files")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -562,7 +626,7 @@ pub async fn handle_python_job(
|
||||
if v == '<function call>':
|
||||
del pre_args[k]
|
||||
kwargs = inner_script.preprocessor(**pre_args)
|
||||
kwrags_json = res_to_json(kwargs)
|
||||
kwrags_json = res_to_json(kwargs)
|
||||
with open("args.json", 'w') as f:
|
||||
f.write(kwrags_json)"#
|
||||
)
|
||||
@@ -638,7 +702,7 @@ except BaseException as e:
|
||||
tb = traceback.format_tb(exc_traceback)
|
||||
with open(result_json, 'w') as f:
|
||||
err = {{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}
|
||||
extra = e.__dict__
|
||||
extra = e.__dict__
|
||||
if extra and len(extra) > 0:
|
||||
err['extra'] = extra
|
||||
flow_node_id = os.environ.get('WM_FLOW_STEP_ID')
|
||||
@@ -674,7 +738,14 @@ except BaseException as e:
|
||||
// ^^^^^^ ^
|
||||
// We also want this be priorotized, that's why we insert it to the beginning
|
||||
}
|
||||
paths.iter().join(":")
|
||||
#[cfg(windows)]
|
||||
{
|
||||
paths.iter().join(";")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
paths.iter().join(":")
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -1370,6 +1441,7 @@ async fn spawn_uv_install(
|
||||
.envs(PROXY_ENVS.clone())
|
||||
.env("SystemRoot", SYSTEM_ROOT.as_str())
|
||||
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
|
||||
.env("HOME", HOME_ENV.as_str())
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
@@ -1379,6 +1451,29 @@ async fn spawn_uv_install(
|
||||
std::env::var("LOCALAPPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
|
||||
)
|
||||
.env(
|
||||
"APPDATA",
|
||||
std::env::var("APPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Roaming", crate::USERPROFILE_ENV.as_str())),
|
||||
)
|
||||
.env(
|
||||
"ComSpec",
|
||||
std::env::var("ComSpec").unwrap_or_else(|_| String::from("C:\\Windows\\System32\\cmd.exe")),
|
||||
)
|
||||
.env(
|
||||
"PATHEXT",
|
||||
std::env::var("PATHEXT").unwrap_or_else(|_|
|
||||
String::from(".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL")
|
||||
),
|
||||
)
|
||||
.env(
|
||||
"ProgramData",
|
||||
std::env::var("ProgramData").unwrap_or_else(|_| String::from("C:\\ProgramData")),
|
||||
)
|
||||
.env(
|
||||
"ProgramFiles",
|
||||
std::env::var("ProgramFiles").unwrap_or_else(|_| String::from("C:\\Program Files")),
|
||||
)
|
||||
.args(&command_args[1..])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
@@ -1763,7 +1858,7 @@ pub async fn handle_python_reqs(
|
||||
|
||||
// Create a file to indicate that installation was successfull
|
||||
let valid_path = venv_p.clone() + "/.valid.windmill";
|
||||
// This is atomic operation, meaning, that it either completes and wheel is valid,
|
||||
// This is atomic operation, meaning, that it either completes and wheel is valid,
|
||||
// or it does not and wheel is invalid and will be reinstalled next run
|
||||
if let Err(e) = File::create(&valid_path).await{
|
||||
tracing::error!(
|
||||
@@ -1890,8 +1985,16 @@ pub async fn handle_python_reqs(
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
if s3_push {
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
tokio::spawn(build_tar_and_push(os, venv_p.clone(), py_version.to_cache_dir_top_level(false), None, false));
|
||||
// Send to upload channel for sequential processing
|
||||
let upload_task = PiptarUploadTask {
|
||||
venv_path: venv_p.clone(),
|
||||
cache_dir: py_version.to_cache_dir_top_level(false),
|
||||
};
|
||||
|
||||
if let Err(e) = PIPTAR_UPLOAD_CHANNEL.send(upload_task) {
|
||||
tracing::warn!("Failed to queue piptar upload for {venv_p}: {e}");
|
||||
} else {
|
||||
tracing::info!("Queued piptar upload for {venv_p}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1906,7 +2009,7 @@ pub async fn handle_python_reqs(
|
||||
pids.lock().await.get_mut(i).and_then(|e| e.take());
|
||||
// Create a file to indicate that installation was successfull
|
||||
let valid_path = venv_p.clone() + "/.valid.windmill";
|
||||
// This is atomic operation, meaning, that it either completes and wheel is valid,
|
||||
// This is atomic operation, meaning, that it either completes and wheel is valid,
|
||||
// or it does not and wheel is invalid and will be reinstalled next run
|
||||
if let Err(e) = File::create(&valid_path).await{
|
||||
tracing::error!(
|
||||
@@ -2022,7 +2125,6 @@ pub async fn start_worker(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.to_vec();
|
||||
@@ -2142,7 +2244,6 @@ for line in sys.stdin:
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -2193,3 +2294,4 @@ for line in sys.stdin:
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -858,13 +858,14 @@ pub fn start_interactive_worker_shell(
|
||||
.await;
|
||||
}
|
||||
_ => {
|
||||
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE)).await;
|
||||
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 10)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err);
|
||||
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 20)).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1861,6 +1862,7 @@ pub async fn run_worker(
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err);
|
||||
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 5)).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ fn try_normalize(path: &Path) -> Option<PathBuf> {
|
||||
|
||||
fn parse_ts_relative_imports(raw_code: &str, script_path: &str) -> error::Result<Vec<String>> {
|
||||
let mut relative_imports = vec![];
|
||||
let r = parse_expr_for_imports(raw_code)?;
|
||||
let r = parse_expr_for_imports(raw_code, true)?;
|
||||
for import in r {
|
||||
let import = import.trim_end_matches(".ts");
|
||||
if import.starts_with("/") {
|
||||
|
||||
+1
-1
@@ -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.498.0";
|
||||
export const VERSION = "v1.501.3";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.498.0";
|
||||
export const VERSION = "1.501.3";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Frontend Development (Svelte 5)
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Follow @svelte5-best-practices.mdc for detailed guidelines
|
||||
- Use Runes ($state, $derived, $effect) for reactivity
|
||||
- Keep components small and focused
|
||||
- Always use keys in {#each} blocks
|
||||
|
||||
## UI Guidelines
|
||||
|
||||
- Follow existing design system
|
||||
- Use consistent spacing and colors
|
||||
|
||||
## Backend API
|
||||
|
||||
- If you need to call the backend API, you can find the available routes in ../backend/windmill-api/openapi.yaml
|
||||
- You can also use the associated types and services that are auto generated from the openapi file. They are in src/lib/gen/\*gen.ts files
|
||||
|
||||
### OpenAPI Autogeneration
|
||||
|
||||
Windmill automatically generates TypeScript types and services from the OpenAPI specification.
|
||||
|
||||
#### Service Generation Pattern
|
||||
|
||||
The autogeneration follows this pattern:
|
||||
|
||||
- **Tag** → **Service Name**: The OpenAPI tag becomes the service name with "Service" suffix
|
||||
- **operationId** → **Method Name**: The operationId becomes the method name in the service
|
||||
|
||||
#### Example
|
||||
|
||||
Given this OpenAPI specification:
|
||||
|
||||
```yaml
|
||||
/w/{workspace}/audit/list:
|
||||
get:
|
||||
summary: list audit logs (requires admin privilege)
|
||||
operationId: listAuditLogs
|
||||
tags:
|
||||
- audit
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/WorkspaceId'
|
||||
- $ref: '#/components/parameters/Page'
|
||||
- $ref: '#/components/parameters/PerPage'
|
||||
- $ref: '#/components/parameters/Before'
|
||||
- $ref: '#/components/parameters/After'
|
||||
- $ref: '#/components/parameters/Username'
|
||||
- $ref: '#/components/parameters/Operation'
|
||||
- name: operations
|
||||
in: query
|
||||
description: comma separated list of exact operations to include
|
||||
schema:
|
||||
type: string
|
||||
```
|
||||
|
||||
This generates:
|
||||
|
||||
- **Service**: `AuditService` (from tag "audit")
|
||||
- **Method**: `listAuditLogs` (from operationId)
|
||||
|
||||
#### Method Arguments
|
||||
|
||||
The generated method arguments correspond to the OpenAPI parameters:
|
||||
|
||||
```typescript
|
||||
AuditService.listAuditLogs({
|
||||
workspace: string, // from WorkspaceId parameter
|
||||
page?: number, // from Page parameter
|
||||
perPage?: number, // from PerPage parameter
|
||||
before?: string, // from Before parameter
|
||||
after?: string, // from After parameter
|
||||
username?: string, // from Username parameter
|
||||
operation?: string, // from Operation parameter
|
||||
operations?: string // from operations parameter
|
||||
})
|
||||
```
|
||||
Generated
+6
-16
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.498.0",
|
||||
"version": "1.501.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.498.0",
|
||||
"version": "1.501.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -77,7 +77,7 @@
|
||||
"windmill-parser-wasm-java": "^1.478.1",
|
||||
"windmill-parser-wasm-nu": "^1.474.1",
|
||||
"windmill-parser-wasm-php": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.477.1",
|
||||
"windmill-parser-wasm-py": "^1.499.0",
|
||||
"windmill-parser-wasm-regex": "^1.492.1",
|
||||
"windmill-parser-wasm-rust": "^1.429.0",
|
||||
"windmill-parser-wasm-ts": "^1.486.1",
|
||||
@@ -132,7 +132,6 @@
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"svelte-highlight": "^7.6.0",
|
||||
"svelte-multiselect": "^11.0.0-rc.1",
|
||||
"svelte-popperjs": "^1.3.2",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"svelte-range-slider-pips": "^2.3.1",
|
||||
@@ -11862,15 +11861,6 @@
|
||||
"resolved": "https://registry.npmjs.org/svelte-infinite-loading/-/svelte-infinite-loading-1.4.0.tgz",
|
||||
"integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg=="
|
||||
},
|
||||
"node_modules/svelte-multiselect": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-11.1.0.tgz",
|
||||
"integrity": "sha512-D93t6GlOV//gU7upR59uCd755Hq6OWhqv9ddhOXBDN/mEZj5XOAYHwDjUjePUKlE+bI+jry1BlZef8Mof2Y+YA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-popperjs": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-popperjs/-/svelte-popperjs-1.3.2.tgz",
|
||||
@@ -12962,9 +12952,9 @@
|
||||
"integrity": "sha512-SGJAtNpfdRZftkGboxWsm/yQDnJBJodwPQUbX2cWk/aoNook6ULesZwsYtBC9WN1VH6TIskLiVPohMmu6jtXmw=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-py": {
|
||||
"version": "1.477.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.477.1.tgz",
|
||||
"integrity": "sha512-EY3mSMWpqFPzd7fsLg2/hSfQFU8HpW9nplFwm4JHHCDbcTpBzlvzjPJoHAAGO5kMzowAxjqi5ai/mXjeUWuiSg=="
|
||||
"version": "1.499.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.499.0.tgz",
|
||||
"integrity": "sha512-ur5SU+YTuYm1Hgo+SWohYRsmkVxIZPME7GzzRivPhkK5XBQcc7PsxlMjYkIvFJwufz+jX2t6p0aYNIdDXpMNbA=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-regex": {
|
||||
"version": "1.492.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.498.0",
|
||||
"version": "1.501.3",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -57,7 +57,6 @@
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"svelte-highlight": "^7.6.0",
|
||||
"svelte-multiselect": "^11.0.0-rc.1",
|
||||
"svelte-popperjs": "^1.3.2",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"svelte-range-slider-pips": "^2.3.1",
|
||||
@@ -145,7 +144,7 @@
|
||||
"windmill-parser-wasm-java": "^1.478.1",
|
||||
"windmill-parser-wasm-nu": "^1.474.1",
|
||||
"windmill-parser-wasm-php": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.477.1",
|
||||
"windmill-parser-wasm-py": "^1.499.0",
|
||||
"windmill-parser-wasm-regex": "^1.492.1",
|
||||
"windmill-parser-wasm-rust": "^1.429.0",
|
||||
"windmill-parser-wasm-ts": "^1.486.1",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
|
||||
|
||||
export interface TriggerableByAIOptions {
|
||||
id?: string
|
||||
description?: string
|
||||
callback?: (value?: string) => void
|
||||
disabled?: boolean
|
||||
showAnimation?: boolean
|
||||
}
|
||||
|
||||
export function triggerableByAI(element: HTMLElement, options: TriggerableByAIOptions = {}) {
|
||||
let { id, description, callback, showAnimation = true } = options
|
||||
|
||||
// Component is not discoverable if id or description is not provided
|
||||
const isDisabled = !id || !description
|
||||
|
||||
function createPulseEffect() {
|
||||
if (!showAnimation) return
|
||||
|
||||
// Get the bounding rect of the element
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (rect.width === 0 && rect.height === 0) return // Skip if no dimensions
|
||||
|
||||
const centerX = rect.left + rect.width / 2
|
||||
const centerY = rect.top + rect.height / 2
|
||||
|
||||
// Create pulse element
|
||||
const pulse = document.createElement('div')
|
||||
pulse.className = 'fixed w-10 h-10 bg-blue-500/90 rounded-full pointer-events-none animate-ping'
|
||||
pulse.style.cssText = `
|
||||
left: ${centerX - 20}px;
|
||||
top: ${centerY - 20}px;
|
||||
z-index: 9999;
|
||||
`
|
||||
|
||||
// Add to body
|
||||
document.body.appendChild(pulse)
|
||||
|
||||
// Remove after animation
|
||||
setTimeout(() => {
|
||||
if (pulse && pulse.parentNode) {
|
||||
pulse.parentNode.removeChild(pulse)
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function handleTrigger(value?: string) {
|
||||
if (!callback) return
|
||||
if (!showAnimation) {
|
||||
callback(value)
|
||||
return
|
||||
}
|
||||
|
||||
createPulseEffect()
|
||||
callback(value)
|
||||
}
|
||||
|
||||
function register() {
|
||||
if (isDisabled || !id || !description) return
|
||||
|
||||
// register the triggerable
|
||||
const currentData = { description, onTrigger: handleTrigger }
|
||||
aiChatManager.triggerablesByAI[id] = currentData
|
||||
}
|
||||
|
||||
function unregister() {
|
||||
if (isDisabled || !id) return
|
||||
|
||||
// unregister the triggerable
|
||||
if (aiChatManager.triggerablesByAI[id]) {
|
||||
delete aiChatManager.triggerablesByAI[id]
|
||||
}
|
||||
}
|
||||
|
||||
// Initial registration
|
||||
register()
|
||||
|
||||
return {
|
||||
update(newOptions: TriggerableByAIOptions) {
|
||||
;({ id, description, callback, showAnimation = true } = newOptions)
|
||||
register()
|
||||
},
|
||||
destroy() {
|
||||
unregister()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { EnumType } from '$lib/common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
|
||||
interface Props {
|
||||
disabled: boolean
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
setInputCat as computeInputCat,
|
||||
debounce,
|
||||
emptyString,
|
||||
getSchemaFromProperties
|
||||
getSchemaFromProperties,
|
||||
clone
|
||||
} from '$lib/utils'
|
||||
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
|
||||
import { createEventDispatcher, onMount, tick, untrack } from 'svelte'
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Button, SecondsInput } from './common'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
@@ -41,6 +41,8 @@
|
||||
import type { Script } from '$lib/gen'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
|
||||
import type { ComponentCustomCSS } from './apps/types'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
@@ -243,7 +245,7 @@
|
||||
nvalue = undefined
|
||||
}
|
||||
if ((value == undefined || value == null) && !ignoreValueUndefined) {
|
||||
nvalue = structuredClone($state.snapshot(defaultValue))
|
||||
nvalue = clone(defaultValue)
|
||||
if (defaultValue === undefined || defaultValue === null) {
|
||||
if (inputCat === 'string') {
|
||||
nvalue = nullable ? null : ''
|
||||
@@ -295,7 +297,7 @@
|
||||
lastValue = value
|
||||
}
|
||||
|
||||
let oldDefaultValue = structuredClone($state.snapshot(defaultValue))
|
||||
let oldDefaultValue = clone(defaultValue)
|
||||
function handleDefaultValueChange() {
|
||||
if (
|
||||
deepEqual(value, oldDefaultValue) &&
|
||||
@@ -304,7 +306,7 @@
|
||||
) {
|
||||
value = defaultValue
|
||||
}
|
||||
oldDefaultValue = structuredClone($state.snapshot(defaultValue))
|
||||
oldDefaultValue = clone(defaultValue)
|
||||
}
|
||||
|
||||
function isObjectCat(inputCat?: string) {
|
||||
@@ -448,7 +450,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let debounced = debounce(() => compareValues(value), 50)
|
||||
let { debounced, clearDebounce } = debounce(() => compareValues(value), 50)
|
||||
let inputCat = $derived(computeInputCat(type, format, itemsType?.type, enum_, contentEncoding))
|
||||
$effect(() => {
|
||||
oneOf && untrack(() => updateOneOfSelected(oneOf))
|
||||
@@ -492,6 +494,10 @@
|
||||
$effect(() => {
|
||||
shouldDispatchChanges && debounced(value)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
clearDebounce()
|
||||
})
|
||||
</script>
|
||||
|
||||
<S3FilePicker
|
||||
@@ -631,43 +637,22 @@
|
||||
<div class="w-full">
|
||||
{#if Array.isArray(itemsType?.multiselect) && Array.isArray(value)}
|
||||
<div class="items-start">
|
||||
<Multiselect
|
||||
ulOptionsClass={'p-2 !bg-surface-secondary'}
|
||||
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
|
||||
<MultiSelect
|
||||
{disabled}
|
||||
bind:selected={value}
|
||||
onremove={(e) => {
|
||||
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
|
||||
}}
|
||||
options={itemsType?.multiselect ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
onopen={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
bind:value
|
||||
items={safeSelectItems(itemsType?.multiselect)}
|
||||
onOpen={() => dispatch('focus')}
|
||||
reorderable
|
||||
/>
|
||||
</div>
|
||||
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && (Array.isArray(value) || value == undefined)}
|
||||
<div class="items-start">
|
||||
<Multiselect
|
||||
ulOptionsClass={'p-2 !bg-surface-secondary'}
|
||||
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
|
||||
<MultiSelect
|
||||
{disabled}
|
||||
onremove={(e) => {
|
||||
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
|
||||
}}
|
||||
bind:selected={
|
||||
() => [...(value ?? [])],
|
||||
(v) => {
|
||||
if (!deepEqual(v, value)) {
|
||||
value = v
|
||||
}
|
||||
}
|
||||
}
|
||||
options={itemsType?.enum ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
onopen={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
bind:value
|
||||
items={safeSelectItems(itemsType?.enum)}
|
||||
onOpen={() => dispatch('focus')}
|
||||
reorderable
|
||||
/>
|
||||
</div>
|
||||
{:else if itemsType?.type == 'object' && itemsType?.resourceType == 's3object'}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import EditableSchemaDrawer from './schema/EditableSchemaDrawer.svelte'
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { tick } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
canEditResourceType?: boolean
|
||||
@@ -48,20 +50,6 @@
|
||||
? 'bytes'
|
||||
: 'string'
|
||||
)
|
||||
|
||||
let schema = $state({
|
||||
properties: itemsType?.properties || {},
|
||||
order: Object.keys(itemsType?.properties || {}),
|
||||
required: Object.values(itemsType?.properties || {}).map((p) => p.required)
|
||||
})
|
||||
|
||||
function updateItemsType() {
|
||||
itemsType = {
|
||||
...itemsType,
|
||||
properties: schema.properties,
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if canEditResourceType || originalType == 'string[]' || originalType == 'object[]'}
|
||||
@@ -180,10 +168,40 @@
|
||||
{/if}
|
||||
|
||||
{#if selected === 'object'}
|
||||
<EditableSchemaDrawer
|
||||
bind:schema
|
||||
on:change={() => {
|
||||
updateItemsType()
|
||||
}}
|
||||
<Toggle
|
||||
bind:checked={
|
||||
() => {
|
||||
return itemsType?.properties != undefined
|
||||
},
|
||||
async (v) => {
|
||||
await tick()
|
||||
if (v) {
|
||||
itemsType = { type: 'object', properties: {} }
|
||||
} else {
|
||||
itemsType = { type: 'object', properties: undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
options={{ left: 'JSON', right: 'Custom Object' }}
|
||||
/>
|
||||
{#if itemsType?.properties != undefined}
|
||||
<EditableSchemaDrawer
|
||||
bind:schema={
|
||||
() => {
|
||||
return {
|
||||
properties: itemsType?.properties || {},
|
||||
order: Object.keys(itemsType?.properties || {}),
|
||||
required: Object.values(itemsType?.properties || {}).map((p) => p.required)
|
||||
}
|
||||
},
|
||||
(schema) => {
|
||||
itemsType = {
|
||||
...itemsType,
|
||||
properties: schema.properties,
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { ExternalLink, Loader2, X } from 'lucide-svelte'
|
||||
import { SettingService, WorkerService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { superadmin } from '$lib/stores'
|
||||
import { superadmin, devopsRole } from '$lib/stores'
|
||||
import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
|
||||
import { CUSTOM_TAGS_SETTING } from '$lib/consts'
|
||||
import { base } from '$lib/base'
|
||||
@@ -80,10 +80,10 @@
|
||||
loadCustomTags()
|
||||
sendUserToast('Tag added')
|
||||
}}
|
||||
disabled={newTag.trim() == '' || !$superadmin}
|
||||
disabled={newTag.trim() == '' || !($superadmin || $devopsRole)}
|
||||
>
|
||||
Add {#if !$superadmin}
|
||||
<span class="text-2xs text-tertiary">superadmin only</span>
|
||||
Add {#if !($superadmin || $devopsRole)}
|
||||
<span class="text-2xs text-tertiary">superadmin or devops only</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<span class="text-sm text-primary"
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Label from './Label.svelte'
|
||||
import MultiSelect from 'svelte-multiselect'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
interface Props {
|
||||
config: AutoscalingConfig | undefined
|
||||
@@ -209,32 +210,14 @@
|
||||
{#if config}
|
||||
{#if config.custom_tags}
|
||||
<MultiSelect
|
||||
outerDivClass="text-secondary !bg-surface-disabled !border-0"
|
||||
selected={config.custom_tags}
|
||||
onchange={(e) => {
|
||||
console.log(e.type, config?.custom_tags)
|
||||
if (e && config?.custom_tags) {
|
||||
if (e.type === 'add') {
|
||||
config.custom_tags = [
|
||||
...config.custom_tags,
|
||||
...(e.option ? [e.option.toString()] : [])
|
||||
]
|
||||
} else if (e.type === 'remove') {
|
||||
config.custom_tags = config.custom_tags.filter((t) => t !== e.option)
|
||||
if (config?.custom_tags && config.custom_tags.length == 0) {
|
||||
config.custom_tags = undefined
|
||||
}
|
||||
} else if (e.type === 'removeAll') {
|
||||
config.custom_tags = undefined
|
||||
} else {
|
||||
console.error(`Priority tags multiselect - unknown event type: '${e.type}'`)
|
||||
}
|
||||
bind:value={
|
||||
() => config?.custom_tags ?? [],
|
||||
(v) => {
|
||||
config && (config.custom_tags = v.length ? v : undefined)
|
||||
dispatch('dirty')
|
||||
}
|
||||
}}
|
||||
options={worker_tags ?? []}
|
||||
selectedOptionsDraggable={false}
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
}
|
||||
items={safeSelectItems(worker_tags)}
|
||||
placeholder="Tags"
|
||||
/>
|
||||
{:else}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
|
||||
interface ChannelItem {
|
||||
channel_id?: string
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import { ScheduleService } from '$lib/gen'
|
||||
import { emptyString, formatCron, sendUserToast } from '$lib/utils'
|
||||
import Badge from './Badge.svelte'
|
||||
// @ts-ignore
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import { Button } from './common'
|
||||
import timezones from './timezones'
|
||||
import CronBuilder from './CronBuilder.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import CronGen from './copilot/CronGen.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
export let schedule: string
|
||||
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
|
||||
@@ -237,7 +237,7 @@
|
||||
<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">
|
||||
<div class="w-full flex flex-col gap-1 mb-2">
|
||||
<select
|
||||
{disabled}
|
||||
name="execute_every"
|
||||
@@ -280,26 +280,24 @@
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
{#if executeEvery == 'month'}
|
||||
<div class="w-full flex flex-col">
|
||||
<Multiselect
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
{disabled}
|
||||
bind:selected={monthsOfYear}
|
||||
options={monthsOfYearOptions}
|
||||
selectedOptionsDraggable={false}
|
||||
bind:value={monthsOfYear}
|
||||
items={safeSelectItems(monthsOfYearOptions)}
|
||||
placeholder="Every month"
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if executeEvery == 'day-week'}
|
||||
<div class="w-full flex flex-col">
|
||||
<Multiselect
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
{disabled}
|
||||
bind:selected={daysOfWeek}
|
||||
options={daysOfWeekOptions}
|
||||
selectedOptionsDraggable={false}
|
||||
bind:value={daysOfWeek}
|
||||
items={safeSelectItems(daysOfWeekOptions)}
|
||||
placeholder="Every day"
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -311,13 +309,12 @@
|
||||
{/if}
|
||||
<div class="w-full flex gap-4">
|
||||
<div class="w-full flex">
|
||||
<Multiselect
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
{disabled}
|
||||
bind:selected={daysOfMonth}
|
||||
options={daysOfMonthOptions}
|
||||
selectedOptionsDraggable={false}
|
||||
bind:value={daysOfMonth}
|
||||
items={safeSelectItems(daysOfMonthOptions)}
|
||||
placeholder="Every day"
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
import { getFlatTableNamesFromSchema, type DBSchema } from '$lib/stores'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
const { onConfirm, resourceType, previewSql, dbSchema, currentSchema }: DBTableEditorProps =
|
||||
$props()
|
||||
@@ -165,7 +166,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
items={columnTypes.map((type) => ({ value: type, label: type }))}
|
||||
items={safeSelectItems(columnTypes)}
|
||||
class="w-48"
|
||||
/>
|
||||
</Cell>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import DefaultTagsInner from './DefaultTagsInner.svelte'
|
||||
|
||||
export let defaultTagPerWorkspace: boolean | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] = []
|
||||
|
||||
let placement: 'bottom-end' | 'top-end' = 'bottom-end'
|
||||
</script>
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
import { enterpriseLicense, superadmin } from '$lib/stores'
|
||||
import { DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING } from '$lib/consts'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import MultiSelectWrapper from './multiselect/MultiSelectWrapper.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
let defaultTags: string[] | undefined = undefined
|
||||
export let defaultTagPerWorkspace: boolean | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] = []
|
||||
let limitToWorkspaces = false
|
||||
|
||||
let workspaces: string[] = []
|
||||
@@ -62,7 +63,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div id="default-tags-settings" class="py-4 flex flex-col gap-2">
|
||||
<div class="py-4 flex flex-col gap-2">
|
||||
<Toggle
|
||||
bind:checked={defaultTagPerWorkspace}
|
||||
options={{ right: 'workspace specific default tags' }}
|
||||
@@ -70,9 +71,9 @@
|
||||
{#if defaultTagPerWorkspace}
|
||||
<Toggle bind:checked={limitToWorkspaces} options={{ right: 'only for some workspaces' }} />
|
||||
{#if limitToWorkspaces}
|
||||
<MultiSelectWrapper
|
||||
target="#default-tags-settings"
|
||||
items={workspaces}
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
items={safeSelectItems(workspaces)}
|
||||
bind:value={defaultTagWorkspaces}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
loadingSave = false,
|
||||
newFlow = false,
|
||||
dropdownItems = []
|
||||
} = $props<{
|
||||
}: {
|
||||
loading?: boolean
|
||||
loadingSave?: boolean
|
||||
newFlow?: boolean
|
||||
@@ -16,7 +16,7 @@
|
||||
label: string
|
||||
onClick: () => void
|
||||
}>
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
import { TestSteps } from './flows/testSteps.svelte'
|
||||
|
||||
let flowCopilotContext: FlowCopilotContext = {
|
||||
shouldUpdatePropertyType: writable<{
|
||||
@@ -436,7 +437,7 @@
|
||||
const moving = writable<{ id: string } | undefined>(undefined)
|
||||
const history = initHistory(flowStore.val)
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const testSteps = new TestSteps()
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
@@ -455,7 +456,7 @@
|
||||
pathStore: writable(''),
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
testStepStore,
|
||||
testSteps,
|
||||
saveDraft: () => {},
|
||||
initialPathStore: writable(''),
|
||||
fakeInitialPath: '',
|
||||
@@ -715,7 +716,7 @@
|
||||
noEditor
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
$testStepStore['preprocessor'] = ev.detail.args ?? {}
|
||||
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
} else {
|
||||
previewArgsStore.val = ev.detail.args ?? {}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
orderedJsonStringify,
|
||||
orderedYamlStringify,
|
||||
replaceFalseWithUndefined,
|
||||
type Value
|
||||
type Value,
|
||||
clone
|
||||
} from '$lib/utils'
|
||||
import type { Script } from '$lib/gen'
|
||||
|
||||
@@ -19,12 +20,25 @@
|
||||
metadata: string
|
||||
}
|
||||
|
||||
let contentType: 'content' | 'metadata' | undefined = undefined
|
||||
let diffType: 'draft' | 'deployed' | 'custom' | undefined = undefined
|
||||
let diffViewer: Drawer
|
||||
let diffType: 'draft' | 'deployed' | 'custom' | undefined = $state(undefined)
|
||||
|
||||
export let restoreDeployed: () => Promise<void> = async () => {}
|
||||
export let restoreDraft: () => Promise<void> = async () => {}
|
||||
let contentType = $derived.by(() => {
|
||||
if (!data || !diffType) return undefined
|
||||
const dataType = diffType === 'custom' ? 'original' : diffType
|
||||
return data[dataType]?.content !== data.current.content
|
||||
? 'content'
|
||||
: data[dataType]?.metadata !== data.current.metadata
|
||||
? 'metadata'
|
||||
: undefined
|
||||
})
|
||||
let diffViewer: Drawer | undefined = $state(undefined)
|
||||
|
||||
interface Props {
|
||||
restoreDeployed?: () => Promise<void>
|
||||
restoreDraft?: () => Promise<void>
|
||||
}
|
||||
|
||||
let { restoreDeployed = async () => {}, restoreDraft = async () => {} }: Props = $props()
|
||||
|
||||
let data:
|
||||
| {
|
||||
@@ -42,21 +56,20 @@
|
||||
current: DiffData
|
||||
button?: { text: string; onClick: () => void }
|
||||
}
|
||||
| undefined = undefined
|
||||
| undefined = $state(undefined)
|
||||
|
||||
export function openDrawer() {
|
||||
data = undefined
|
||||
contentType = undefined
|
||||
diffType = undefined
|
||||
diffViewer.openDrawer()
|
||||
diffViewer?.openDrawer()
|
||||
}
|
||||
|
||||
export function closeDrawer() {
|
||||
diffViewer.closeDrawer()
|
||||
diffViewer?.closeDrawer()
|
||||
}
|
||||
|
||||
function prepareDiff(data: Value) {
|
||||
const metadata = structuredClone(cleanValueProperties(replaceFalseWithUndefined(data)))
|
||||
const metadata = clone(cleanValueProperties(replaceFalseWithUndefined(data)))
|
||||
const content = metadata['content']
|
||||
if (metadata['content'] !== undefined) {
|
||||
metadata['content'] = 'check content diff'
|
||||
@@ -116,20 +129,6 @@
|
||||
diffType = 'custom'
|
||||
}
|
||||
}
|
||||
|
||||
function updateContentType(data_: typeof data, diffType_: typeof diffType) {
|
||||
if (!data_) return
|
||||
if (!diffType_) return
|
||||
const dataType = diffType_ === 'custom' ? 'original' : diffType_
|
||||
contentType =
|
||||
data_[dataType]?.content !== data_.current.content
|
||||
? 'content'
|
||||
: data_[dataType]?.metadata !== data_.current.metadata
|
||||
? 'metadata'
|
||||
: undefined
|
||||
}
|
||||
|
||||
$: updateContentType(data, diffType)
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={diffViewer} size="1200px" on:close>
|
||||
@@ -262,7 +261,7 @@
|
||||
on:click={() => {
|
||||
if (data?.button) {
|
||||
data.button.onClick()
|
||||
diffViewer.closeDrawer()
|
||||
diffViewer?.closeDrawer()
|
||||
}
|
||||
}}>{data.button.text}</Button
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
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 './TriggerableByAI.svelte'
|
||||
import { triggerableByAI } from '$lib/actions/triggerableByAI'
|
||||
|
||||
export let aiId: string | undefined = undefined
|
||||
export let aiDescription: string | undefined = undefined
|
||||
@@ -81,9 +81,13 @@
|
||||
|
||||
<ResolveOpen {open} on:open on:close />
|
||||
|
||||
<TriggerableByAI id={aiId} description={aiDescription} onTrigger={() => buttonEl?.click()} />
|
||||
<button
|
||||
bind:this={buttonEl}
|
||||
use:triggerableByAI={{
|
||||
id: aiId,
|
||||
description: aiDescription,
|
||||
callback: () => buttonEl?.click()
|
||||
}}
|
||||
class={twMerge('w-full flex items-center justify-end', fixedHeight && 'h-8', $$props.class)}
|
||||
use:melt={$trigger}
|
||||
{disabled}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { MenubarMenuElements } from '@melt-ui/svelte'
|
||||
import type { Item } from '$lib/utils'
|
||||
import TriggerableByAI from './TriggerableByAI.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
interface Props {
|
||||
aiId?: string
|
||||
@@ -30,42 +28,31 @@
|
||||
{#if computedItems}
|
||||
<div class="flex flex-col">
|
||||
{#each computedItems ?? [] as item}
|
||||
<TriggerableByAI
|
||||
id={`${aiId ? `${aiId}-${item.displayName}` : undefined}`}
|
||||
description={item.displayName}
|
||||
onTrigger={() => {
|
||||
if (item.action) {
|
||||
item.action({} as MouseEvent)
|
||||
}
|
||||
if (item.href) {
|
||||
goto(item.href)
|
||||
}
|
||||
}}
|
||||
<MenuItem
|
||||
on:click={(e) => item?.action?.(e)}
|
||||
href={item?.href}
|
||||
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',
|
||||
'data-[highlighted]:bg-surface-hover',
|
||||
'flex flex-row gap-2 items-center',
|
||||
item?.disabled && 'text-gray-400 cursor-not-allowed',
|
||||
item?.type === 'delete' &&
|
||||
!item?.disabled &&
|
||||
'text-red-500 hover:bg-red-100 hover:text-red-500 data-[highlighted]:text-red-500 data-[highlighted]:bg-red-100'
|
||||
)}
|
||||
item={meltItem}
|
||||
aiId={`${aiId ? `${aiId}-${item.displayName}` : undefined}`}
|
||||
aiDescription={item.displayName}
|
||||
>
|
||||
<MenuItem
|
||||
on:click={(e) => item?.action?.(e)}
|
||||
href={item?.href}
|
||||
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',
|
||||
'data-[highlighted]:bg-surface-hover',
|
||||
'flex flex-row gap-2 items-center',
|
||||
item?.disabled && 'text-gray-400 cursor-not-allowed',
|
||||
item?.type === 'delete' &&
|
||||
!item?.disabled &&
|
||||
'text-red-500 hover:bg-red-100 hover:text-red-500 data-[highlighted]:text-red-500 data-[highlighted]:bg-red-100'
|
||||
)}
|
||||
item={meltItem}
|
||||
>
|
||||
{#if item.icon}
|
||||
<item.icon size={14} color={item.iconColor} />
|
||||
{/if}
|
||||
<p title={item.displayName} class="truncate grow min-w-0 whitespace-nowrap text-left">
|
||||
{item.displayName}
|
||||
</p>
|
||||
{@render item.extra?.()}
|
||||
</MenuItem>
|
||||
</TriggerableByAI>
|
||||
{#if item.icon}
|
||||
<item.icon size={14} color={item.iconColor} />
|
||||
{/if}
|
||||
<p title={item.displayName} class="truncate grow min-w-0 whitespace-nowrap text-left">
|
||||
{item.displayName}
|
||||
</p>
|
||||
{@render item.extra?.()}
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -1,26 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
|
||||
import type { Script } from '$lib/gen'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import SelectLegacy from './apps/svelte-select/lib/SelectLegacy.svelte'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import ResultJobLoader from './ResultJobLoader.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
|
||||
|
||||
export let value: any = undefined
|
||||
export let helperScript:
|
||||
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
|
||||
| { type: 'hash'; hash: string }
|
||||
| undefined = undefined
|
||||
export let entrypoint: string
|
||||
export let args: Record<string, any> = {}
|
||||
export let name: string
|
||||
|
||||
let darkMode: boolean = false
|
||||
|
||||
let rawCode = JSON.stringify(value, null, 2)
|
||||
<script lang="ts" module>
|
||||
function validSelectObject(x): string | undefined {
|
||||
if (typeof x != 'object') {
|
||||
return JSON.stringify(x) + ' is not an object'
|
||||
@@ -34,115 +12,105 @@
|
||||
}
|
||||
return
|
||||
}
|
||||
async function getItemsFromOptions(text: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Script } from '$lib/gen'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import ResultJobLoader from './ResultJobLoader.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { readFieldsRecursively, clone} from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
value?: any
|
||||
helperScript?:
|
||||
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
|
||||
| { type: 'hash'; hash: string }
|
||||
entrypoint: string
|
||||
args?: Record<string, any>
|
||||
name: string
|
||||
}
|
||||
|
||||
let { value = $bindable(), helperScript, entrypoint, args: _args, name }: Props = $props()
|
||||
|
||||
let args = $state(clone(_args))
|
||||
$effect(() => {
|
||||
readFieldsRecursively(_args, { excludeField: [name] })
|
||||
untrack(() => !deepEqual(args, _args) && (args = $state.snapshot(_args)))
|
||||
})
|
||||
|
||||
async function getItemsFromOptions() {
|
||||
return new Promise<{ label: string; value: any }[]>((resolve, reject) => {
|
||||
let cb = {
|
||||
done(res) {
|
||||
if (!res || !Array.isArray(res)) {
|
||||
reject('Result was not an array')
|
||||
return
|
||||
}
|
||||
if (res.length == 0) {
|
||||
resolve([])
|
||||
}
|
||||
|
||||
if (res.length == 0) resolve([])
|
||||
if (res.every((x) => typeof x == 'string')) {
|
||||
res = res.map((x) => ({
|
||||
label: x,
|
||||
value: x
|
||||
}))
|
||||
res = res.map((x) => ({ label: x, value: x }))
|
||||
} else if (res.find((x) => validSelectObject(x) != undefined)) {
|
||||
reject(validSelectObject(res.find((x) => validSelectObject(x) != undefined)))
|
||||
} else {
|
||||
if (text != undefined && text != '') {
|
||||
res = res.filter((x) => x['label'].includes(text))
|
||||
}
|
||||
if (filterText != undefined && filterText != '')
|
||||
res = res.filter((x) => x['label'].includes(filterText))
|
||||
resolve(res)
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
reject()
|
||||
},
|
||||
error(err) {
|
||||
reject(err)
|
||||
}
|
||||
cancel: () => reject(),
|
||||
error: (err) => reject(err)
|
||||
}
|
||||
helperScript?.type == 'inline'
|
||||
? resultJobLoader?.runPreview(
|
||||
helperScript?.path ?? 'NO_PATH',
|
||||
helperScript.code,
|
||||
helperScript.lang,
|
||||
{ ...args, text, _ENTRYPOINT_OVERRIDE: entrypoint },
|
||||
{ ...args, filterText, _ENTRYPOINT_OVERRIDE: entrypoint },
|
||||
undefined,
|
||||
cb
|
||||
)
|
||||
: resultJobLoader?.runScriptByHash(
|
||||
helperScript?.hash ?? 'NO_HASH',
|
||||
{
|
||||
...args,
|
||||
text,
|
||||
_ENTRYPOINT_OVERRIDE: entrypoint
|
||||
},
|
||||
{ ...args, filterText, _ENTRYPOINT_OVERRIDE: entrypoint },
|
||||
cb
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let lastArgs = structuredClone({ ...stateSnapshot(args), [name]: undefined })
|
||||
$: (entrypoint || helperScript) && refreshOptions()
|
||||
let _items = usePromise(getItemsFromOptions)
|
||||
let items = $derived(_items.value)
|
||||
$effect(() => {
|
||||
;[args, name, entrypoint, helperScript, filterText]
|
||||
untrack(() => _items.refresh())
|
||||
})
|
||||
|
||||
$: args && changeArgs()
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
function changeArgs() {
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
let argsWithoutSelf = { ...stateSnapshot(args), [name]: undefined }
|
||||
if (deepEqual(argsWithoutSelf, lastArgs)) {
|
||||
return
|
||||
}
|
||||
refreshOptions()
|
||||
lastArgs = structuredClone(argsWithoutSelf)
|
||||
timeout = undefined
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function refreshOptions() {
|
||||
error = undefined
|
||||
renderCount += 1
|
||||
}
|
||||
|
||||
let error: string | undefined = undefined
|
||||
let resultJobLoader: ResultJobLoader
|
||||
let renderCount = 0
|
||||
let resultJobLoader: ResultJobLoader | undefined = $state()
|
||||
let filterText: string = $state('')
|
||||
let open: boolean = $state(false)
|
||||
</script>
|
||||
|
||||
{#if helperScript}
|
||||
<DarkModeObserver bind:darkMode />
|
||||
<ResultJobLoader bind:this={resultJobLoader} />
|
||||
|
||||
<div class="w-full flex-col flex">
|
||||
<div class="w-full">
|
||||
{#key renderCount}
|
||||
<SelectLegacy
|
||||
on:error={(e) => {
|
||||
error = e.detail.details
|
||||
}}
|
||||
on:change={(e) => {
|
||||
value = e.detail.value
|
||||
}}
|
||||
{value}
|
||||
computeOnClick={value == undefined}
|
||||
loadOptions={getItemsFromOptions}
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={darkMode
|
||||
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
|
||||
: SELECT_INPUT_DEFAULT_STYLE.containerStyles}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="text-red-400 text-2xs">error: <Tooltip>{JSON.stringify(error)}</Tooltip></div>
|
||||
<Select
|
||||
bind:value
|
||||
bind:open
|
||||
{items}
|
||||
bind:filterText
|
||||
loading={!open && _items.status === 'loading'}
|
||||
clearable
|
||||
noItemsMsg={_items.status === 'loading' ? 'Loading...' : 'No items found'}
|
||||
/>
|
||||
{#if _items.error}
|
||||
<div class="text-red-400 text-2xs">
|
||||
error: <Tooltip>{JSON.stringify(_items.error)}</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -153,7 +121,7 @@
|
||||
{#await import('$lib/components/JsonEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default code={rawCode} bind:value />
|
||||
<Module.default code={JSON.stringify(value, null, 2)} bind:value />
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import Label from './Label.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { emptyString, clone} from '$lib/utils'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
@@ -114,7 +114,7 @@
|
||||
Object.keys(schema?.properties ?? {}).forEach((key) => {
|
||||
if (schema?.properties[key].default != undefined && args?.[key] == undefined) {
|
||||
let value = schema?.properties[key].default
|
||||
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
|
||||
nargs[key] = value === 'object' ? clone(value) : value
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { formatResourceTypes } from './copilot/chat/script/core'
|
||||
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
|
||||
import { editorPositionMap } from '$lib/utils'
|
||||
// import EditorTheme from './EditorTheme.svelte'
|
||||
|
||||
let divEl: HTMLDivElement | null = null
|
||||
@@ -188,6 +189,7 @@
|
||||
export let extraLib: string | undefined = undefined
|
||||
export let changeTimeout: number = 500
|
||||
export let loadAsync = false
|
||||
export let key: string | undefined = undefined
|
||||
|
||||
let lang = scriptLangToEditorLang(scriptLang)
|
||||
$: lang = scriptLangToEditorLang(scriptLang)
|
||||
@@ -1277,6 +1279,10 @@
|
||||
tabSize: lang == 'python' ? 4 : 2,
|
||||
folding
|
||||
})
|
||||
if (key && editorPositionMap?.[key]) {
|
||||
editor.setPosition(editorPositionMap[key])
|
||||
editor.revealPositionInCenterIfOutsideViewport(editorPositionMap[key])
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading monaco:', e)
|
||||
return
|
||||
@@ -1306,6 +1312,10 @@
|
||||
dispatch('blur')
|
||||
})
|
||||
|
||||
editor?.onDidChangeCursorPosition((event) => {
|
||||
if (key) editorPositionMap[key] = event.position
|
||||
})
|
||||
|
||||
editor?.onDidFocusEditorText(() => {
|
||||
dispatch('focus')
|
||||
|
||||
@@ -1496,10 +1506,11 @@
|
||||
})
|
||||
}
|
||||
|
||||
let loadTimeout: NodeJS.Timeout | undefined = undefined
|
||||
onMount(async () => {
|
||||
if (BROWSER) {
|
||||
if (loadAsync) {
|
||||
setTimeout(() => loadMonaco().then((x) => (disposeMethod = x)), 0)
|
||||
loadTimeout = setTimeout(() => loadMonaco().then((x) => (disposeMethod = x)), 0)
|
||||
} else {
|
||||
let m = await loadMonaco()
|
||||
disposeMethod = m
|
||||
@@ -1517,6 +1528,8 @@
|
||||
completorDisposable && completorDisposable.dispose()
|
||||
sqlTypeCompletor && sqlTypeCompletor.dispose()
|
||||
timeoutModel && clearTimeout(timeoutModel)
|
||||
loadTimeout && clearTimeout(loadTimeout)
|
||||
aiChatEditorHandler?.clear()
|
||||
})
|
||||
|
||||
async function genRoot(hostname: string) {
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
diffMode?: boolean
|
||||
showHistoryDrawer?: boolean
|
||||
right?: import('svelte').Snippet
|
||||
openAiChat?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -98,7 +99,8 @@
|
||||
lastDeployedCode = undefined,
|
||||
diffMode = false,
|
||||
showHistoryDrawer = $bindable(false),
|
||||
right
|
||||
right,
|
||||
openAiChat = false
|
||||
}: Props = $props()
|
||||
|
||||
let contextualVariablePicker: ItemPicker | undefined = $state()
|
||||
@@ -810,7 +812,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
{/if}
|
||||
|
||||
{#if customUi?.aiGen != false}
|
||||
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} />
|
||||
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} {openAiChat} />
|
||||
{/if}
|
||||
|
||||
<EditorSettings {customUi} />
|
||||
|
||||
@@ -27,11 +27,13 @@
|
||||
readFieldsRecursively,
|
||||
replaceFalseWithUndefined,
|
||||
type StateStore,
|
||||
type Value
|
||||
type Value,
|
||||
clone
|
||||
} from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { Drawer } from '$lib/components/common'
|
||||
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
|
||||
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'
|
||||
@@ -72,7 +74,13 @@
|
||||
} from './triggers/utils'
|
||||
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
import { TestSteps } from './flows/testSteps.svelte'
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
import {
|
||||
setStepHistoryLoaderContext,
|
||||
StepHistoryLoader,
|
||||
type stepState
|
||||
} from './stepHistoryLoader.svelte'
|
||||
|
||||
interface Props {
|
||||
initialPath?: string
|
||||
@@ -94,6 +102,11 @@
|
||||
draftTriggersFromUrl?: Trigger[] | undefined
|
||||
selectedTriggerIndexFromUrl?: number | undefined
|
||||
children?: import('svelte').Snippet
|
||||
loadedFromHistoryFromUrl?: {
|
||||
flowJobInitial: boolean | undefined
|
||||
stepsState: Record<string, stepState>
|
||||
}
|
||||
noInitial?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -115,7 +128,9 @@
|
||||
setSavedraftCb = undefined,
|
||||
draftTriggersFromUrl = undefined,
|
||||
selectedTriggerIndexFromUrl = undefined,
|
||||
children
|
||||
children,
|
||||
loadedFromHistoryFromUrl,
|
||||
noInitial = false
|
||||
}: Props = $props()
|
||||
|
||||
let initialPathStore = writable(initialPath)
|
||||
@@ -139,6 +154,10 @@
|
||||
let draftTriggersModalOpen = $state(false)
|
||||
let confirmDeploymentCallback: (triggersToDeploy: Trigger[]) => void = () => {}
|
||||
|
||||
// AI changes warning modal
|
||||
let aiChangesWarningOpen = $state(false)
|
||||
let aiChangesConfirmCallback = $state<() => void>(() => {})
|
||||
|
||||
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
|
||||
const { selectedTriggers } = event.detail
|
||||
// Continue with saving the flow
|
||||
@@ -146,12 +165,28 @@
|
||||
confirmDeploymentCallback(selectedTriggers)
|
||||
}
|
||||
|
||||
function hasAIChanges(): boolean {
|
||||
return aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
|
||||
}
|
||||
|
||||
function withAIChangesWarning(callback: () => void) {
|
||||
if (hasAIChanges()) {
|
||||
aiChangesConfirmCallback = () => {
|
||||
aiChatManager.flowAiChatHelpers?.rejectAllModuleActions()
|
||||
callback()
|
||||
}
|
||||
aiChangesWarningOpen = true
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
|
||||
return {
|
||||
savedValue: savedFlow,
|
||||
modifiedValue: {
|
||||
...flowStore.val,
|
||||
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,12 +239,19 @@
|
||||
let loadingDraft = $state(false)
|
||||
|
||||
export async function saveDraft(forceSave = false): Promise<void> {
|
||||
withAIChangesWarning(async () => {
|
||||
await saveDraftInternal(forceSave)
|
||||
})
|
||||
}
|
||||
|
||||
async function saveDraftInternal(forceSave = false): Promise<void> {
|
||||
if (!newFlow && !savedFlow) {
|
||||
return
|
||||
}
|
||||
|
||||
if (savedFlow) {
|
||||
const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow)
|
||||
const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
const currentDraftTriggers = clone(triggersState.getDraftTriggersSnapshot())
|
||||
const current = cleanValueProperties(
|
||||
$state.snapshot({
|
||||
...flowStore.val,
|
||||
@@ -222,7 +264,7 @@
|
||||
{
|
||||
label: 'Save anyway',
|
||||
callback: () => {
|
||||
saveDraft(true)
|
||||
saveDraftInternal(true)
|
||||
}
|
||||
}
|
||||
])
|
||||
@@ -288,15 +330,15 @@
|
||||
savedFlow = {
|
||||
...(newFlow || savedFlow?.draft_only
|
||||
? {
|
||||
...structuredClone($state.snapshot(flowStore.val)),
|
||||
...clone(flowStore.val),
|
||||
path: $pathStore,
|
||||
draft_only: true
|
||||
}
|
||||
: savedFlow),
|
||||
draft: {
|
||||
...structuredClone($state.snapshot(flowStore.val)),
|
||||
...clone(flowStore.val),
|
||||
path: $pathStore,
|
||||
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
|
||||
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
|
||||
}
|
||||
} as FlowWithDraftAndDraftTriggers
|
||||
|
||||
@@ -331,6 +373,12 @@
|
||||
}
|
||||
|
||||
async function handleSaveFlow(deploymentMsg?: string) {
|
||||
withAIChangesWarning(async () => {
|
||||
await handleSaveFlowInternal(deploymentMsg)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSaveFlowInternal(deploymentMsg?: string) {
|
||||
await compareVersions()
|
||||
if (onLatest || initialPath == '' || savedFlow?.draft_only) {
|
||||
// Handle directly
|
||||
@@ -472,7 +520,7 @@
|
||||
draft_triggers: Trigger[]
|
||||
}
|
||||
savedFlow = {
|
||||
...structuredClone($state.snapshot(newSavedFlow)),
|
||||
...clone(newSavedFlow),
|
||||
path: $pathStore
|
||||
} as Flow
|
||||
setDraftTriggers([])
|
||||
@@ -498,7 +546,11 @@
|
||||
path: $pathStore,
|
||||
selectedId: $selectedIdStore,
|
||||
draft_triggers: triggersState.getDraftTriggersSnapshot(),
|
||||
selected_trigger: triggersState.getSelectedTriggerSnapshot()
|
||||
selected_trigger: triggersState.getSelectedTriggerSnapshot(),
|
||||
loadedFromHistory: {
|
||||
flowJobInitial: stepHistoryLoader.flowJobInitial,
|
||||
stepsState: stepHistoryLoader.stepStates
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
@@ -526,7 +578,7 @@
|
||||
payloadData: undefined
|
||||
})
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const testSteps = new TestSteps()
|
||||
|
||||
function select(selectedId: string) {
|
||||
selectedIdStore.set(selectedId)
|
||||
@@ -544,7 +596,7 @@
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
pathStore,
|
||||
testStepStore,
|
||||
testSteps,
|
||||
saveDraft,
|
||||
initialPathStore,
|
||||
fakeInitialPath,
|
||||
@@ -721,7 +773,10 @@
|
||||
disabled?: boolean
|
||||
}[] = $state([])
|
||||
|
||||
function onCustomUiChange(customUi: FlowBuilderWhitelabelCustomUi | undefined) {
|
||||
function onCustomUiChange(
|
||||
customUi: FlowBuilderWhitelabelCustomUi | undefined,
|
||||
hasAiDiff: boolean
|
||||
) {
|
||||
moreItems = [
|
||||
...(customUi?.topBar?.history != false
|
||||
? [
|
||||
@@ -745,7 +800,8 @@
|
||||
{
|
||||
displayName: 'Edit in YAML',
|
||||
icon: FileJson,
|
||||
action: () => yamlEditorDrawer?.openDrawer()
|
||||
action: () => yamlEditorDrawer?.openDrawer(),
|
||||
disabled: hasAiDiff
|
||||
}
|
||||
]
|
||||
: [])
|
||||
@@ -766,6 +822,9 @@
|
||||
|
||||
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
|
||||
|
||||
let forceTestTab: Record<string, boolean> = $state({})
|
||||
let highlightArg: Record<string, string | undefined> = $state({})
|
||||
|
||||
run(() => {
|
||||
initialPathStore.set(initialPath)
|
||||
})
|
||||
@@ -788,8 +847,43 @@
|
||||
initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers())
|
||||
})
|
||||
run(() => {
|
||||
customUi && untrack(() => onCustomUiChange(customUi))
|
||||
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
|
||||
customUi && untrack(() => onCustomUiChange(customUi, hasAiDiff))
|
||||
})
|
||||
|
||||
export async function loadFlowState() {
|
||||
await stepHistoryLoader.loadIndividualStepsStates(
|
||||
flowStore.val as Flow,
|
||||
flowStateStore,
|
||||
$workspaceStore!,
|
||||
$initialPathStore,
|
||||
$pathStore
|
||||
)
|
||||
}
|
||||
|
||||
let stepHistoryLoader = new StepHistoryLoader(
|
||||
loadedFromHistoryFromUrl?.stepsState ?? {},
|
||||
loadedFromHistoryFromUrl?.flowJobInitial,
|
||||
saveSessionDraft,
|
||||
noInitial
|
||||
)
|
||||
setStepHistoryLoaderContext(stepHistoryLoader)
|
||||
|
||||
export function setLoadedFromHistory(
|
||||
loadedFromHistoryUrl:
|
||||
| {
|
||||
flowJobInitial: boolean | undefined
|
||||
stepsState: Record<string, stepState>
|
||||
}
|
||||
| undefined
|
||||
) {
|
||||
if (!loadedFromHistoryUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
stepHistoryLoader.setFlowJobInitial(loadedFromHistoryUrl.flowJobInitial)
|
||||
stepHistoryLoader.stepStates = loadedFromHistoryUrl.stepsState
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
@@ -815,6 +909,8 @@
|
||||
on:confirmed={handleDraftTriggersConfirmed}
|
||||
/>
|
||||
|
||||
<AIChangesWarningModal bind:open={aiChangesWarningOpen} onConfirm={aiChangesConfirmCallback} />
|
||||
|
||||
{#key renderCount}
|
||||
{#if !$userStore?.operator}
|
||||
{#if $pathStore}
|
||||
@@ -947,17 +1043,19 @@
|
||||
|
||||
await syncWithDeployed()
|
||||
|
||||
const currentDraftTriggers = structuredClone(
|
||||
const currentDraftTriggers = clone(
|
||||
triggersState.getDraftTriggersSnapshot()
|
||||
)
|
||||
|
||||
diffDrawer?.openDrawer()
|
||||
const currentFlow =
|
||||
aiChatManager.flowAiChatHelpers?.getPreviewFlow() ?? flowStore.val
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'normal',
|
||||
deployed: deployedValue ?? savedFlow,
|
||||
draft: savedFlow?.draft,
|
||||
current: {
|
||||
...flowStore.val,
|
||||
...currentFlow,
|
||||
path: $pathStore,
|
||||
draft_triggers: currentDraftTriggers
|
||||
}
|
||||
@@ -1020,7 +1118,7 @@
|
||||
{newFlow}
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
$testStepStore['preprocessor'] = ev.detail.args ?? {}
|
||||
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
}
|
||||
}}
|
||||
@@ -1028,8 +1126,27 @@
|
||||
previewArgsStore.val = JSON.parse(JSON.stringify(e.detail))
|
||||
flowPreviewButtons?.openPreview(true)
|
||||
}}
|
||||
onTestUpTo={() => {
|
||||
flowPreviewButtons?.testUpTo()
|
||||
}}
|
||||
{savedFlow}
|
||||
onDeployTrigger={handleDeployTrigger}
|
||||
onEditInput={(moduleId, key) => {
|
||||
selectedIdStore.set(moduleId)
|
||||
// Use new prop-based system
|
||||
forceTestTab[moduleId] = true
|
||||
highlightArg[moduleId] = key
|
||||
// Reset the force flag after a short delay to allow re-triggering
|
||||
setTimeout(() => {
|
||||
forceTestTab[moduleId] = false
|
||||
highlightArg[moduleId] = undefined
|
||||
}, 500)
|
||||
}}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
onRunPreview={() => {
|
||||
flowPreviewButtons?.openPreview(true)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type Job, JobService, type Flow, type RestartedFrom, type OpenFlow } from '$lib/gen'
|
||||
import { type Job, JobService, type RestartedFrom, type OpenFlow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Badge, Button } from './common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
@@ -18,9 +18,11 @@
|
||||
import Toggle from './Toggle.svelte'
|
||||
import JsonInputs from './JsonInputs.svelte'
|
||||
import FlowHistoryJobPicker from './FlowHistoryJobPicker.svelte'
|
||||
import { NEVER_TESTED_THIS_FAR } from './flows/models'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { DurationStatus, GraphModuleState } from './graph'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
|
||||
|
||||
export let previewMode: 'upTo' | 'whole'
|
||||
export let open: boolean
|
||||
@@ -28,7 +30,6 @@
|
||||
|
||||
export let jobId: string | undefined = undefined
|
||||
export let job: Job | undefined = undefined
|
||||
export let initial: boolean = false
|
||||
|
||||
export let selectedJobStep: string | undefined = undefined
|
||||
export let selectedJobStepIsTopLevel: boolean | undefined = undefined
|
||||
@@ -69,15 +70,18 @@
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let stepHistoryLoader = getStepHistoryLoaderContext()
|
||||
|
||||
let renderCount: number = 0
|
||||
let schemaFormWithArgPicker: SchemaFormWithArgPicker | undefined = undefined
|
||||
let currentJobId: string | undefined = undefined
|
||||
|
||||
function extractFlow(previewMode: 'upTo' | 'whole'): OpenFlow {
|
||||
const previewFlow = aiChatManager.flowAiChatHelpers?.getPreviewFlow()
|
||||
if (previewMode === 'whole') {
|
||||
return flowStore.val
|
||||
return previewFlow ?? flowStore.val
|
||||
} else {
|
||||
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
|
||||
const flow = previewFlow ?? stateSnapshot(flowStore).val
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
let upToIndex = idOrders.indexOf($selectedId)
|
||||
|
||||
@@ -93,8 +97,8 @@
|
||||
args: Record<string, any>,
|
||||
restartedFrom: RestartedFrom | undefined
|
||||
) {
|
||||
if (initial) {
|
||||
initial = false
|
||||
if (stepHistoryLoader?.flowJobInitial) {
|
||||
stepHistoryLoader?.setFlowJobInitial(false)
|
||||
}
|
||||
try {
|
||||
lastPreviewFlow = JSON.stringify(flowStore.val)
|
||||
@@ -183,44 +187,6 @@
|
||||
|
||||
$: selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
|
||||
async function loadIndividualStepsStates() {
|
||||
// console.log('loadIndividualStepsStates')
|
||||
dfs(flowStore.val.value.modules, async (module) => {
|
||||
// console.log('module', $flowStateStore[module.id], module.id)
|
||||
const prev = $flowStateStore[module.id]?.previewResult
|
||||
if (prev && prev != NEVER_TESTED_THIS_FAR) {
|
||||
return
|
||||
}
|
||||
const previousJobId = await JobService.listJobs({
|
||||
workspace: $workspaceStore!,
|
||||
scriptPathExact:
|
||||
`path` in module.value
|
||||
? module.value.path
|
||||
: ($initialPathStore == '' ? $pathStore : $initialPathStore) + '/' + module.id,
|
||||
jobKinds: ['preview', 'script', 'flowpreview', 'flow', 'flowscript'].join(','),
|
||||
page: 1,
|
||||
perPage: 1
|
||||
})
|
||||
// console.log('previousJobId', previousJobId, module.id)
|
||||
|
||||
if (previousJobId.length > 0) {
|
||||
const getJobResult = await JobService.getCompletedJobResultMaybe({
|
||||
workspace: $workspaceStore!,
|
||||
id: previousJobId[0].id
|
||||
})
|
||||
if ('result' in getJobResult) {
|
||||
$flowStateStore[module.id] = {
|
||||
...($flowStateStore[module.id] ?? {}),
|
||||
previewResult: getJobResult.result,
|
||||
previewJobId: previousJobId[0].id,
|
||||
previewWorkspaceId: previousJobId[0].workspace_id,
|
||||
previewSuccess: getJobResult.success
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let scrollableDiv: HTMLDivElement | undefined = undefined
|
||||
function handleScroll() {
|
||||
scrollTop = scrollableDiv?.scrollTop ?? 0
|
||||
@@ -275,7 +241,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grow justify-center flex flex-row gap-4">
|
||||
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel}
|
||||
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel && aiChatManager.flowAiChatHelpers?.getModuleAction(selectedJobStep) !== 'removed'}
|
||||
{#if selectedJobStepType == 'single'}
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -379,7 +345,14 @@
|
||||
id="flow-editor-test-flow-drawer"
|
||||
shortCut={{ Icon: CornerDownLeft }}
|
||||
>
|
||||
Test flow
|
||||
{#if previewMode == 'upTo'}
|
||||
Test up to
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{$selectedId}
|
||||
</Badge>
|
||||
{:else}
|
||||
Test flow
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -476,16 +449,15 @@
|
||||
>
|
||||
<FlowHistoryJobPicker
|
||||
selectInitial={jobId == undefined}
|
||||
on:nohistory={() => {
|
||||
loadIndividualStepsStates()
|
||||
}}
|
||||
on:select={(e) => {
|
||||
if (!currentJobId) {
|
||||
currentJobId = jobId
|
||||
}
|
||||
const detail = e.detail
|
||||
initial = detail.initial
|
||||
jobId = detail.jobId
|
||||
if (detail.initial && stepHistoryLoader?.flowJobInitial === undefined) {
|
||||
stepHistoryLoader?.setFlowJobInitial(detail.initial)
|
||||
}
|
||||
}}
|
||||
on:unselect={() => {
|
||||
jobId = currentJobId
|
||||
@@ -495,12 +467,12 @@
|
||||
/>
|
||||
</div>
|
||||
{#if jobId}
|
||||
{#if initial}
|
||||
{#if stepHistoryLoader?.flowJobInitial}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
on:click={() => {
|
||||
initial = false
|
||||
stepHistoryLoader?.setFlowJobInitial(false)
|
||||
}}
|
||||
class="cursor-pointer h-full hover:bg-gray-500/20 dark:hover:bg-gray-500/20 dark:bg-gray-500/80 rounded bg-gray-500/40 absolute top-0 left-0 w-full z-50"
|
||||
>
|
||||
@@ -522,12 +494,6 @@
|
||||
on:done={() => {
|
||||
$executionCount = $executionCount + 1
|
||||
}}
|
||||
on:jobsLoaded={() => {
|
||||
if (initial) {
|
||||
console.log('loading initial steps after initial job loaded')
|
||||
loadIndividualStepsStates()
|
||||
}
|
||||
}}
|
||||
bind:selectedJobStep
|
||||
bind:rightColumnSelect
|
||||
/>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
>
|
||||
| undefined = undefined
|
||||
|
||||
let debounced = debounce(() => computeItems($durationStatuses), 30)
|
||||
let { debounced, clearDebounce } = debounce(() => computeItems($durationStatuses), 30)
|
||||
$: flowDone != undefined && $durationStatuses && debounced()
|
||||
|
||||
export function reset() {
|
||||
@@ -117,6 +117,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
interval && clearInterval(interval)
|
||||
clearDebounce()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -187,8 +188,8 @@
|
||||
? b.started_at
|
||||
? b.started_at - b?.created_at
|
||||
: b.duration_ms
|
||||
? 0
|
||||
: now - b?.created_at
|
||||
? 0
|
||||
: now - b?.created_at
|
||||
: 0}
|
||||
<div class="flex w-full">
|
||||
<TimelineBar
|
||||
@@ -209,7 +210,7 @@
|
||||
{min}
|
||||
concat
|
||||
started_at={b.started_at}
|
||||
len={b.started_at ? b?.duration_ms ?? now - b?.started_at : 0}
|
||||
len={b.started_at ? (b?.duration_ms ?? now - b?.started_at) : 0}
|
||||
running={b?.duration_ms == undefined}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
import Label from './Label.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
export let name: string
|
||||
let can_write = false
|
||||
@@ -197,7 +198,7 @@
|
||||
ownerKind === 'user'
|
||||
? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/' + x))
|
||||
: groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/' + x))}
|
||||
<Select items={items.map((x) => ({ label: x, value: x }))} bind:value={ownerItem} />
|
||||
<Select items={safeSelectItems(items)} bind:value={ownerItem} />
|
||||
{#if ownerKind == 'group'}
|
||||
<Button
|
||||
title="View Group"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user