diff --git a/.github/workflows/aider-after-review.yaml b/.github/workflows/aider-after-review.yaml.archived similarity index 100% rename from .github/workflows/aider-after-review.yaml rename to .github/workflows/aider-after-review.yaml.archived diff --git a/.github/workflows/aider-common.yml b/.github/workflows/aider-common.yml.archived similarity index 100% rename from .github/workflows/aider-common.yml rename to .github/workflows/aider-common.yml.archived diff --git a/.github/workflows/aider-external.yaml b/.github/workflows/aider-external.yaml.archived similarity index 100% rename from .github/workflows/aider-external.yaml rename to .github/workflows/aider-external.yaml.archived diff --git a/.github/workflows/aider.yaml b/.github/workflows/aider.yaml.archived similarity index 100% rename from .github/workflows/aider.yaml rename to .github/workflows/aider.yaml.archived diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index c7d1c50db1..1ddc3a7781 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -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 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 7261fb607c..26420fd6c1 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -64,22 +64,76 @@ jobs: with: fetch-depth: 1 + # Cache rust dependencies + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "./backend -> target" + + # Cache node dependencies + - 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 run check),Bash(npm install),Bash(cargo check),Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y)" - 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, install Rust and then 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. - - Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y): Install Rust. You need this to run cargo check." + 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" diff --git a/.github/workflows/update-sqlx.yaml b/.github/workflows/update-sqlx.yaml new file mode 100644 index 0000000000..7632a9b705 --- /dev/null +++ b/.github/workflows/update-sqlx.yaml @@ -0,0 +1,105 @@ +name: Update SQLx + +on: + issue_comment: + types: [created] + +jobs: + update-sqlx: + if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/updatesqlx') + runs-on: ubicloud-standard-8 + permissions: + contents: write + pull-requests: write + issues: write + + services: + postgres: + image: postgres:14 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: windmill + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Comment on PR - Starting + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Starting sqlx update...' + }) + + - name: Checkout repository + uses: actions/checkout@v3 + with: + ref: ${{ github.event.issue.pull_request.head.ref }} + fetch-depth: 0 + + - name: Checkout windmill-ee-private + uses: actions/checkout@v3 + with: + repository: windmill-labs/windmill-ee-private + path: windmill-ee-private + token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }} + + # Cache rust dependencies + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "./backend -> target" + + - name: Install xmlsec build-time deps + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + pkg-config libxml2-dev libssl-dev \ + xmlsec1 libxmlsec1-dev libxmlsec1-openssl + + - name: Run update-sqlx script + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/windmill + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.issue.number }} + BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName) + echo "Checking out PR branch: $BRANCH_NAME" + git checkout $BRANCH_NAME + mkdir frontend/build + cd backend + cargo install sqlx-cli --version 0.8.5 + sqlx migrate run + ./update_sqlx.sh --dir ./windmill-ee-private + # Pass the branch name to the next step + echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV + + - name: Commit changes if any + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add backend/.sqlx + git commit -m "Update SQLx metadata" + git push origin ${{ env.BRANCH_NAME }} + + - name: Comment on PR - Completed + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Successfully ran sqlx update' + }) diff --git a/CHANGELOG.md b/CHANGELOG.md index a496bda1b5..d52dfb4622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [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) diff --git a/CLAUDE.md b/CLAUDE.md index 5a6512b371..f61e0336be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/backend/.sqlx/query-0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05.json b/backend/.sqlx/query-0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05.json deleted file mode 100644 index fd8f30417f..0000000000 --- a/backend/.sqlx/query-0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n path\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "path", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "TextArray", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05" -} diff --git a/backend/.sqlx/query-144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db.json b/backend/.sqlx/query-144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db.json deleted file mode 100644 index e573bfdab7..0000000000 --- a/backend/.sqlx/query-144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "route_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "route_path_key", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "workspaced_route", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "http_method: _", - "type_info": { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - } - }, - { - "ordinal": 8, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 12, - "name": "is_async", - "type_info": "Bool" - }, - { - "ordinal": 13, - "name": "authentication_method: _", - "type_info": { - "Custom": { - "name": "authentication_method", - "kind": { - "Enum": [ - "none", - "windmill", - "api_key", - "basic_http", - "custom_script", - "signature" - ] - } - } - } - }, - { - "ordinal": 14, - "name": "static_asset_config: _", - "type_info": "Jsonb" - }, - { - "ordinal": 15, - "name": "is_static_website", - "type_info": "Bool" - }, - { - "ordinal": 16, - "name": "authentication_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "wrap_body", - "type_info": "Bool" - }, - { - "ordinal": 18, - "name": "raw_string", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false - ] - }, - "hash": "144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db" -} diff --git a/backend/.sqlx/query-187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b.json b/backend/.sqlx/query-187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b.json deleted file mode 100644 index db7d3ab2ec..0000000000 --- a/backend/.sqlx/query-187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n edited_at = now(), \n is_static_website = $16\n WHERE \n workspace_id = $17 AND \n path = $18\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Bool", - "Bool", - "Bool", - "Varchar", - "Varchar", - "Varchar", - "Bool", - { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - }, - "Jsonb", - "Varchar", - "Varchar", - "Bool", - { - "Custom": { - "name": "authentication_method", - "kind": { - "Enum": [ - "none", - "windmill", - "api_key", - "basic_http", - "custom_script", - "signature" - ] - } - } - }, - "Bool", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b" -} diff --git a/backend/.sqlx/query-4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d.json b/backend/.sqlx/query-33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8.json similarity index 84% rename from backend/.sqlx/query-4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d.json rename to backend/.sqlx/query-33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8.json index e0c5d2bb4c..01dfc9ebe3 100644 --- a/backend/.sqlx/query-4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d.json +++ b/backend/.sqlx/query-33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ", + "query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ", "describe": { "columns": [ { @@ -31,5 +31,5 @@ false ] }, - "hash": "4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d" + "hash": "33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8" } diff --git a/backend/.sqlx/query-3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119.json b/backend/.sqlx/query-3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119.json deleted file mode 100644 index 5121dca128..0000000000 --- a/backend/.sqlx/query-3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n path\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "path", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "TextArray", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119" -} diff --git a/backend/.sqlx/query-56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364.json b/backend/.sqlx/query-56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364.json deleted file mode 100644 index d547567d28..0000000000 --- a/backend/.sqlx/query-56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "workspaced_route", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "route_path", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "route_path_key", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "authentication_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 12, - "name": "is_async", - "type_info": "Bool" - }, - { - "ordinal": 13, - "name": "authentication_method: _", - "type_info": { - "Custom": { - "name": "authentication_method", - "kind": { - "Enum": [ - "none", - "windmill", - "api_key", - "basic_http", - "custom_script", - "signature" - ] - } - } - } - }, - { - "ordinal": 14, - "name": "http_method: _", - "type_info": { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - } - }, - { - "ordinal": 15, - "name": "static_asset_config: _", - "type_info": "Jsonb" - }, - { - "ordinal": 16, - "name": "is_static_website", - "type_info": "Bool" - }, - { - "ordinal": 17, - "name": "wrap_body", - "type_info": "Bool" - }, - { - "ordinal": 18, - "name": "raw_string", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false - ] - }, - "hash": "56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364" -} diff --git a/backend/.sqlx/query-6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71.json b/backend/.sqlx/query-6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71.json deleted file mode 100644 index fe40dfd0cd..0000000000 --- a/backend/.sqlx/query-6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "route_path", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "http_method: _", - "type_info": { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - } - }, - { - "ordinal": 2, - "name": "is_async", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "workspaced_route", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "TextArray", - "TextArray", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71" -} diff --git a/backend/.sqlx/query-7aa589db3199d7f727cc69e63e1281b7ed329ff0c9d1617747f4ccd6014720cf.json b/backend/.sqlx/query-7aa589db3199d7f727cc69e63e1281b7ed329ff0c9d1617747f4ccd6014720cf.json new file mode 100644 index 0000000000..b2101cc0d4 --- /dev/null +++ b/backend/.sqlx/query-7aa589db3199d7f727cc69e63e1281b7ed329ff0c9d1617747f4ccd6014720cf.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM variable WHERE account = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int4", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "7aa589db3199d7f727cc69e63e1281b7ed329ff0c9d1617747f4ccd6014720cf" +} diff --git a/backend/.sqlx/query-8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12.json b/backend/.sqlx/query-8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12.json deleted file mode 100644 index 961a55d8b1..0000000000 --- a/backend/.sqlx/query-8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Bool", - "Bool", - "Varchar", - "Bool", - "Bool", - { - "Custom": { - "name": "authentication_method", - "kind": { - "Enum": [ - "none", - "windmill", - "api_key", - "basic_http", - "custom_script", - "signature" - ] - } - } - }, - { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - }, - "Jsonb", - "Varchar", - "Varchar", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12" -} diff --git a/backend/.sqlx/query-c925264b7b0fd44ea7ab01c9af1514b9a9f2200e5a5db0a741697b28cd8b505f.json b/backend/.sqlx/query-c925264b7b0fd44ea7ab01c9af1514b9a9f2200e5a5db0a741697b28cd8b505f.json new file mode 100644 index 0000000000..37025d99d8 --- /dev/null +++ b/backend/.sqlx/query-c925264b7b0fd44ea7ab01c9af1514b9a9f2200e5a5db0a741697b28cd8b505f.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT account FROM variable WHERE path = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "account", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "c925264b7b0fd44ea7ab01c9af1514b9a9f2200e5a5db0a741697b28cd8b505f" +} diff --git a/backend/.sqlx/query-d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163.json b/backend/.sqlx/query-dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd.json similarity index 84% rename from backend/.sqlx/query-d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163.json rename to backend/.sqlx/query-dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd.json index febfc4c21f..57f859d416 100644 --- a/backend/.sqlx/query-d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163.json +++ b/backend/.sqlx/query-dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ", + "query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ", "describe": { "columns": [ { @@ -31,5 +31,5 @@ false ] }, - "hash": "d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163" + "hash": "dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd" } diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md new file mode 100644 index 0000000000..1fe509a0d6 --- /dev/null +++ b/backend/CLAUDE.md @@ -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 diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 24fb8436bb..73a66bb231 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -741,9 +741,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" @@ -902,9 +902,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.74.0" +version = "1.75.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d440e1d368759bd10df0dbdddbfff6473d7cd73e9d9ef2363dc9995ac2d711" +checksum = "e3258fa707f2f585ee3049d9550954b959002abd59176975150a01d5cf38ae3f" dependencies = [ "aws-credential-types", "aws-runtime", @@ -2303,9 +2303,9 @@ dependencies = [ [[package]] name = "croner" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38fd53511eaf0b00a185613875fee58b208dfce016577d0ad4bb548e1c4fb3ee" +checksum = "c344b0690c1ad1c7176fe18eb173e0c927008fdaaa256e40dfd43ddd149c0843" dependencies = [ "chrono", ] @@ -7188,9 +7188,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.173" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libduckdb-sys" @@ -10410,7 +10410,7 @@ dependencies = [ [[package]] name = "rmcp" version = "0.1.5" -source = "git+https://github.com/modelcontextprotocol/rust-sdk#d1884fe598cd4c524ff6e0101859a60d4b4c8b8f" +source = "git+https://github.com/modelcontextprotocol/rust-sdk#8a577659f01917289064c0a3c753392b44305ef4" dependencies = [ "base64 0.22.1", "bytes", @@ -10439,7 +10439,7 @@ dependencies = [ [[package]] name = "rmcp-macros" version = "0.1.5" -source = "git+https://github.com/modelcontextprotocol/rust-sdk#d1884fe598cd4c524ff6e0101859a60d4b4c8b8f" +source = "git+https://github.com/modelcontextprotocol/rust-sdk#8a577659f01917289064c0a3c753392b44305ef4" dependencies = [ "proc-macro2", "quote", @@ -11922,9 +11922,9 @@ dependencies = [ [[package]] name = "sse-stream" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f649a9f9e91db2ed32f3724516eac2bc09fab77fc33be8f670f5619b9dc6c3f" +checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" dependencies = [ "bytes", "futures-util", @@ -14564,7 +14564,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "axum", @@ -14616,7 +14616,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "argon2", @@ -14727,7 +14727,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.498.0" +version = "1.499.0" dependencies = [ "base64 0.22.1", "chrono", @@ -14742,7 +14742,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.498.0" +version = "1.499.0" dependencies = [ "chrono", "serde", @@ -14755,7 +14755,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "serde", @@ -14769,7 +14769,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "async-stream", @@ -14846,7 +14846,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.498.0" +version = "1.499.0" dependencies = [ "regex", "serde", @@ -14860,7 +14860,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "bytes", @@ -14883,7 +14883,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.498.0" +version = "1.499.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -14895,7 +14895,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.498.0" +version = "1.499.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -14904,7 +14904,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "lazy_static", @@ -14916,7 +14916,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "serde_json", @@ -14928,7 +14928,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "gosyn", @@ -14940,7 +14940,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "lazy_static", @@ -14952,7 +14952,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "serde_json", @@ -14964,7 +14964,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "nu-parser", @@ -14975,7 +14975,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14986,7 +14986,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14997,7 +14997,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "async-recursion", @@ -15020,7 +15020,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15037,7 +15037,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "lazy_static", @@ -15049,7 +15049,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "lazy_static", @@ -15067,7 +15067,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "getrandom 0.2.16", @@ -15091,7 +15091,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "serde_json", @@ -15101,7 +15101,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "async-recursion", @@ -15134,7 +15134,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.498.0" +version = "1.499.0" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15144,7 +15144,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.498.0" +version = "1.499.0" dependencies = [ "anyhow", "async-recursion", @@ -15865,18 +15865,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 1ac013b916..506f360852 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.498.0" +version = "1.499.0" authors.workspace = true edition.workspace = true @@ -32,7 +32,7 @@ members = [ ] [workspace.package] -version = "1.498.0" +version = "1.499.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/.cursor/rules/rust-best-practices.mdc b/backend/rust-best-practices.mdc similarity index 100% rename from .cursor/rules/rust-best-practices.mdc rename to backend/rust-best-practices.mdc diff --git a/backend/update_sqlx.sh b/backend/update_sqlx.sh index 09824d9e24..6e64115ce0 100755 --- a/backend/update_sqlx.sh +++ b/backend/update_sqlx.sh @@ -1,4 +1,18 @@ -./substitute_ee_code.sh --dir ../windmill-ee-private +#!/bin/bash + +# Default directory +EE_DIR="../windmill-ee-private" + +# Parse arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --dir) EE_DIR="$2"; shift ;; + *) echo "Unknown parameter: $1"; exit 1 ;; + esac + shift +done + +./substitute_ee_code.sh --dir "$EE_DIR" # Check if running on macOS if [[ "$(uname)" == "Darwin" ]]; then @@ -18,4 +32,4 @@ if [[ "$(uname)" == "Darwin" ]]; then sed -i '' 's/^#samael = { version="0.0.14", features = \["xmlsec"\] }/samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml # Comment out the git-based samael dependency sed -i '' 's/^\(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/# \1/' Cargo.toml -fi +fi \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index a5e693277e..6922c98379 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.498.0 + version: 1.499.0 title: Windmill API contact: diff --git a/backend/windmill-api/src/configs.rs b/backend/windmill-api/src/configs.rs index 9110d104c8..606cd11ebe 100644 --- a/backend/windmill-api/src/configs.rs +++ b/backend/windmill-api/src/configs.rs @@ -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, Extension(db): Extension, ) -> error::JsonResult> { - 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, ) -> error::Result { - 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, authed: ApiAuthed, ) -> error::Result { - 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, ) -> error::JsonResult> { - 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?; diff --git a/backend/windmill-api/src/openapi.rs b/backend/windmill-api/src/openapi.rs index c0e1d046b6..95f73f95ba 100644 --- a/backend/windmill-api/src/openapi.rs +++ b/backend/windmill-api/src/openapi.rs @@ -9,7 +9,6 @@ use axum::{ }; use http::{header, HeaderValue, Method, StatusCode}; use indexmap::IndexMap; -use itertools::Itertools; use serde::{Deserialize, Serialize}; use serde_json::{to_value, Map, Value}; use sqlx::PgConnection; @@ -24,9 +23,12 @@ use windmill_common::{ use crate::db::ApiAuthed; #[cfg(feature = "http_trigger")] -use crate::{ - http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication, - http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as, +use { + crate::{ + http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication, + http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as, + }, + itertools::Itertools, }; lazy_static::lazy_static! { @@ -311,9 +313,15 @@ fn generate_paths( } }; - let mut duplicate_webhooks = HashSet::new(); + let mut webhooks = HashSet::new(); for path in paths { + if let Kind::Webhook(WebhookConfig { runnable_kind }) = &path.kind { + if !webhooks.insert((path.route_path.clone(), runnable_kind.to_owned())) { + continue; + } + } + let (route_paths, parameters) = from_route_path_to_openapi_path(&path.route_path, &path.kind)?; @@ -340,9 +348,6 @@ fn generate_paths( let (methods, is_webhook) = match &path.kind { Kind::Webhook(_) => { - if !duplicate_webhooks.insert(route_path.clone()) { - return Err(anyhow!("Found duplicate webhook: {}", path.route_path).into()); - } is_async = route_path.starts_with("/run/"); let methods = if is_async { vec![Method::POST] @@ -354,7 +359,12 @@ fn generate_paths( } Kind::HttpRoute(HttpRouteConfig { method }) => { if path_object.get(&method.to_string()).is_some() { - return Err(anyhow!("Found duplicate route: {}", path.route_path).into()); + return Err(anyhow!( + "Found duplicate {} method, for route at path: {}", + method, + path.route_path + ) + .into()); } is_async = path.is_async.unwrap_or(true); (vec![method.to_owned()], false) @@ -809,14 +819,6 @@ async fn webhook_to_future_paths( summary: Option, } - impl PartialEq for MinifiedWebhook { - fn eq(&self, other: &Self) -> bool { - self.path == other.path - } - } - - impl Eq for MinifiedWebhook {} - let webhook_scripts = sqlx::query_as!( MinifiedWebhook, r#"SELECT @@ -827,16 +829,14 @@ async fn webhook_to_future_paths( script WHERE path ~ ANY($1) AND - workspace_id = $2 + workspace_id = $2 AND + archived is FALSE "#, &script_webhook_filter, &w_id ) .fetch_all(&mut *pg_pool) - .await? - .into_iter() - .unique() - .collect_vec(); + .await?; let webhook_flows = sqlx::query_as!( MinifiedWebhook, @@ -848,16 +848,14 @@ async fn webhook_to_future_paths( flow WHERE path ~ ANY($1) AND - workspace_id = $2 + workspace_id = $2 AND + archived is FALSE "#, &flow_webhook_filter, &w_id ) .fetch_all(&mut *pg_pool) - .await? - .into_iter() - .unique() - .collect_vec(); + .await?; openapi_future_paths.reserve_exact(webhook_scripts.len() + webhook_flows.len()); diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 7dc6a85b49..4b6601e1e6 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -450,6 +450,7 @@ struct EditVariable { value: Option, is_secret: Option, description: Option, + account: Option, } #[derive(Deserialize)] @@ -506,6 +507,10 @@ async fn update_variable( sqlb.set_str("description", &desc); } + if let Some(account_id) = ns.account { + sqlb.set_str("account", account_id); + } + if let Some(nbool) = ns.is_secret { let old_secret = sqlx::query_scalar!( "SELECT is_secret from variable WHERE path = $1 AND workspace_id = $2", @@ -523,6 +528,21 @@ async fn update_variable( sqlb.set_str("is_secret", nbool); } sqlb.returning("path"); + + // Get old account_id if we're updating the account field + let old_account_id = if ns.account.is_some() { + sqlx::query_scalar!( + "SELECT account FROM variable WHERE path = $1 AND workspace_id = $2", + &path, + &w_id + ) + .fetch_optional(&db) + .await? + .flatten() + } else { + None + }; + let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?; if let Some(npath) = ns.path { @@ -575,6 +595,33 @@ async fn update_variable( None, ) .await?; + + // Clean up old account if it's no longer referenced and different from new account + if let Some(old_acc_id) = old_account_id { + if ns.account.is_some() && ns.account != Some(old_acc_id) { + // Check if old account is still referenced by other variables or resources + let account_still_used = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM variable WHERE account = $1 AND workspace_id = $2)", + old_acc_id, + &w_id + ) + .fetch_one(&mut *tx) + .await? + .unwrap_or(true); + + if !account_still_used { + // Delete the orphaned account + sqlx::query!( + "DELETE FROM account WHERE id = $1 AND workspace_id = $2", + old_acc_id, + &w_id + ) + .execute(&mut *tx) + .await?; + } + } + } + tx.commit().await?; handle_deployment_metadata( diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 1676148b00..3e89cd98b2 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -4028,6 +4028,7 @@ pub async fn push<'c, 'd>( ), }; + let final_priority: Option; #[cfg(not(feature = "enterprise"))] { diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 35815e452c..90ef0afccd 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -62,12 +62,52 @@ lazy_static::lazy_static! { static ref EPHEMERAL_TOKEN_CMD: Option = 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 = { + 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) { + 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; @@ -674,7 +714,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)] @@ -1890,8 +1937,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}"); } } @@ -2193,3 +2248,4 @@ for line in sys.stdin: ) .await } + diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 45de71358f..a988908588 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -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.499.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/main.ts b/cli/main.ts index 3a505bbc18..f0ad23f8cf 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -63,7 +63,7 @@ export { // } // }); -export const VERSION = "1.498.0"; +export const VERSION = "1.499.0"; const command = new Command() .name("wmill") diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 0000000000..2caad3c0cf --- /dev/null +++ b/frontend/CLAUDE.md @@ -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 +}) +``` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 02bb998500..ce57c0026d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.498.0", + "version": "1.499.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.498.0", + "version": "1.499.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -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", diff --git a/frontend/package.json b/frontend/package.json index bee87418b0..cab3c1d9a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.498.0", + "version": "1.499.0", "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", diff --git a/frontend/src/lib/components/ArgEnum.svelte b/frontend/src/lib/components/ArgEnum.svelte index b1f94d39f0..d87d34ea33 100644 --- a/frontend/src/lib/components/ArgEnum.svelte +++ b/frontend/src/lib/components/ArgEnum.svelte @@ -1,7 +1,7 @@ diff --git a/frontend/src/lib/components/DefaultTagsInner.svelte b/frontend/src/lib/components/DefaultTagsInner.svelte index 5e4e824787..ab715770de 100644 --- a/frontend/src/lib/components/DefaultTagsInner.svelte +++ b/frontend/src/lib/components/DefaultTagsInner.svelte @@ -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 @@ {/each} -
+
{#if limitToWorkspaces} - {/if} diff --git a/frontend/src/lib/components/DeployButton.svelte b/frontend/src/lib/components/DeployButton.svelte index 22d0ef473f..e7151c3990 100644 --- a/frontend/src/lib/components/DeployButton.svelte +++ b/frontend/src/lib/components/DeployButton.svelte @@ -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() diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 5b5681b3b3..91044ead7a 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -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} - + {/if} diff --git a/frontend/src/lib/components/FolderEditor.svelte b/frontend/src/lib/components/FolderEditor.svelte index 5ae36a7e24..38f980f042 100644 --- a/frontend/src/lib/components/FolderEditor.svelte +++ b/frontend/src/lib/components/FolderEditor.svelte @@ -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))} - {#if ownerKind == 'group'} diff --git a/frontend/src/lib/components/InputTransformForm.svelte b/frontend/src/lib/components/InputTransformForm.svelte index 2252652663..b54daaeac5 100644 --- a/frontend/src/lib/components/InputTransformForm.svelte +++ b/frontend/src/lib/components/InputTransformForm.svelte @@ -177,6 +177,9 @@ if (arg.type) { propertyType = arg.type } + if (arg.expr != undefined) { + arg.expr = undefined + } } } @@ -332,6 +335,8 @@ untrack(() => updatePropertyType()) }) $effect(() => { + arg.value + arg.expr inputCat && propertyType && arg && untrack(() => onArgChange()) }) $effect(() => { diff --git a/frontend/src/lib/components/LightweightResourcePicker.svelte b/frontend/src/lib/components/LightweightResourcePicker.svelte index 50da38607b..87570466b4 100644 --- a/frontend/src/lib/components/LightweightResourcePicker.svelte +++ b/frontend/src/lib/components/LightweightResourcePicker.svelte @@ -8,7 +8,7 @@ import type { AppViewerContext } from './apps/types' import { sendUserToast } from '$lib/toast' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' - import Select from './Select.svelte' + import Select from './select/Select.svelte' const dispatch = createEventDispatcher() const dispatchIfMounted = createDispatcherIfMounted(dispatch) diff --git a/frontend/src/lib/components/ObjectResourceInput.svelte b/frontend/src/lib/components/ObjectResourceInput.svelte index a39355898e..d6594a2ddd 100644 --- a/frontend/src/lib/components/ObjectResourceInput.svelte +++ b/frontend/src/lib/components/ObjectResourceInput.svelte @@ -4,40 +4,43 @@ import S3ObjectPicker from './S3ObjectPicker.svelte' import type SimpleEditor from './SimpleEditor.svelte' - export let format: string - export let value: any - export let disablePortal = false - export let showSchemaExplorer = false - export let selectFirst = false - export let defaultValue: any - export let editor: SimpleEditor | undefined = undefined function isString(value: any) { return typeof value === 'string' || value instanceof String } - export let path: string = '' - - function resourceToValue() { - if (path && path != '') { - value = `$res:${path}` - } else { - value = undefined - } + interface Props { + format: string + value: any + disablePortal?: boolean + showSchemaExplorer?: boolean + selectFirst?: boolean + defaultValue: any + editor?: SimpleEditor | undefined + path?: string } + let { + format, + value = $bindable(), + disablePortal = false, + showSchemaExplorer = false, + selectFirst = false, + defaultValue, + editor = $bindable(undefined) + }: Props = $props() + function isResource() { return isString(value) && value.length >= '$res:'.length } function valueToPath() { if (isResource()) { - path = value.substr('$res:'.length) + return value.substr('$res:'.length) } } - - $: value && valueToPath() +
{#if format === 'resource-s3_object'} @@ -45,12 +48,17 @@ { - path = e.detail - resourceToValue() - }} on:clear - bind:value={path} + bind:value={ + () => valueToPath(), + (v) => { + if (v == undefined) { + value = undefined + } else { + value = `$res:${v}` + } + } + } initialValue={typeof defaultValue == 'string' && defaultValue.startsWith('$res:') ? defaultValue.substr('$res:'.length) : defaultValue} diff --git a/frontend/src/lib/components/OktaSetting.svelte b/frontend/src/lib/components/OktaSetting.svelte index d6ac83aee5..5a1836ec7d 100644 --- a/frontend/src/lib/components/OktaSetting.svelte +++ b/frontend/src/lib/components/OktaSetting.svelte @@ -1,6 +1,4 @@ diff --git a/frontend/src/lib/components/ResourcePicker.svelte b/frontend/src/lib/components/ResourcePicker.svelte index 69e8a4bf9e..f136fd32a6 100644 --- a/frontend/src/lib/components/ResourcePicker.svelte +++ b/frontend/src/lib/components/ResourcePicker.svelte @@ -10,23 +10,44 @@ import { Pen, Plus, RotateCw } from 'lucide-svelte' import { sendUserToast } from '$lib/toast' import { isDbType } from './apps/components/display/dbtable/utils' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' const dispatch = createEventDispatcher() const dispatchIfMounted = createDispatcherIfMounted(dispatch) - export let initialValue: string | undefined = undefined - export let value: string | undefined = initialValue - export let valueType: string | undefined = undefined - export let resourceType: string | undefined = undefined - export let disabled = false - export let disablePortal = false - export let showSchemaExplorer = false - export let selectFirst = false - export let expressOAuthSetup = false - export let defaultValues: Record | undefined = undefined - export let placeholder: string | undefined = undefined + interface Props { + initialValue?: string | undefined + value?: string | undefined + valueType?: string | undefined + resourceType?: string | undefined + disabled?: boolean + disablePortal?: boolean + showSchemaExplorer?: boolean + selectFirst?: boolean + expressOAuthSetup?: boolean + defaultValues?: Record | undefined + placeholder?: string | undefined + } + + let { + initialValue = $bindable(undefined), + value = $bindable(undefined), + valueType = $bindable(undefined), + resourceType = undefined, + disabled = false, + disablePortal = false, + showSchemaExplorer = false, + selectFirst = false, + expressOAuthSetup = false, + defaultValues = undefined, + placeholder = undefined + }: Props = $props() + + if (initialValue && value == undefined) { + console.log('initialValue', initialValue) + value = initialValue + } onMount(() => { setTimeout(() => { @@ -36,26 +57,36 @@ }, 500) }) - let valueSelect = - initialValue || value - ? { - value: value ?? initialValue, - label: value ?? initialValue, - type: valueType - } - : undefined + // let valueSelect = $state( + // initialValue || value + // ? { + // value: value ?? initialValue, + // label: value ?? initialValue, + // type: valueType + // } + // : undefined + // ) - $: if (value === undefined && initialValue) { - value = initialValue - } + $effect(() => { + if (value === undefined) { + if (initialValue) { + console.log('initialValue', initialValue) + value = initialValue + } else { + console.log('no value') + } + } else { + console.log('value', value) + } + }) - let collection = valueSelect ? [valueSelect] : [] + let collection = $state(value ? [{ value, label: value, type: valueType }] : []) export async function askNewResource() { appConnect?.open?.(resourceType) } - let loading = true + let loading = $state(true) async function loadResources(resourceType: string | undefined) { loading = true try { @@ -84,10 +115,10 @@ nc.push({ value: value ?? initialValue!, label: value ?? initialValue!, type: '' }) } collection = nc - if (collection.length == 1 && selectFirst && valueSelect == undefined) { + if (collection.length == 1 && selectFirst && value == undefined) { + console.log('selectFirst', collection[0].value) value = collection[0].value valueType = collection[0].type - valueSelect = collection[0] } } catch (e) { sendUserToast('Failed to load resource types', true) @@ -96,12 +127,16 @@ loading = false } - $: $workspaceStore && loadResources(resourceType) + $effect(() => { + $workspaceStore && loadResources(resourceType) + }) - $: dispatchIfMounted('change', value) + $effect(() => { + dispatchIfMounted('change', value) + }) - let appConnect: AppConnect - let resourceEditor: ResourceEditorDrawer + let appConnect: AppConnect | undefined = $state() + let resourceEditor: ResourceEditorDrawer | undefined = $state() x?.value == value)?.type - valueSelect = { - value: e.detail, - label: e.detail, - type: valueType ?? '' - } }} bind:this={appConnect} {expressOAuthSetup} @@ -125,11 +155,11 @@ if (e.detail) { value = e.detail valueType = collection.find((x) => x?.value == value)?.type - valueSelect = { value: e.detail, label: e.detail, type: valueType ?? '' } + // valueSelect = { value: e.detail, label: e.detail, type: valueType ?? '' } } }} /> - +
{#if collection?.length > 0} @@ -137,18 +167,16 @@ {disabled} {disablePortal} bind:value={ - () => valueSelect?.value, + () => value, (v) => { - valueSelect = collection.find((x) => x.value === v) value = v - valueType = valueSelect?.type + valueType = collection.find((x) => x?.value == v)?.type } } onClear={() => { initialValue = undefined value = undefined valueType = undefined - valueSelect = undefined dispatch('clear') }} items={collection} diff --git a/frontend/src/lib/components/SavedInputsV2.svelte b/frontend/src/lib/components/SavedInputsV2.svelte index 538f3ce05f..0fb4f89e58 100644 --- a/frontend/src/lib/components/SavedInputsV2.svelte +++ b/frontend/src/lib/components/SavedInputsV2.svelte @@ -10,7 +10,8 @@ import { ExternalLink, Search } from 'lucide-svelte' import { Popover } from './meltComponents' import SchemaForm from './SchemaForm.svelte' - import MultiSelect from './multiselect/MultiSelectWrapper.svelte' + import MultiSelect from './select/MultiSelect.svelte' + import { safeSelectItems } from './select/utils.svelte' const dispatch = createEventDispatcher() interface Props { @@ -142,7 +143,6 @@ {/snippet} {#snippet content()} -
Search by args
@@ -175,11 +175,10 @@
{#key filteredSchema} diff --git a/frontend/src/lib/components/ScriptPicker.svelte b/frontend/src/lib/components/ScriptPicker.svelte index 0bb0a9caca..c4fe27167a 100644 --- a/frontend/src/lib/components/ScriptPicker.svelte +++ b/frontend/src/lib/components/ScriptPicker.svelte @@ -5,7 +5,7 @@ import { base } from '$lib/base' import { createEventDispatcher, untrack } from 'svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { getScriptByPath } from '$lib/scripts' import { Button, Drawer, DrawerContent } from './common' diff --git a/frontend/src/lib/components/Select.svelte b/frontend/src/lib/components/Select.svelte deleted file mode 100644 index 0afdf2cc32..0000000000 --- a/frontend/src/lib/components/Select.svelte +++ /dev/null @@ -1,271 +0,0 @@ - - - { - if (!open || !processedItems?.length) return - if (e.key === 'ArrowUp' && keyArrowPos !== undefined && processedItems.length > 0) { - keyArrowPos = keyArrowPos <= 0 ? undefined : keyArrowPos - 1 - } else if (e.key === 'ArrowDown') { - if (keyArrowPos === undefined) { - keyArrowPos = 0 - } else { - keyArrowPos = Math.min(processedItems.length - 1, keyArrowPos + 1) - } - } else if (e.key === 'Enter' && keyArrowPos !== undefined && processedItems?.[keyArrowPos]) { - setValue(processedItems[keyArrowPos]) - } else { - keyArrowPos = undefined - return - } - e.preventDefault() - }} -/> - -
(open = false) }} - onpointerdown={() => onFocus?.()} - onfocus={() => onFocus?.()} - onblur={() => onBlur?.()} -> - {#if loading} -
- -
- {:else if clearable && !disabled && value} -
- -
- {:else if RightIcon} -
- -
- {/if} - - filterText, (v) => (filterText = v)} - placeholder={loading ? 'Loading...' : (valueEntry?.label ?? placeholder)} - style={containerStyle} - class={twMerge( - '!bg-surface text-ellipsis', - open ? '' : 'cursor-pointer', - valueEntry && !loading ? '!placeholder-primary' : '', - (clearable || RightIcon) && !disabled && value ? '!pr-8' : '', - inputClass ?? '' - )} - autocomplete="off" - onpointerdown={() => (open = true)} - bind:this={inputEl} - /> - - - {#if open && !disabled} -
- {#if processedItems?.length === 0} -
No items
- {/if} - {#each processedItems ?? [] as item, itemIndex} - {#if (item.__select_group && itemIndex === 0) || processedItems?.[itemIndex - 1]?.__select_group !== item.__select_group} -
- {item.__select_group} -
- {/if} - - {/each} -
- {/if} -
-
diff --git a/frontend/src/lib/components/ServiceLogsInner.svelte b/frontend/src/lib/components/ServiceLogsInner.svelte index 4504d4e69d..b8b233bf6f 100644 --- a/frontend/src/lib/components/ServiceLogsInner.svelte +++ b/frontend/src/lib/components/ServiceLogsInner.svelte @@ -11,14 +11,13 @@ import { sendUserToast } from '$lib/toast' import { onDestroy, tick, untrack } from 'svelte' import { Loader2 } from 'lucide-svelte' - import { copyToClipboard, truncateRev } from '$lib/utils' + import { copyToClipboard, scroll_into_view_if_needed_polyfill, truncateRev } from '$lib/utils' import LogSnippetViewer from './LogSnippetViewer.svelte' import { Button, Drawer, DrawerContent } from './common' import ClipboardCopy from 'lucide-svelte/icons/clipboard-copy' import AnsiUp from 'ansi_up' - import { scroll_into_view_if_needed_polyfill } from './multiselect/utils' import SplitPanesOrColumnOnMobile from './splitPanes/SplitPanesOrColumnOnMobile.svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { goto } from '$lib/navigation' import { page } from '$app/stores' diff --git a/frontend/src/lib/components/ShareModal.svelte b/frontend/src/lib/components/ShareModal.svelte index eac1b0fefd..a8eeeeb9d1 100644 --- a/frontend/src/lib/components/ShareModal.svelte +++ b/frontend/src/lib/components/ShareModal.svelte @@ -13,7 +13,8 @@ import { isOwner } from '$lib/utils' import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' + import { safeSelectItems } from './select/utils.svelte' const dispatch = createEventDispatcher() @@ -138,9 +139,9 @@
{#key ownerKind} !worker_tags?.includes(x)) - .map((x) => ({ value: x, label: x }))} - {disabled} - bind:value={newTag} - onFocus={() => dispatch('focus')} - onCreateItem={(c) => (createdTags.push(c), (createdTags = [...createdTags]), (newTag = c))} - createText="Press Enter to use this tag" - /> - -
- - -
-
-{/if} + worker_tags, + (w) => ((worker_tags = w.map((s) => s.replaceAll(' ', '_'))), dispatch('dirty')) + } + {disabled} + class={disabled ? 'border-0' : ''} + allowClear={!disabled} + onCreateItem={(c) => worker_tags.push(c)} + createText="Press Enter to use this tag" +/> diff --git a/frontend/src/lib/components/TeamSelector.svelte b/frontend/src/lib/components/TeamSelector.svelte index 638c30d22f..899314bb31 100644 --- a/frontend/src/lib/components/TeamSelector.svelte +++ b/frontend/src/lib/components/TeamSelector.svelte @@ -2,7 +2,7 @@ import { workspaceStore } from '$lib/stores' import { RefreshCcw } from 'lucide-svelte' import { WorkspaceService } from '$lib/gen' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { onMount } from 'svelte' interface TeamItem { diff --git a/frontend/src/lib/components/TriggerableByAI.svelte b/frontend/src/lib/components/TriggerableByAI.svelte index 0c6e2a049b..7c5d1ae8d7 100644 --- a/frontend/src/lib/components/TriggerableByAI.svelte +++ b/frontend/src/lib/components/TriggerableByAI.svelte @@ -7,13 +7,13 @@ onTrigger, children, showAnimation = true - } = $props<{ + }: { id: string | undefined description: string | undefined onTrigger?: (value?: string) => void // Function to call when the trigger is activated, if not provided, the component is discoverable for information purposes only children?: () => any showAnimation?: boolean - }>() + } = $props() let isAnimating = $state(false) diff --git a/frontend/src/lib/components/WorkerGroup.svelte b/frontend/src/lib/components/WorkerGroup.svelte index c822f9bd2c..403995db2c 100644 --- a/frontend/src/lib/components/WorkerGroup.svelte +++ b/frontend/src/lib/components/WorkerGroup.svelte @@ -1,7 +1,6 @@ @@ -209,7 +210,7 @@ drawer?.closeDrawer()} - title={$superadmin ? `Edit worker config '${name}'` : `Worker config '${name}'`} + title={($superadmin || $devopsRole) ? `Edit worker config '${name}'` : `Worker config '${name}'`} > {#if !$enterpriseLicense} @@ -340,7 +341,7 @@ {#if defaultTagPerWorkspace} { @@ -442,7 +423,7 @@ }} bind:value={nconfig.dedicated_worker} /> - {#if $superadmin} + {#if $superadmin || $devopsRole}
- {#if $superadmin} + {#if $superadmin || $devopsRole}
- {#if !superadmin} + {#if !($superadmin || $devopsRole)}
diff --git a/frontend/src/lib/components/WorkerRepl.svelte b/frontend/src/lib/components/WorkerRepl.svelte index 71dff962e4..7939e4b4d5 100644 --- a/frontend/src/lib/components/WorkerRepl.svelte +++ b/frontend/src/lib/components/WorkerRepl.svelte @@ -19,7 +19,7 @@ import Tooltip from './Tooltip.svelte' import { JobService, type QueuedJob } from '$lib/gen' import { sendUserToast } from '$lib/toast' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { emptyString } from '$lib/utils' let container: HTMLDivElement diff --git a/frontend/src/lib/components/WorkerTagSelect.svelte b/frontend/src/lib/components/WorkerTagSelect.svelte index 616cb6d809..e337c55631 100644 --- a/frontend/src/lib/components/WorkerTagSelect.svelte +++ b/frontend/src/lib/components/WorkerTagSelect.svelte @@ -3,19 +3,20 @@ import { WorkerService } from '$lib/gen' import { createEventDispatcher } from 'svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' + import { safeSelectItems } from './select/utils.svelte' let { tag = $bindable(), noLabel = false, nullTag = undefined, disabled = false - } = $props<{ + }: { tag: string | undefined noLabel?: boolean nullTag?: string | undefined disabled?: boolean - }>() + } = $props() loadWorkerGroups() @@ -41,7 +42,7 @@ class="w-full" {disabled} placeholder={nullTag ? `default: ${nullTag}` : 'lang default'} - items={items.map((value) => ({ value }))} + items={safeSelectItems(items)} bind:value={() => tag, (value) => ((tag = value), dispatch('change', value))} />
diff --git a/frontend/src/lib/components/apps/components/buttons/AppForm.svelte b/frontend/src/lib/components/apps/components/buttons/AppForm.svelte index 3df674e5b0..70346d042e 100644 --- a/frontend/src/lib/components/apps/components/buttons/AppForm.svelte +++ b/frontend/src/lib/components/apps/components/buttons/AppForm.svelte @@ -101,7 +101,6 @@ componentStyle={$app.css?.formcomponent} /> {/each} - () + } = $props() const { componentControl, worldStore, selectedComponent, connectingInput, mode } = getContext('AppViewerContext') diff --git a/frontend/src/lib/components/apps/components/inputs/AppDateSelect.svelte b/frontend/src/lib/components/apps/components/inputs/AppDateSelect.svelte index bd454eb780..74b6830e9d 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppDateSelect.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppDateSelect.svelte @@ -11,7 +11,8 @@ import ResolveStyle from '../helpers/ResolveStyle.svelte' import { twMerge } from 'tailwind-merge' import { enUS, fr, de, pt, ja } from 'date-fns/locale' - import Select from '$lib/components/Select.svelte' + import Select from '$lib/components/select/Select.svelte' + import { safeSelectItems } from '$lib/components/select/utils.svelte' export let id: string export let configuration: RichConfigurations @@ -238,9 +239,10 @@ outputs.day.set(v ? Number(v) : undefined) } } - items={Array.from({ length: computeDayPerMonth(selectedMonth, selectedYear) }, (_, i) => { - return { label: String(i + 1), value: String(i + 1) } - })} + items={Array.from( + { length: computeDayPerMonth(selectedMonth, selectedYear) }, + (_, i) => ({ value: String(i + 1) }) + )} class={twMerge('text-clip min-w-0', css?.input?.class, 'wm-date-select')} containerStyle={css?.input?.style} placeholder="Pick a day" @@ -280,9 +282,7 @@ outputs.year.set(selectedYear ? Number(selectedYear) : undefined) } } - items={Array.from({ length: 201 }, (_, i) => `${1900 + i}`).map((value) => ({ - value - }))} + items={safeSelectItems(Array.from({ length: 201 }, (_, i) => `${1900 + i}`))} placeholder="Pick a year" class={twMerge('text-clip min-w-0', css?.input?.class, 'wm-date-select')} containerStyle={css?.input?.style} diff --git a/frontend/src/lib/components/apps/components/inputs/AppMultiSelect.svelte b/frontend/src/lib/components/apps/components/inputs/AppMultiSelect.svelte index d586fd85f3..ac686d97cb 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppMultiSelect.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppMultiSelect.svelte @@ -3,9 +3,6 @@ import AlignWrapper from '../helpers/AlignWrapper.svelte' import InitializeComponent from '../helpers/InitializeComponent.svelte' - // @ts-ignore - import MultiSelect from 'svelte-multiselect' - export let id: string export let configuration: RichConfigurations export let customCss: ComponentCustomCSS<'multiselectcomponent'> | undefined = undefined diff --git a/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte b/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte index 642b109464..929e4cb147 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte @@ -1,5 +1,5 @@ -{#each Object.keys(components['multiselectcomponent'].initialData.configuration) as key (key)} +{#each Object.keys(components['multiselectcomponentv2'].initialData.configuration) as key (key)} {/each} @@ -193,74 +160,33 @@
{ + class={twMerge('w-full', resolvedConfig.allowOverflow ? '' : 'absolute inset-0')} + onpointerdown={(e) => { $selectedComponent = [id] - - if (!e.shiftKey) { - e.stopPropagation() - } + if (!e.shiftKey) e.stopPropagation() selectedComponent.set([id]) }} - use:floatingRef - bind:clientWidth={w} > - {#if !selectedItems || Array.isArray(selectedItems)} - { - if (event?.detail?.type === 'removeAll') { - outputs?.result.set([]) - setContextValue([]) - } else { - setResultsFromSelectedItems() + { + customItems.push(item) + selectedItems.push(item) + customItems = customItems + selectedItems = selectedItems } - }} - on:open={() => { - $selectedComponent = [id] - open = true - }} - on:close={() => { - open = false - }} - let:option - > - - -
{ - let newe = new MouseEvent('mouseup') - e.target?.['parentElement']?.dispatchEvent(newe) - }} - > - {typeof option == 'object' ? (option?.label ?? 'NO_LABEL') : option} -
-
- - - - {:else} - Value {selectedItems} is not an array - {/if} + : undefined} + onOpen={() => ($selectedComponent = [id])} + />
diff --git a/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte b/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte index 5c9fff9c35..4e7d7dc8fc 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte @@ -19,7 +19,7 @@ import { Bug } from 'lucide-svelte' import Popover from '$lib/components/Popover.svelte' import ResolveStyle from '../helpers/ResolveStyle.svelte' - import Select from '$lib/components/Select.svelte' + import Select from '$lib/components/select/Select.svelte' interface Props { id: string diff --git a/frontend/src/lib/components/apps/components/layout/AppDecisionTree.svelte b/frontend/src/lib/components/apps/components/layout/AppDecisionTree.svelte index c29bb46825..d595a7d85a 100644 --- a/frontend/src/lib/components/apps/components/layout/AppDecisionTree.svelte +++ b/frontend/src/lib/components/apps/components/layout/AppDecisionTree.svelte @@ -24,19 +24,29 @@ let { id, componentContainerHeight, customCss = undefined, render, nodes }: Props = $props() - let resolvedConditions = $derived( - nodes.reduce((acc, node) => { + let resolvedConditions = $state(createResolvedConditions()) + + function createResolvedConditions() { + return nodes.reduce((acc, node) => { acc[node.id] = acc[node.id] || [] return acc }, {}) - ) + } - let resolvedNext = $derived( - nodes.reduce((acc, node) => { + let resolvedNext = $state(createResolvedNext()) + + function createResolvedNext() { + return nodes.reduce((acc, node) => { acc[node.id] = acc[node.id] || false return acc }, {}) - ) + } + + $effect(() => { + nodes + resolvedConditions = untrack(() => createResolvedConditions()) + resolvedNext = untrack(() => createResolvedNext()) + }) let everRender = $state(render) $effect.pre(() => { @@ -165,6 +175,8 @@ }) + {#if Object.keys(resolvedConditions).length === nodes.length} {#each nodes ?? [] as node (node.id)} {#each node.next ?? [] as next, conditionIndex} diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index d7555b8460..75db79da69 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -1256,7 +1256,7 @@ {:else}
- Select a component to see the settings for it + Select a component to see the settings for it
{/if} diff --git a/frontend/src/lib/components/apps/editor/DecisionTreeDebug.svelte b/frontend/src/lib/components/apps/editor/DecisionTreeDebug.svelte index 6ad9713e86..bf70dafd61 100644 --- a/frontend/src/lib/components/apps/editor/DecisionTreeDebug.svelte +++ b/frontend/src/lib/components/apps/editor/DecisionTreeDebug.svelte @@ -1,4 +1,7 @@ +
({ id, forceStyle: true, forceClass: true })) }, ...Object.entries(ccomponents) - .filter(([key]) => key !== 'quillcomponent') + .filter(([key]) => !['quillcomponent', ...Object.keys(DEPRECATED_COMPONENTS)].includes(key)) .map(([type, { name, icon, customCss }]) => ({ type: type as keyof typeof components, name, diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts b/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts index 4634959ac0..8545d0627c 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts +++ b/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts @@ -676,8 +676,8 @@ export const customisationByComponent: Customisation[] = [ comment: 'buttons to remove a single or all selected options at once' }, { - selector: '.multiselect > input[autocomplete]', - comment: 'input inside the top-level wrapper div' + selector: '.multiselect.dropdown', + comment: 'dropdown container' }, { selector: '.multiselect > ul.options', @@ -686,23 +686,6 @@ export const customisationByComponent: Customisation[] = [ { selector: '.multiselect > ul.options > li', comment: 'dropdown list items' - }, - { - selector: '.multiselect > ul.options > li.selected', - comment: 'selected options in the dropdown list' - }, - { - selector: '.multiselect > ul.options > li:not(.selected):hover', - comment: 'unselected but hovered options in the dropdown list' - }, - { - selector: '.multiselect > ul.options > li.active', - comment: - 'active item, navigated to with up/down arrow keys and ready to be selected by pressing enter' - }, - { - selector: '.multiselect > ul.options > li.disabled', - comment: 'options with disabled key set to true' } ], variables: [ diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/DecisionTreeGraphEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/DecisionTreeGraphEditor.svelte index 909ff00c74..85c420f152 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/DecisionTreeGraphEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/DecisionTreeGraphEditor.svelte @@ -57,7 +57,13 @@
-
+
{ + // Prevent keyboard events from bubbling to SvelteFlow + e.stopPropagation() + }} + > {#if selectedNode}
@@ -92,6 +98,10 @@ renderCount++ }, 300)() }} + on:keydown={(e) => { + // Prevent keyboard events from bubbling to SvelteFlow + e.stopPropagation() + }} /> diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte index 1bf94a76a4..37697dec9d 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte @@ -1,16 +1,21 @@ {#if condition} - + {@render children?.()} {:else} - + {@render children?.()} {/if} diff --git a/frontend/src/lib/components/copilot/ScriptGen.svelte b/frontend/src/lib/components/copilot/ScriptGen.svelte index 02e48cf20a..cb7ab6c946 100644 --- a/frontend/src/lib/components/copilot/ScriptGen.svelte +++ b/frontend/src/lib/components/copilot/ScriptGen.svelte @@ -28,6 +28,7 @@ import { twMerge } from 'tailwind-merge' import { onDestroy } from 'svelte' import ProviderModelSelector from './chat/ProviderModelSelector.svelte' + import { aiChatManager, AIMode } from './chat/AIChatManager.svelte' interface Props { // props @@ -38,6 +39,7 @@ inlineScript?: boolean args: Record transformer?: boolean + openAiChat?: boolean } let { @@ -47,7 +49,8 @@ diffEditor, inlineScript = false, args, - transformer = false + transformer = false, + openAiChat = false }: Props = $props() run(() => { @@ -72,6 +75,54 @@ run(() => { trimmedDesc = funcDesc.trim() }) + async function callCopilot() { + if (mode === 'edit') { + await copilot( + { + // @ts-ignore + language: transformer && lang === 'frontend' ? 'transformer' : lang!, + description: trimmedDesc, + code: editor?.getCode() || '', + dbSchema: dbSchema, + type: 'edit', + workspace: $workspaceStore! + }, + generatedCode, + abortController + ) + } else { + await copilot( + { + // @ts-ignore + language: transformer && lang === 'frontend' ? 'transformer' : lang!, + description: trimmedDesc, + dbSchema: dbSchema, + type: 'gen', + workspace: $workspaceStore! + }, + generatedCode, + abortController + ) + } + } + + function handleAiButtonClick() { + if (editor && isInitialCode(editor.getCode())) { + mode = 'gen' + } else { + mode = 'edit' + } + + if (openAiChat) { + // Open the AI chat in script mode + aiChatManager.openChat() + aiChatManager.changeMode(AIMode.SCRIPT) + } else { + setTimeout(() => { + autoResize() + }, 0) + } + } async function onGenerate(closePopup: () => void) { if (trimmedDesc.length <= 0) { @@ -82,34 +133,7 @@ genLoading = true blockPopupOpen = true abortController = new AbortController() - if (mode === 'edit') { - await copilot( - { - // @ts-ignore - language: transformer && lang === 'frontend' ? 'transformer' : lang!, - description: trimmedDesc, - code: editor?.getCode() || '', - dbSchema: dbSchema, - type: 'edit', - workspace: $workspaceStore! - }, - generatedCode, - abortController - ) - } else { - await copilot( - { - // @ts-ignore - language: transformer && lang === 'frontend' ? 'transformer' : lang!, - description: trimmedDesc, - dbSchema: dbSchema, - type: 'gen', - workspace: $workspaceStore! - }, - generatedCode, - abortController - ) - } + await callCopilot() setupDiff() diffEditor?.setModified($generatedCode) blockPopupOpen = false @@ -205,14 +229,27 @@ } let promptHistory: string[] = $state([]) - function getPromptHistory() { + + function getPromptStorageKey() { + return 'prompts-' + lang + } + + function safeLocalStorageOperation(operation: () => T, defaultValue?: T): T | undefined { try { - promptHistory = JSON.parse(localStorage.getItem('prompts-' + lang) || '[]') + return operation() } catch (e) { console.error('error interacting with local storage', e) + return defaultValue } } + function getPromptHistory() { + const storageKey = getPromptStorageKey() + promptHistory = + safeLocalStorageOperation(() => JSON.parse(localStorage.getItem(storageKey) || '[]'), []) || + [] + } + function savePrompt() { if (promptHistory.includes(trimmedDesc)) { return @@ -221,20 +258,14 @@ while (promptHistory.length > 5) { promptHistory.pop() } - try { - localStorage.setItem('prompts-' + lang, JSON.stringify(promptHistory)) - } catch (e) { - console.error('error interacting with local storage', e) - } + const storageKey = getPromptStorageKey() + safeLocalStorageOperation(() => localStorage.setItem(storageKey, JSON.stringify(promptHistory))) } function clearPromptHistory() { promptHistory = [] - try { - localStorage.setItem('prompts-' + lang, JSON.stringify(promptHistory)) - } catch (e) { - console.error('error interacting with local storage', e) - } + const storageKey = getPromptStorageKey() + safeLocalStorageOperation(() => localStorage.setItem(storageKey, JSON.stringify(promptHistory))) } run(() => { lang && getPromptHistory() @@ -266,6 +297,12 @@ ;(dbSchema as any).publicOnly = detail === 'true' } + const aiChatScriptModeClasses = $derived( + aiChatManager.mode === AIMode.SCRIPT && aiChatManager.isOpen + ? 'dark:bg-violet-900 bg-violet-100' + : '' + ) + onDestroy(() => { abortController?.abort() }) @@ -343,22 +380,9 @@
{/if} diff --git a/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte b/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte index 1b5f472dba..489837a2cb 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte @@ -5,10 +5,10 @@ import { WorkerService } from '$lib/gen' import WorkerTagSelect from '$lib/components/WorkerTagSelect.svelte' - let { tag = $bindable(), nullTag = $bindable() } = $props<{ + let { tag = $bindable(), nullTag = $bindable() }: { tag: string | undefined nullTag?: string | undefined - }>() + } = $props() const { flowStore, selectedId } = getContext('FlowEditorContext') diff --git a/frontend/src/lib/components/multiselect/MultiSelect.svelte b/frontend/src/lib/components/multiselect/MultiSelect.svelte deleted file mode 100644 index 1e4f00c66d..0000000000 --- a/frontend/src/lib/components/multiselect/MultiSelect.svelte +++ /dev/null @@ -1,828 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/multiselect/MultiSelectWrapper.svelte b/frontend/src/lib/components/multiselect/MultiSelectWrapper.svelte deleted file mode 100644 index b42b205da7..0000000000 --- a/frontend/src/lib/components/multiselect/MultiSelectWrapper.svelte +++ /dev/null @@ -1,130 +0,0 @@ - - - - -
- {#if !value || Array.isArray(value)} -
- [...value], - (newVal) => { - if (!deepEqual(value, newVal)) { - value = newVal - } - } - } - {placeholder} - options={items} - on:close={() => { - open = false - }} - on:open={() => { - open = true - }} - let:option - disableRemoveAll - > -
{ - e.stopPropagation() - }} - onpointerdown={(e) => { - e.stopPropagation() - let newe = new MouseEvent('mouseup') - e.target?.['parentElement']?.dispatchEvent(newe) - }} - aria-selected={value?.includes(option)} - > - {option} -
-
-
- - - - {:else} - Value {value} is not an array - {/if} -
diff --git a/frontend/src/lib/components/multiselect/types.ts b/frontend/src/lib/components/multiselect/types.ts deleted file mode 100644 index d3dc4d7cac..0000000000 --- a/frontend/src/lib/components/multiselect/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -export type Option = string | number | ObjectOption - -// single CSS string or an object with keys 'option' and 'selected', each a string, -// which only apply to the dropdown list and list of selected options, respectively -export type OptionStyle = string | { option: string; selected: string } - -export type ObjectOption = { - label: string | number // user-displayed text - value?: unknown // associated value, can be anything incl. objects (defaults to label if undefined) - title?: string // on-hover tooltip - disabled?: boolean // make this option unselectable - preselected?: boolean // make this option selected on page load (before any user interaction) - disabledTitle?: string // override the default disabledTitle = 'This option is disabled' - selectedTitle?: string // tooltip to display when this option is selected and hovered - style?: OptionStyle - [key: string]: unknown // allow any other keys users might want -} - -export type DispatchEvents = { - add: { option: T } - create: { option: T } - remove: { option: T } - removeAll: { options: T[] } - change: { - option?: T - options?: T[] - type: 'add' | 'remove' | 'removeAll' - } - open: { event: Event } - close: { event: Event } -} - -export type MultiSelectEvents = { - [key in keyof DispatchEvents]: CustomEvent -} & { - blur: FocusEvent - click: MouseEvent - focus: FocusEvent - keydown: KeyboardEvent - keyup: KeyboardEvent - mouseenter: MouseEvent - mouseleave: MouseEvent - touchcancel: TouchEvent - touchend: TouchEvent - touchmove: TouchEvent - touchstart: TouchEvent -} diff --git a/frontend/src/lib/components/multiselect/utils.ts b/frontend/src/lib/components/multiselect/utils.ts deleted file mode 100644 index a9bcfcaa69..0000000000 --- a/frontend/src/lib/components/multiselect/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Option, OptionStyle } from './types' - -// get the label key from an option object or the option itself if it's a string or number -export const get_label = (opt: Option) => { - if (opt instanceof Object) { - if (opt.label === undefined) { - console.error(`MultiSelect option ${JSON.stringify(opt)} is an object but has no label key`) - } - return opt.label - } - return `${opt}` -} - -// this function is used extract CSS strings from a {selected, option} style object to be used in the style attribute of the option -// if the style is a string, it will be returned as is -export function get_style( - option: { style?: OptionStyle; [key: string]: unknown } | string | number, - key: 'selected' | 'option' | null = null -) { - let css_str = `` - if (![`selected`, `option`, null].includes(key)) { - console.error(`MultiSelect: Invalid key=${key} for get_style`) - } - if (typeof option == `object` && option.style) { - if (typeof option.style == `string`) { - css_str = option.style - } - if (typeof option.style == `object`) { - if (key && key in option.style) return option.style[key] ?? `` - else { - console.error(`Invalid style object for option=${JSON.stringify(option)}`) - } - } - } - // ensure css_str ends with a semicolon - if (css_str.trim() && !css_str.trim().endsWith(`;`)) css_str += `;` - return css_str -} - -// Firefox lacks support for scrollIntoViewIfNeeded (https://caniuse.com/scrollintoviewifneeded). -// See https://github.com/janosh/svelte-multiselect/issues/87 -// Polyfill copied from -// https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js -// exported for testing -export function scroll_into_view_if_needed_polyfill(elem: Element, centerIfNeeded: boolean = true) { - const observer = new IntersectionObserver(function ([entry]) { - const ratio = entry.intersectionRatio - if (ratio < 1) { - const place = ratio <= 0 && centerIfNeeded ? `center` : `nearest` - elem.scrollIntoView({ - block: place, - inline: place - }) - } - observer.disconnect() - }, { - root: null, // or specify a scrolling parent if needed - rootMargin: '0px 1000px', // Essentially making horizontal checks irrelevant - threshold: 0.1 // Adjust threshold to control when observer should trigger -}) - observer.observe(elem) - - return observer // return for testing -} diff --git a/frontend/src/lib/components/runs/RunsFilter.svelte b/frontend/src/lib/components/runs/RunsFilter.svelte index 6fd0b0ed03..77ab068bfe 100644 --- a/frontend/src/lib/components/runs/RunsFilter.svelte +++ b/frontend/src/lib/components/runs/RunsFilter.svelte @@ -12,7 +12,8 @@ import { createEventDispatcher, untrack } from 'svelte' import ToggleButtonMore from '../common/toggleButton-v2/ToggleButtonMore.svelte' import Popover from '$lib/components/meltComponents/Popover.svelte' - import Select from '../Select.svelte' + import Select from '../select/Select.svelte' + import { safeSelectItems } from '../select/utils.svelte' interface Props { // Filters @@ -174,7 +175,7 @@
User ({ label: p, value: p }))} + items={safeSelectItems(folders)} bind:value={() => folder ?? undefined, (v) => (folder = v ?? null)} clearable onClear={() => ((folder = null), dispatch('reset'))} @@ -203,7 +204,7 @@
Path ({ label: p, value: p }))} + items={safeSelectItems(usernames)} bind:value={() => user ?? undefined, (v) => (user = v ?? null)} clearable onClear={() => ((user = null), dispatch('reset'))} @@ -575,7 +576,7 @@
+ {/if} + {/snippet} + +
diff --git a/frontend/src/lib/components/select/Select.svelte b/frontend/src/lib/components/select/Select.svelte new file mode 100644 index 0000000000..ec0a4dcc47 --- /dev/null +++ b/frontend/src/lib/components/select/Select.svelte @@ -0,0 +1,156 @@ + + +
(open = false) }} + onpointerdown={() => onFocus?.()} + onfocus={() => onFocus?.()} + onblur={() => onBlur?.()} +> + {#if loading} +
+ +
+ {:else if clearable && !disabled && value} +
+ +
+ {:else if RightIcon} +
+ +
+ {/if} + + filterText, (v) => (filterText = v)} + placeholder={loading ? 'Loading...' : (valueEntry?.label ?? placeholder)} + style={containerStyle} + class={twMerge( + '!bg-surface text-ellipsis', + open ? '' : 'cursor-pointer', + valueEntry && !loading ? '!placeholder-primary' : '', + (clearable || RightIcon) && !disabled && value ? '!pr-8' : '', + inputClass ?? '' + )} + autocomplete="off" + onpointerdown={() => (open = true)} + bind:this={inputEl} + /> + inputEl!.getBoundingClientRect())} + {listAutoWidth} + {noItemsMsg} + /> +
diff --git a/frontend/src/lib/components/select/SelectDropdown.svelte b/frontend/src/lib/components/select/SelectDropdown.svelte new file mode 100644 index 0000000000..7780828a70 --- /dev/null +++ b/frontend/src/lib/components/select/SelectDropdown.svelte @@ -0,0 +1,147 @@ + + + { + if (!open || !processedItems?.length) return + if (e.key === 'ArrowUp' && keyArrowPos !== undefined && processedItems.length > 0) { + keyArrowPos = keyArrowPos <= 0 ? undefined : keyArrowPos - 1 + } else if (e.key === 'ArrowDown') { + if (keyArrowPos === undefined) { + keyArrowPos = 0 + } else { + keyArrowPos = Math.min(processedItems.length - 1, keyArrowPos + 1) + } + } else if (e.key === 'Enter' && keyArrowPos !== undefined && processedItems?.[keyArrowPos]) { + onSelectValue(processedItems[keyArrowPos]) + } else { + keyArrowPos = undefined + return + } + e.preventDefault() + }} +/> + + + {#if open && !disabled} +
+ {@render header?.()} + {#if processedItems?.length === 0} +
{noItemsMsg}
+ {/if} +
    + {#each processedItems ?? [] as item, itemIndex} + {#if (item.__select_group && itemIndex === 0) || processedItems?.[itemIndex - 1]?.__select_group !== item.__select_group} +
  • + {item.__select_group} +
  • + {/if} +
  • + +
  • + {/each} +
+
+ {/if} +
diff --git a/frontend/src/lib/components/select/utils.svelte.ts b/frontend/src/lib/components/select/utils.svelte.ts new file mode 100644 index 0000000000..f3d43b40c1 --- /dev/null +++ b/frontend/src/lib/components/select/utils.svelte.ts @@ -0,0 +1,72 @@ +export function processItems({ + items, + filterText, + groupBy, + sortBy, + onCreateItem, + createText +}: { + items?: Item[] + filterText?: string + groupBy?: (item: Item) => string + sortBy?: (a: Item, b: Item) => number + onCreateItem?: (value: string) => void + createText?: string +}): ProcessedItem[] { + let items2 = + items?.map((item) => ({ + ...item, + label: getLabel(item) + })) ?? [] + if (groupBy) { + items2 = + items2?.map((item) => ({ + ...item, + __select_group: groupBy(item) + })) ?? [] + } + if (sortBy) { + items2 = items2?.sort(sortBy) + } + if (onCreateItem && filterText && !items2.some((item) => item.label === filterText)) { + items2.push({ + label: createText ?? `Add new: "${filterText}"`, + value: filterText, + __is_create: true + } as any) + } + return items2 +} + +export type ProcessedItem = { + __select_group?: string + __is_create?: true + label: string + value: T +} + +export function getLabel(item: { label?: string; value: T } | undefined): string { + if (!item) return '' + if (item.label) return item.label + if (typeof item.value === 'string') return item.value + if (typeof item.value == 'number' || typeof item.value == 'boolean') return item.value.toString() + + return JSON.stringify(item.value) +} + +export function safeSelectItems( + list: (T | { value: T; label?: string } | undefined | null)[] | undefined | null +): { value: T; label?: string }[] { + if (!list) return [] + return list + .filter((item) => item !== undefined && item !== null) + .map((item) => { + if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') { + return { value: item } + } + if (typeof item === 'object' && 'value' in item) { + return item + } + return { value: null as any, label: 'UNKNOWN_ITEM' } + }) +} diff --git a/frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte b/frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte index 3753051eaf..28747e5e45 100644 --- a/frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte +++ b/frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte @@ -7,7 +7,7 @@ import { Pen } from 'lucide-svelte' import Toggle from '$lib/components/Toggle.svelte' - let { open = false } = $props<{ open?: boolean }>() + let { open = false }: { open?: boolean } = $props() let colorEnabled = $state(false) let editingColor = $state(undefined) diff --git a/frontend/src/lib/components/settings/ChangeWorkspaceName.svelte b/frontend/src/lib/components/settings/ChangeWorkspaceName.svelte index fa79f6a824..42e9e8a12e 100644 --- a/frontend/src/lib/components/settings/ChangeWorkspaceName.svelte +++ b/frontend/src/lib/components/settings/ChangeWorkspaceName.svelte @@ -7,7 +7,7 @@ import { Pen } from 'lucide-svelte' import { untrack } from 'svelte' - let { open = false } = $props<{ open?: boolean }>() + let { open = false }: { open?: boolean } = $props() let newName = $state('') let currentName = $state('') diff --git a/frontend/src/lib/components/settings/CreateToken.svelte b/frontend/src/lib/components/settings/CreateToken.svelte index dd25ca2da9..70c60e2aca 100644 --- a/frontend/src/lib/components/settings/CreateToken.svelte +++ b/frontend/src/lib/components/settings/CreateToken.svelte @@ -3,7 +3,6 @@ import { Button } from '../common' import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte' import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte' - import MultiSelectWrapper from '../multiselect/MultiSelectWrapper.svelte' import TriggerableByAI from '../TriggerableByAI.svelte' import Toggle from '../Toggle.svelte' import { UserService, type NewToken } from '$lib/gen' @@ -39,7 +38,7 @@ let newMcpScope = $state('favorites') let loadingApps = $state(false) let errorFetchApps = $state(false) - let allApps = $state([]) + // let allApps = $state([]) function handleCopyClick() { copyToClipboard(newToken ?? '') @@ -205,7 +204,7 @@ {:else if errorFetchApps}
Error fetching apps
{:else} - + {/if}
diff --git a/frontend/src/lib/components/settings/TokensTable.svelte b/frontend/src/lib/components/settings/TokensTable.svelte index 9601b92f67..d66e6c2df1 100644 --- a/frontend/src/lib/components/settings/TokensTable.svelte +++ b/frontend/src/lib/components/settings/TokensTable.svelte @@ -12,8 +12,9 @@ import Toggle from '../Toggle.svelte' import ClipboardPanel from '../details/ClipboardPanel.svelte' import { sendUserToast } from '$lib/toast' - import MultiSelectWrapper from '../multiselect/MultiSelectWrapper.svelte' import TriggerableByAI from '../TriggerableByAI.svelte' + import MultiSelect from '../select/MultiSelect.svelte' + import { safeSelectItems } from '../select/utils.svelte' // --- Props --- interface Props { @@ -306,10 +307,11 @@ {:else if errorFetchApps}
Error fetching apps
{:else} - {/if}
@@ -319,7 +321,7 @@ +
{#if !mcpCreationMode} @@ -339,7 +341,7 @@ Expires In (optional) - diff --git a/frontend/src/lib/components/table/DataTable.svelte b/frontend/src/lib/components/table/DataTable.svelte index 82b2f3f401..2fed249679 100644 --- a/frontend/src/lib/components/table/DataTable.svelte +++ b/frontend/src/lib/components/table/DataTable.svelte @@ -73,7 +73,7 @@ const hasScrollbar = tableContainer.scrollHeight > tableContainer.clientHeight if (!hasScrollbar && hasMore) { - dispatch('loadMore') + triggerLoadMore() } } @@ -92,11 +92,20 @@ if (!tableContainer) return const { scrollTop, scrollHeight, clientHeight } = tableContainer - if (scrollHeight - (scrollTop + clientHeight) < 50) { - dispatch('loadMore') + if (scrollHeight - (scrollTop + clientHeight) === 0) { + triggerLoadMore() } } + let lastLoadMoreDispatch: number | undefined = $state(undefined) + function triggerLoadMore() { + if (lastLoadMoreDispatch && Date.now() - lastLoadMoreDispatch < 1000) { + return + } + dispatch('loadMore') + lastLoadMoreDispatch = Date.now() + } + $effect(() => { if (tableContainer && hasMore && !loading) { untrack(() => checkScrollStatus()) @@ -170,7 +179,7 @@