diff --git a/.github/workflows/change-versions.yml b/.github/workflows/change-versions.yml index 034b022f06..eea62bf6b7 100644 --- a/.github/workflows/change-versions.yml +++ b/.github/workflows/change-versions.yml @@ -9,7 +9,14 @@ jobs: runs-on: ubicloud container: node:18 steps: + - uses: actions/create-github-app-token@v2 + id: app + with: + app-id: ${{ vars.INTERNAL_APP_ID }} + private-key: ${{ secrets.INTERNAL_APP_KEY }} - uses: actions/checkout@v4 + with: + token: ${{ steps.app.outputs.token }} - run: git config --system --add safe.directory /__w/windmill/windmill - name: Change versions run: ./.github/change-versions.sh "$(cat version.txt)" @@ -21,3 +28,8 @@ jobs: cd backend cargo generate-lockfile - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_user_name: windmill-internal-app[bot] + commit_user_email: windmill-internal-app[bot]@users.noreply.github.com + env: + GITHUB_TOKEN: ${{ steps.app.outputs.token }} diff --git a/.github/workflows/check-org-membership.yml b/.github/workflows/check-org-membership.yml new file mode 100644 index 0000000000..910c39c7ba --- /dev/null +++ b/.github/workflows/check-org-membership.yml @@ -0,0 +1,60 @@ +name: Check Organization Membership + +on: + workflow_call: + inputs: + commenter: + required: true + type: string + description: 'The username to check for organization membership' + organization: + required: false + type: string + default: 'windmill-labs' + description: 'The organization to check membership for' + trusted_bot: + required: false + type: string + default: 'windmill-internal-app[bot]' + description: 'The trusted bot username to allow' + secrets: + access_token: + required: true + description: 'The access token to use for org membership check' + outputs: + is_member: + description: 'Whether the user is an organization member or trusted bot' + value: ${{ jobs.check-membership.outputs.is_member }} + +jobs: + check-membership: + runs-on: ubicloud-standard-2 + outputs: + is_member: ${{ steps.check-membership.outputs.is_member }} + steps: + - name: Check organization membership + id: check-membership + env: + ORG_ACCESS_TOKEN: ${{ secrets.access_token }} + COMMENTER: ${{ inputs.commenter }} + ORG: ${{ inputs.organization }} + TRUSTED_BOT: ${{ inputs.trusted_bot }} + run: | + # 1. Allow the trusted bot straight away + if [[ "$COMMENTER" == "$TRUSTED_BOT" ]]; then + echo "is_member=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # 2. Otherwise fall back to the org-membership check + STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $ORG_ACCESS_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/orgs/$ORG/members/$COMMENTER") + + if [ "$STATUS" -eq 204 ]; then + echo "is_member=true" >> $GITHUB_OUTPUT + else + echo "is_member=false" >> $GITHUB_OUTPUT + fi \ No newline at end of file diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 26420fd6c1..0c5d6d59d4 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -11,45 +11,40 @@ on: types: [submitted] jobs: - check-membership: + determine-commenter: if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai') && !contains(github.event.review.user.login, '[bot]')) || - (github.event_name == 'issues' && contains(github.event.issue.body, '/ai') && !contains(github.event.issue.user.login, '[bot]')) + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai')) || + (github.event_name == 'issues' && contains(github.event.issue.body, '/ai')) runs-on: ubicloud-standard-2 outputs: - is_member: ${{ steps.check-membership.outputs.is_member }} + commenter: ${{ steps.determine-commenter.outputs.commenter }} steps: - - name: Check organization membership - id: check-membership - env: - ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} + - name: Determine commenter + id: determine-commenter run: | - ORG="windmill-labs" - - if [[ "${{ github.event_name }}" == "issue_comment" || "${{ github.event_name }}" == "pull_request_review_comment" ]]; then + # Work out who wrote the comment / review + if [[ "${{ github.event_name }}" == "issue_comment" || \ + "${{ github.event_name }}" == "pull_request_review_comment" ]]; then COMMENTER="${{ github.event.comment.user.login }}" elif [[ "${{ github.event_name }}" == "pull_request_review" ]]; then COMMENTER="${{ github.event.review.user.login }}" else COMMENTER="${{ github.event.issue.user.login }}" fi + echo "commenter=$COMMENTER" >> $GITHUB_OUTPUT - STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: token $ORG_ACCESS_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/orgs/$ORG/members/$COMMENTER") - - if [ "$STATUS" -eq 204 ]; then - echo "is_member=true" >> $GITHUB_OUTPUT - else - echo "is_member=false" >> $GITHUB_OUTPUT - fi + check-membership: + needs: determine-commenter + uses: ./.github/workflows/check-org-membership.yml + with: + commenter: ${{ needs.determine-commenter.outputs.commenter }} + secrets: + access_token: ${{ secrets.ORG_ACCESS_TOKEN }} claude-code-action: - needs: check-membership + needs: [determine-commenter, check-membership] if: | needs.check-membership.outputs.is_member == 'true' runs-on: ubicloud-standard-8 @@ -64,13 +59,6 @@ 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 diff --git a/.github/workflows/create-docs.yml b/.github/workflows/create-docs.yml index 2d264f9755..e6b313617c 100644 --- a/.github/workflows/create-docs.yml +++ b/.github/workflows/create-docs.yml @@ -4,38 +4,36 @@ on: jobs: check-membership: - if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && github.event.comment.user.type != 'Bot' }} + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') }} + uses: ./.github/workflows/check-org-membership.yml + with: + commenter: ${{ github.event.comment.user.login }} + secrets: + access_token: ${{ secrets.ORG_ACCESS_TOKEN }} + + generate-token: + needs: check-membership + if: ${{ needs.check-membership.outputs.is_member == 'true' }} runs-on: ubicloud-standard-2 outputs: - is_member: ${{ steps.check-membership.outputs.is_member }} + app_token: ${{ steps.app.outputs.token }} steps: - - name: Check organization membership - id: check-membership - env: - ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} - COMMENTER: ${{ github.event.comment.user.login }} - run: | - ORG="windmill-labs" - STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: token $ORG_ACCESS_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/orgs/$ORG/members/$COMMENTER") - - if [ "$STATUS" -eq 204 ]; then - echo "is_member=true" >> $GITHUB_OUTPUT - else - echo "is_member=false" >> $GITHUB_OUTPUT - fi + - name: Generate an installation token + id: app + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.INTERNAL_APP_ID }} + private-key: ${{ secrets.INTERNAL_APP_KEY }} + owner: windmill-labs trigger-docs: - needs: check-membership - if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && needs.check-membership.outputs.is_member == 'true' }} + needs: [generate-token, check-membership] + if: ${{ needs.check-membership.outputs.is_member == 'true' }} uses: windmill-labs/windmilldocs/.github/workflows/create-docs.yml@main with: pr_number: ${{ github.event.issue.number }} repo: ${{ github.event.repository.name }} comment_text: ${{ github.event.comment.body }} secrets: - DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }} + DOCS_TOKEN: ${{ needs.generate-token.outputs.app_token }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} diff --git a/.github/workflows/discord-notification.yml b/.github/workflows/discord-notification.yml index 012c406cef..525c343fe6 100644 --- a/.github/workflows/discord-notification.yml +++ b/.github/workflows/discord-notification.yml @@ -24,7 +24,7 @@ jobs: DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }} merge_success_emoji: - if: github.event.pull_request.merged == true + if: github.event.action == 'closed' uses: ./.github/workflows/shareable-discord-notification.yml with: PR_STATUS: "merged" diff --git a/.github/workflows/helmchart_on_release.yml b/.github/workflows/helmchart_on_release.yml index eeae218d5d..0a4ee872ce 100644 --- a/.github/workflows/helmchart_on_release.yml +++ b/.github/workflows/helmchart_on_release.yml @@ -9,11 +9,19 @@ jobs: runs-on: ubicloud-standard-2 steps: + - name: Generate an installation token + id: app + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.INTERNAL_APP_ID }} + private-key: ${{ secrets.INTERNAL_APP_KEY }} + owner: windmill-labs + - name: Checkout on helm repository uses: actions/checkout@v3 with: repository: windmill-labs/windmill-helm-charts - token: ${{ secrets.HELM_CHART_TOKEN }} + token: ${{ steps.app.outputs.token }} - name: Get version id: get_version @@ -49,6 +57,23 @@ jobs: APP_VERSION=${APP_VERSION%/} sed -i "s/appVersion: .*/appVersion: $APP_VERSION/" ./charts/windmill/Chart.yaml + - name: Close existing bump-helm PRs + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + run: | + # List open PR numbers whose title starts with the prefix + prs=$(gh pr list \ + --state open \ + --search '"helm: bump version to" in:title' \ + --json number \ + -q '.[].number') + + for pr in $prs; do + echo "Closing outdated bump PR #$pr" + gh pr close "$pr" \ + --comment "Closed automatically – superseded by a newer Helm-chart bump PR." + done + - name: Commit and push run: | git add . @@ -57,7 +82,7 @@ jobs: - name: Create PR env: - GH_TOKEN: ${{ secrets.HELM_CHART_TOKEN }} + GH_TOKEN: ${{ steps.app.outputs.token }} run: | gh pr create \ --title "helm: bump version to ${{ env.VERSION }}" \ diff --git a/.github/workflows/pr-ready-review.yml b/.github/workflows/pr-ready-review.yml new file mode 100644 index 0000000000..bddb0788ae --- /dev/null +++ b/.github/workflows/pr-ready-review.yml @@ -0,0 +1,22 @@ +name: Auto Comment on PR Ready for Review + +on: + pull_request: + types: [opened, ready_for_review] + +jobs: + add-review-comment: + if: github.event.pull_request.draft == false + runs-on: ubicloud-standard-2 + steps: + - name: Add review comment + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PUBLIC_REPO_TOKEN }} + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: '/ai review this PR' + }); diff --git a/CHANGELOG.md b/CHANGELOG.md index d52dfb4622..8c72616fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## [1.500.2](https://github.com/windmill-labs/windmill/compare/v1.500.1...v1.500.2) (2025-06-20) + + +### Bug Fixes + +* consistency of root job propagation fixing cases where runFlow in scripts would fail ([9c2f6a7](https://github.com/windmill-labs/windmill/commit/9c2f6a757fb168c7305c991c9fdbf78acd856a1c)) + +## [1.500.1](https://github.com/windmill-labs/windmill/compare/v1.500.0...v1.500.1) (2025-06-20) + + +### Bug Fixes + +* git repository resource picker effect loop ([#6017](https://github.com/windmill-labs/windmill/issues/6017)) ([1b1bee5](https://github.com/windmill-labs/windmill/commit/1b1bee5b53d78e4407b684b567d0fddd2b5283f3)) + +## [1.500.0](https://github.com/windmill-labs/windmill/compare/v1.499.0...v1.500.0) (2025-06-20) + + +### Features + +* add typescript client context to ai chat system prompt ([#6004](https://github.com/windmill-labs/windmill/issues/6004)) ([3e82282](https://github.com/windmill-labs/windmill/commit/3e822823519d1d5c22e422e4bd1ad4d37b6428b6)) +* blacklist remote agent worker token ([#5985](https://github.com/windmill-labs/windmill/issues/5985)) ([86eb907](https://github.com/windmill-labs/windmill/commit/86eb9074cc94f309f17ea72e9cecd0d502ffd2be)) +* **frontend:** run steps from graph ([#5915](https://github.com/windmill-labs/windmill/issues/5915)) ([67e6bce](https://github.com/windmill-labs/windmill/commit/67e6bce9b2eba1653450921afab3eabbd41fc715)) + + +### Bug Fixes + +* ai button in inline script editor to open AI chat in flow builder ([#5989](https://github.com/windmill-labs/windmill/issues/5989)) ([4ae5928](https://github.com/windmill-labs/windmill/commit/4ae5928788831196672e212b32ca410afab640e0)) +* improve piptar upload - sequential uploads via background task queue ([#5994](https://github.com/windmill-labs/windmill/issues/5994)) ([c4adaee](https://github.com/windmill-labs/windmill/commit/c4adaeeabd287ca1c4f3522bcd8bcea30b00fe6d)) +* new MultiSelect component ([#5979](https://github.com/windmill-labs/windmill/issues/5979)) ([fa8d1b4](https://github.com/windmill-labs/windmill/commit/fa8d1b47db19e15fe854e01f9987c8f97cb45b44)) +* replace worker tags to listen multiselect ([#5997](https://github.com/windmill-labs/windmill/issues/5997)) ([e4255e6](https://github.com/windmill-labs/windmill/commit/e4255e6276565c4a45b1f45a5d627bcfb5369270)) + ## [1.499.0](https://github.com/windmill-labs/windmill/compare/v1.498.0...v1.499.0) (2025-06-18) diff --git a/backend/.sqlx/query-3c0b2a840102b12864c5d721b8e0142602ab37f3e1a95d39b3c7cbd7ff34d0b2.json b/backend/.sqlx/query-019100d178129340a7c35d60ab61f983c8a9cb810db4369554bf26c6b0d6003d.json similarity index 61% rename from backend/.sqlx/query-3c0b2a840102b12864c5d721b8e0142602ab37f3e1a95d39b3c7cbd7ff34d0b2.json rename to backend/.sqlx/query-019100d178129340a7c35d60ab61f983c8a9cb810db4369554bf26c6b0d6003d.json index dbb99a4ec1..9e97808b41 100644 --- a/backend/.sqlx/query-3c0b2a840102b12864c5d721b8e0142602ab37f3e1a95d39b3c7cbd7ff34d0b2.json +++ b/backend/.sqlx/query-019100d178129340a7c35d60ab61f983c8a9cb810db4369554bf26c6b0d6003d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n id As \"id!\",\n flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json\"\n FROM v2_as_queue\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id AND workspace_id = $2", + "query": "SELECT\n id As \"id!\",\n flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json\"\n FROM v2_job_status\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id", "describe": { "columns": [ { @@ -16,14 +16,13 @@ ], "parameters": { "Left": [ - "Uuid", - "Text" + "Uuid" ] }, "nullable": [ - true, + false, null ] }, - "hash": "3c0b2a840102b12864c5d721b8e0142602ab37f3e1a95d39b3c7cbd7ff34d0b2" + "hash": "019100d178129340a7c35d60ab61f983c8a9cb810db4369554bf26c6b0d6003d" } diff --git a/backend/.sqlx/query-1c5d3556fc8436ddd294f39c5431e1f501a821d6143c5d8aece20814237a6b86.json b/backend/.sqlx/query-1c5d3556fc8436ddd294f39c5431e1f501a821d6143c5d8aece20814237a6b86.json new file mode 100644 index 0000000000..adf00884b8 --- /dev/null +++ b/backend/.sqlx/query-1c5d3556fc8436ddd294f39c5431e1f501a821d6143c5d8aece20814237a6b86.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n ORDER BY blacklisted_at DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "expires_at", + "type_info": "Timestamp" + }, + { + "ordinal": 2, + "name": "blacklisted_at", + "type_info": "Timestamp" + }, + { + "ordinal": 3, + "name": "blacklisted_by", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "1c5d3556fc8436ddd294f39c5431e1f501a821d6143c5d8aece20814237a6b86" +} diff --git a/backend/.sqlx/query-2bf99d540365c228e1776ee5d2ba01ebe289183526afab19c1390bbf5082f019.json b/backend/.sqlx/query-2bf99d540365c228e1776ee5d2ba01ebe289183526afab19c1390bbf5082f019.json new file mode 100644 index 0000000000..04acf05335 --- /dev/null +++ b/backend/.sqlx/query-2bf99d540365c228e1776ee5d2ba01ebe289183526afab19c1390bbf5082f019.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM agent_token_blacklist WHERE token = $1 AND expires_at > $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Timestamp" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2bf99d540365c228e1776ee5d2ba01ebe289183526afab19c1390bbf5082f019" +} diff --git a/backend/.sqlx/query-54fee31b61d62598c89cf7d0729079ac1721fe7bd1844f339236379211defc78.json b/backend/.sqlx/query-54fee31b61d62598c89cf7d0729079ac1721fe7bd1844f339236379211defc78.json new file mode 100644 index 0000000000..b41cb7bf8c --- /dev/null +++ b/backend/.sqlx/query-54fee31b61d62598c89cf7d0729079ac1721fe7bd1844f339236379211defc78.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM agent_token_blacklist WHERE token = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "54fee31b61d62598c89cf7d0729079ac1721fe7bd1844f339236379211defc78" +} diff --git a/backend/.sqlx/query-995b194da28092d5aa053df936e7a9ee4b80cf3ade038a032c57ecff8fa3c6cf.json b/backend/.sqlx/query-995b194da28092d5aa053df936e7a9ee4b80cf3ade038a032c57ecff8fa3c6cf.json new file mode 100644 index 0000000000..d5946b5114 --- /dev/null +++ b/backend/.sqlx/query-995b194da28092d5aa053df936e7a9ee4b80cf3ade038a032c57ecff8fa3c6cf.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM agent_token_blacklist WHERE expires_at <= now() RETURNING token", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "995b194da28092d5aa053df936e7a9ee4b80cf3ade038a032c57ecff8fa3c6cf" +} diff --git a/backend/.sqlx/query-cf12a70e7b75ae471a0944de34502384be156cf25129f9c52bda34b240cf469a.json b/backend/.sqlx/query-b46a0fbebdc8e5e9852a06444b0aeaa4eaf67959e68b69eb2f0896ebe9244691.json similarity index 51% rename from backend/.sqlx/query-cf12a70e7b75ae471a0944de34502384be156cf25129f9c52bda34b240cf469a.json rename to backend/.sqlx/query-b46a0fbebdc8e5e9852a06444b0aeaa4eaf67959e68b69eb2f0896ebe9244691.json index 962572fa3d..2f6737bfb2 100644 --- a/backend/.sqlx/query-cf12a70e7b75ae471a0944de34502384be156cf25129f9c52bda34b240cf469a.json +++ b/backend/.sqlx/query-b46a0fbebdc8e5e9852a06444b0aeaa4eaf67959e68b69eb2f0896ebe9244691.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT leaf_jobs->$1::text AS \"leaf_jobs: Json>\", parent_job\n FROM v2_as_queue\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = id AND workspace_id = $3", + "query": "SELECT flow_leaf_jobs->$1::text AS \"leaf_jobs: Json>\", v2_job.parent_job\n FROM v2_job_status\n LEFT JOIN v2_job ON v2_job.id = v2_job_status.id AND v2_job.workspace_id = $3\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = v2_job_status.id", "describe": { "columns": [ { @@ -26,5 +26,5 @@ true ] }, - "hash": "cf12a70e7b75ae471a0944de34502384be156cf25129f9c52bda34b240cf469a" + "hash": "b46a0fbebdc8e5e9852a06444b0aeaa4eaf67959e68b69eb2f0896ebe9244691" } diff --git a/backend/.sqlx/query-cccdcb7fe7968eadfc04d8957a8e98b2f2d92a6d7f687a9dd5a70edb3d5a63e6.json b/backend/.sqlx/query-b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d.json similarity index 64% rename from backend/.sqlx/query-cccdcb7fe7968eadfc04d8957a8e98b2f2d92a6d7f687a9dd5a70edb3d5a63e6.json rename to backend/.sqlx/query-b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d.json index 94f591b3da..e6ecb2b7d6 100644 --- a/backend/.sqlx/query-cccdcb7fe7968eadfc04d8957a8e98b2f2d92a6d7f687a9dd5a70edb3d5a63e6.json +++ b/backend/.sqlx/query-b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)", + "query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)", "describe": { "columns": [], "parameters": { @@ -97,10 +97,11 @@ "Bool", "Bool", "JsonbArray", - "TextArray" + "TextArray", + "Uuid" ] }, "nullable": [] }, - "hash": "cccdcb7fe7968eadfc04d8957a8e98b2f2d92a6d7f687a9dd5a70edb3d5a63e6" + "hash": "b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d" } diff --git a/backend/.sqlx/query-c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6.json b/backend/.sqlx/query-c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6.json new file mode 100644 index 0000000000..c03a00eec3 --- /dev/null +++ b/backend/.sqlx/query-c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_token_blacklist (token, expires_at, blacklisted_by) \n VALUES ($1, $2, $3) \n ON CONFLICT (token) DO UPDATE SET \n expires_at = EXCLUDED.expires_at,\n blacklisted_at = NOW(),\n blacklisted_by = EXCLUDED.blacklisted_by", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Timestamp", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6" +} diff --git a/backend/.sqlx/query-d56722c25877222af9affd5da5bb83b28fa8cbb528a2cfc90684cb10a69e4375.json b/backend/.sqlx/query-d56722c25877222af9affd5da5bb83b28fa8cbb528a2cfc90684cb10a69e4375.json new file mode 100644 index 0000000000..a8256a8060 --- /dev/null +++ b/backend/.sqlx/query-d56722c25877222af9affd5da5bb83b28fa8cbb528a2cfc90684cb10a69e4375.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n WHERE expires_at > $1 \n ORDER BY blacklisted_at DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "expires_at", + "type_info": "Timestamp" + }, + { + "ordinal": 2, + "name": "blacklisted_at", + "type_info": "Timestamp" + }, + { + "ordinal": 3, + "name": "blacklisted_by", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Timestamp" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "d56722c25877222af9affd5da5bb83b28fa8cbb528a2cfc90684cb10a69e4375" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 73a66bb231..3c2d605615 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -4905,12 +4905,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6435,7 +6435,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] @@ -7432,9 +7432,9 @@ dependencies = [ [[package]] name = "lz4_flex" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c592ad9fbc1b7838633b3ae55ce69b17d01150c72fcef229fbb819d39ee51ee" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" dependencies = [ "twox-hash 2.1.1", ] @@ -9822,9 +9822,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases 0.2.1", "libc", @@ -10275,7 +10275,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] @@ -10410,7 +10410,7 @@ dependencies = [ [[package]] name = "rmcp" version = "0.1.5" -source = "git+https://github.com/modelcontextprotocol/rust-sdk#8a577659f01917289064c0a3c753392b44305ef4" +source = "git+https://github.com/modelcontextprotocol/rust-sdk#a62c6d1db8c8daf271aabaf49627de5f3a30613d" 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#8a577659f01917289064c0a3c753392b44305ef4" +source = "git+https://github.com/modelcontextprotocol/rust-sdk#a62c6d1db8c8daf271aabaf49627de5f3a30613d" dependencies = [ "proc-macro2", "quote", @@ -14377,14 +14377,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.0", + "webpki-root-certs 1.0.1", ] [[package]] name = "webpki-root-certs" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" +checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e" dependencies = [ "rustls-pki-types", ] @@ -14395,14 +14395,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" dependencies = [ "rustls-pki-types", ] @@ -14564,7 +14564,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "axum", @@ -14616,7 +14616,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "argon2", @@ -14727,7 +14727,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.499.0" +version = "1.500.2" dependencies = [ "base64 0.22.1", "chrono", @@ -14742,7 +14742,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.499.0" +version = "1.500.2" dependencies = [ "chrono", "serde", @@ -14755,7 +14755,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "serde", @@ -14769,7 +14769,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "async-stream", @@ -14846,7 +14846,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.499.0" +version = "1.500.2" dependencies = [ "regex", "serde", @@ -14860,7 +14860,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "bytes", @@ -14883,7 +14883,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.499.0" +version = "1.500.2" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -14895,7 +14895,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.499.0" +version = "1.500.2" dependencies = [ "convert_case 0.6.0", "serde", @@ -14904,7 +14904,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "lazy_static", @@ -14916,7 +14916,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "serde_json", @@ -14928,7 +14928,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "gosyn", @@ -14940,7 +14940,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "lazy_static", @@ -14952,7 +14952,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "serde_json", @@ -14964,7 +14964,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "nu-parser", @@ -14975,7 +14975,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14986,7 +14986,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14997,7 +14997,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "async-recursion", @@ -15020,7 +15020,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15037,7 +15037,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "lazy_static", @@ -15049,7 +15049,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "lazy_static", @@ -15067,7 +15067,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "getrandom 0.2.16", @@ -15091,7 +15091,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "serde_json", @@ -15101,7 +15101,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "async-recursion", @@ -15134,7 +15134,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.499.0" +version = "1.500.2" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15144,7 +15144,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.499.0" +version = "1.500.2" dependencies = [ "anyhow", "async-recursion", @@ -15473,6 +15473,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 506f360852..13414a87ec 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.499.0" +version = "1.500.2" authors.workspace = true edition.workspace = true @@ -32,7 +32,7 @@ members = [ ] [workspace.package] -version = "1.499.0" +version = "1.500.2" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index c41787fda1..425a17dc1a 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -67e727c618cf673850a0887931c803241abfcfe8 \ No newline at end of file +835a91c7c31ea749759cd8af0922ad837049ea2a \ No newline at end of file diff --git a/backend/migrations/20250618164000_add_agent_token_blacklist.down.sql b/backend/migrations/20250618164000_add_agent_token_blacklist.down.sql new file mode 100644 index 0000000000..aad208817c --- /dev/null +++ b/backend/migrations/20250618164000_add_agent_token_blacklist.down.sql @@ -0,0 +1,2 @@ +-- Remove agent token blacklist table +DROP TABLE IF EXISTS agent_token_blacklist; \ No newline at end of file diff --git a/backend/migrations/20250618164000_add_agent_token_blacklist.up.sql b/backend/migrations/20250618164000_add_agent_token_blacklist.up.sql new file mode 100644 index 0000000000..aefff3913d --- /dev/null +++ b/backend/migrations/20250618164000_add_agent_token_blacklist.up.sql @@ -0,0 +1,14 @@ +-- Add agent token blacklist table +CREATE TABLE agent_token_blacklist ( + token VARCHAR PRIMARY KEY, + expires_at TIMESTAMP NOT NULL, + blacklisted_at TIMESTAMP NOT NULL DEFAULT NOW(), + blacklisted_by VARCHAR NOT NULL +); + +-- Add index for efficient expiry cleanup +CREATE INDEX idx_agent_token_blacklist_expires_at ON agent_token_blacklist(expires_at); + +-- Grant permissions to windmill users +GRANT ALL ON agent_token_blacklist TO windmill_user; +GRANT ALL ON agent_token_blacklist TO windmill_admin; \ No newline at end of file diff --git a/backend/parsers/windmill-parser-py-imports/src/lib.rs b/backend/parsers/windmill-parser-py-imports/src/lib.rs index 92f4346115..97ac1c848f 100644 --- a/backend/parsers/windmill-parser-py-imports/src/lib.rs +++ b/backend/parsers/windmill-parser-py-imports/src/lib.rs @@ -30,8 +30,6 @@ use windmill_common::{ worker::PythonAnnotations, }; -const DEF_MAIN: &str = "def main("; - fn replace_import(x: String) -> String { SHORT_IMPORTS_MAP .get(&x) @@ -48,6 +46,9 @@ lazy_static! { static ref RE: Regex = Regex::new(r"^\#\s?(\S+)\s*$").unwrap(); static ref PIN_RE: Regex = Regex::new(r"(?:\s*#\s*(pin|repin):\s*)(\S*)").unwrap(); static ref PKG_RE: Regex = Regex::new(r"^([^!=<>]+)(?:[!=<>]|$)").unwrap(); + // Regex to properly match main function definition at line start, + // capturing both sync and async variants + static ref DEF_MAIN_RE: Regex = Regex::new(r"(?m)^(async\s+)?def\s+main\s*\(").unwrap(); } fn process_import(module: Option, path: &str, level: usize) -> Vec { @@ -143,7 +144,12 @@ struct ImportPin { } fn parse_code_for_imports(code: &str, path: &str) -> error::Result> { - let mut code = code.split(DEF_MAIN).next().unwrap_or("").to_string(); + // Use regex to safely find the main function definition + let mut code = DEF_MAIN_RE + .split(code) + .next() + .unwrap_or_default() + .to_string(); // remove main function decorator from end of file if it exists if code @@ -160,10 +166,17 @@ fn parse_code_for_imports(code: &str, path: &str) -> error::Result> + "\n"; } - let ast = Suite::parse(&code, "main.py").map_err(|e| { + // Add a fake main function to ensure the parser can process the code correctly + // This is needed because we've split off the real main function above + let code_with_fake_main = format!("{}\n\ndef main(): pass", code); + + let ast = Suite::parse(&code_with_fake_main, "main.py").map_err(|e| { error::Error::ExecutionErr(format!("Error parsing code for imports: {}", e.to_string())) })?; + // Note: We're still using the original code for finding pins, + // as the TextRange values from the parsed AST would be based on code_with_fake_main + // but we want to match against the original code let find_pin = |range: TextRange, key: String| { let hs = code .chars() @@ -378,7 +391,7 @@ async fn parse_python_imports_inner( }) .join("\n") .parse::() - .map_err(to_anyhow)?; + .map_err(to_anyhow)?; { if let Some(v) = metadata.get("requires-python").and_then(|v| v.as_str()) { diff --git a/backend/parsers/windmill-parser-py/src/lib.rs b/backend/parsers/windmill-parser-py/src/lib.rs index dbac8498ec..22d2ea6516 100644 --- a/backend/parsers/windmill-parser-py/src/lib.rs +++ b/backend/parsers/windmill-parser-py/src/lib.rs @@ -232,7 +232,14 @@ fn parse_typ(id: &str) -> Typ { x @ _ if x.starts_with("DynSelect_") => { Typ::DynSelect(x.strip_prefix("DynSelect_").unwrap().to_string()) } - _ => Typ::Resource(id.to_string()), + _ => Typ::Resource(map_resource_name(id)), + } +} + +fn map_resource_name(x: &str) -> String { + match x { + "S3Object" => "s3_object".to_string(), + _ => x.to_string(), } } @@ -468,7 +475,7 @@ def main(test1: str, Arg { otyp: None, name: "s3o".to_string(), - typ: Typ::Resource("S3Object".to_string()), + typ: Typ::Resource("s3_object".to_string()), default: None, has_default: false, oidx: None diff --git a/backend/parsers/windmill-parser/src/lib.rs b/backend/parsers/windmill-parser/src/lib.rs index 617b906154..0cb662c91b 100644 --- a/backend/parsers/windmill-parser/src/lib.rs +++ b/backend/parsers/windmill-parser/src/lib.rs @@ -92,6 +92,12 @@ mod test { fn test_snake_case() { assert_eq!("s3", to_snake_case("S3")); assert_eq!("s3", to_snake_case("s3")); + assert_eq!("s3_object", to_snake_case("S3Object")); + assert_eq!("s3_object", to_snake_case("S3object")); + assert_eq!("s3_object", to_snake_case("s3object")); + assert_eq!("abc", to_snake_case("ABC")); + assert_eq!("aa_bc", to_snake_case("AaBC")); + assert_eq!("a_b_c", to_snake_case("A_B_C")); assert_eq!("s_3", to_snake_case("S_3")); assert_eq!("type_name_here", to_snake_case("typeNameHere")); } diff --git a/backend/src/main.rs b/backend/src/main.rs index be9a7578a3..aa3eee4a18 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1102,6 +1102,9 @@ Windmill Community Edition {GIT_VERSION} _ = tokio::time::sleep(Duration::from_secs(12 * 60 * 60)) => { tracing::info!("Reloading config after 12 hours"); initial_load(&conn, tx.clone(), worker_mode, server_mode, #[cfg(feature = "parquet")] disable_s3_store).await; + if let Err(e) = reload_license_key(&conn).await { + tracing::error!("Failed to reload license key on agent: {e:#}"); + } #[cfg(feature = "enterprise")] ee_oss::verify_license_key().await; } diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 5c4677018c..4b4946e3e4 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -840,6 +840,24 @@ pub async fn delete_expired_items(db: &DB) -> () { tracing::error!("Error deleting audit log on CE: {:?}", e); } + match sqlx::query_scalar!( + "DELETE FROM agent_token_blacklist WHERE expires_at <= now() RETURNING token", + ) + .fetch_all(db) + .await + { + Ok(deleted_tokens) => { + if deleted_tokens.len() > 0 { + tracing::info!( + "deleted {} expired blacklisted agent tokens: {:?}", + deleted_tokens.len(), + deleted_tokens + ); + } + } + Err(e) => tracing::error!("Error deleting expired blacklisted agent tokens: {:?}", e), + } + let job_retention_secs = *JOB_RETENTION_SECS.read().await; if job_retention_secs > 0 { match db.begin().await { diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6922c98379..d1bb65d2ff 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.499.0 + version: 1.500.2 title: Windmill API contact: @@ -11240,6 +11240,98 @@ paths: schema: type: string + /agent_workers/blacklist_token: + post: + summary: blacklist agent token (requires super admin) + operationId: blacklistAgentToken + tags: + - agent_workers + requestBody: + description: token to blacklist + required: true + content: + application/json: + schema: + type: object + properties: + token: + type: string + description: The agent token to blacklist + expires_at: + type: string + format: date-time + description: Optional expiration date for the blacklist entry + required: + - token + responses: + "200": + description: token blacklisted successfully + + /agent_workers/remove_blacklist_token: + post: + summary: remove agent token from blacklist (requires super admin) + operationId: removeBlacklistAgentToken + tags: + - agent_workers + requestBody: + description: token to remove from blacklist + required: true + content: + application/json: + schema: + type: object + properties: + token: + type: string + description: The agent token to remove from blacklist + required: + - token + responses: + "200": + description: token removed from blacklist successfully + + /agent_workers/list_blacklisted_tokens: + get: + summary: list blacklisted agent tokens (requires super admin) + operationId: listBlacklistedAgentTokens + tags: + - agent_workers + parameters: + - name: include_expired + in: query + description: Whether to include expired blacklisted tokens + schema: + type: boolean + default: false + responses: + "200": + description: list of blacklisted tokens + content: + application/json: + schema: + type: array + items: + type: object + properties: + token: + type: string + description: The blacklisted token (without prefix) + expires_at: + type: string + format: date-time + description: When the blacklist entry expires + blacklisted_at: + type: string + format: date-time + description: When the token was blacklisted + blacklisted_by: + type: string + description: Email of the user who blacklisted the token + required: + - token + - expires_at + - blacklisted_at + - blacklisted_by /w/{workspace}/acls/get/{kind}/{path}: get: diff --git a/backend/windmill-api/src/agent_workers_oss.rs b/backend/windmill-api/src/agent_workers_oss.rs index 655058c493..dabb2cb079 100644 --- a/backend/windmill-api/src/agent_workers_oss.rs +++ b/backend/windmill-api/src/agent_workers_oss.rs @@ -16,9 +16,6 @@ use crate::db::DB; #[cfg(not(feature = "private"))] use axum::Router; -#[cfg(not(feature = "private"))] -use serde::{Deserialize, Serialize}; - #[cfg(not(feature = "private"))] pub fn global_service() -> Router { Router::new() @@ -44,15 +41,6 @@ pub fn workspaced_service( (router, vec![], Some(job_completed_tx)) } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg(not(feature = "private"))] -pub struct AgentAuth { - pub worker_group: String, - pub suffix: Option, - pub tags: Vec, - pub exp: Option, -} - #[cfg(not(feature = "private"))] pub struct AgentCache {} diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index befe569229..6e127ebc60 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -2011,7 +2011,7 @@ async fn upload_s3_file_from_app( if !has_unnamed_policy { return Err(Error::BadRequest( - "no policy found for unnamed s3 file uplooad".to_string(), + "no policy found for unnamed s3 file upload".to_string(), )); } diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 8b0b5e5e76..9ee8b09fff 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -3564,7 +3564,7 @@ pub async fn run_flow_by_path_inner( scheduled_for, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, @@ -3657,7 +3657,7 @@ pub async fn restart_flow( scheduled_for, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, @@ -3749,7 +3749,7 @@ pub async fn run_script_by_path_inner( scheduled_for, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, @@ -4417,7 +4417,7 @@ pub async fn run_wait_result_job_by_path_get( None, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, @@ -4557,7 +4557,7 @@ pub async fn run_wait_result_script_by_path_internal( None, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, @@ -4670,7 +4670,7 @@ pub async fn run_wait_result_script_by_hash( None, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, @@ -4784,7 +4784,7 @@ pub async fn run_wait_result_flow_by_path_internal( scheduled_for, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, @@ -5625,7 +5625,7 @@ pub async fn run_job_by_hash_inner( scheduled_for, None, run_query.parent_job, - run_query.root_job.or(run_query.parent_job), + run_query.root_job, run_query.job_id, false, false, diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 14fdfef536..19fd422805 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -583,7 +583,6 @@ pub async fn transform_json_value<'c>( job.schedule_path.clone(), job.flow_step_id.clone(), job.root_job.map(|x| x.to_string()), - None, Some(job.scheduled_for.clone()), ) .await; diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 4b6601e1e6..c1192feb3b 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -74,8 +74,7 @@ async fn list_contextual_variables( Some("u/user/triggering_flow_path".to_string()), Some("c".to_string()), Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()), - Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c".to_string()), - Some(chrono::offset::Utc::now()) + Some(chrono::offset::Utc::now()), ) .await .to_vec(), diff --git a/backend/windmill-common/src/variables.rs b/backend/windmill-common/src/variables.rs index 3fd2e0513b..1dfa1438d8 100644 --- a/backend/windmill-common/src/variables.rs +++ b/backend/windmill-common/src/variables.rs @@ -180,7 +180,6 @@ pub async fn get_reserved_variables( schedule_path: Option, step_id: Option, root_flow_id: Option, - jwt_token: Option, scheduled_for: Option>, ) -> Vec { let state_path = { @@ -334,12 +333,6 @@ pub async fn get_reserved_variables( description: "Script or flow step execution unique path, useful for storing results in an external service".to_string(), is_custom: false, }, - ContextualVariable { - name: "WM_OIDC_JWT".to_string(), - value: jwt_token.unwrap_or_else(|| "".to_string()), - description: "OIDC JWT token (EE only)".to_string(), - is_custom: false, - }, ContextualVariable { name: "WM_WORKER_GROUP".to_string(), value: WORKER_GROUP.clone(), diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 3e89cd98b2..fe2e08d0a1 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -2721,10 +2721,9 @@ pub async fn get_result_by_id( "SELECT id As \"id!\", flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json\" - FROM v2_as_queue - WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id AND workspace_id = $2", - flow_id, - &w_id + FROM v2_job_status + WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id", + flow_id ) .fetch_optional(&db) .await?; @@ -2863,9 +2862,10 @@ pub async fn get_result_by_id_from_running_flow_inner( node_id: &str, ) -> error::Result { let flow_job_result = sqlx::query!( - "SELECT leaf_jobs->$1::text AS \"leaf_jobs: Json>\", parent_job - FROM v2_as_queue - WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = id AND workspace_id = $3", + "SELECT flow_leaf_jobs->$1::text AS \"leaf_jobs: Json>\", v2_job.parent_job + FROM v2_job_status + LEFT JOIN v2_job ON v2_job.id = v2_job_status.id AND v2_job.workspace_id = $3 + WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = v2_job_status.id", node_id, flow_id, w_id, @@ -2873,11 +2873,13 @@ pub async fn get_result_by_id_from_running_flow_inner( .fetch_optional(db) .await?; + // tracing::error!("flow_job_result: {:?} {:?}", flow_job_result, flow_id); let flow_job_result = windmill_common::utils::not_found_if_none( flow_job_result, "Root job of parent runnnig flow", format!("parent: {}, id: {}", flow_id, node_id), )?; + // tracing::error!("flow_job_result: {:?}, {:?}", flow_job_result.leaf_jobs, flow_job_result.parent_job); let job_result = flow_job_result .leaf_jobs @@ -4230,10 +4232,10 @@ pub async fn push<'c, 'd>( INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job, created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger, script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner, - flow_innermost_root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, + flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, - $19, $20, $21, $22, $23, $24, $25, $26, + $19, $20, $38, $21, $22, $23, $24, $25, $26, CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END, ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27) ), @@ -4289,6 +4291,7 @@ pub async fn push<'c, 'd>( job_authed.is_operator, folders.as_slice(), job_authed.groups.as_slice(), + root_job.or(parent_job) ) .execute(&mut *tx) .warn_after_seconds(1) diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index fa64832ee4..cbe389403b 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -1566,7 +1566,6 @@ pub async fn start_worker( None, None, None, - None, ) .await; let context_envs = build_envs_map(context.to_vec()).await; diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index b419626845..3b5312c14c 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -434,7 +434,6 @@ pub async fn get_reserved_variables( job.schedule_path(), job.flow_step_id.clone(), job.flow_innermost_root_job.clone().map(|x| x.to_string()), - None, Some(job.scheduled_for.clone()), ) .await diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs index 2215c94d8d..e9db387b03 100644 --- a/backend/windmill-worker/src/deno_executor.rs +++ b/backend/windmill-worker/src/deno_executor.rs @@ -11,8 +11,7 @@ use crate::{ start_child_process, OccupancyMetrics, }, handle_child::handle_child, - DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, HOME_ENV, NPM_CONFIG_REGISTRY, - PATH_ENV, TZ_ENV, + DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, HOME_ENV, NPM_CONFIG_REGISTRY, PATH_ENV, TZ_ENV, }; use windmill_common::client::AuthedClient; @@ -534,7 +533,6 @@ pub async fn start_worker( None, None, None, - None, ) .await; let context_envs = build_envs_map(context.to_vec()).await; diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 90ef0afccd..90be4e9cfc 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -2077,7 +2077,6 @@ pub async fn start_worker( None, None, None, - None, ) .await .to_vec(); @@ -2197,7 +2196,6 @@ for line in sys.stdin: None, None, None, - None, ) .await; diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 56ac7c00e0..058ad19598 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -858,13 +858,14 @@ pub fn start_interactive_worker_shell( .await; } _ => { - tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE)).await; + tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 10)).await; } } } Err(err) => { tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err); + tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 20)).await; } }; } @@ -1861,6 +1862,7 @@ pub async fn run_worker( } Err(err) => { tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err); + tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 5)).await; } }; } diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index a988908588..1381d15d2c 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.499.0"; +export const VERSION = "v1.500.2"; 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 f0ad23f8cf..557b1a18c6 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -63,7 +63,7 @@ export { // } // }); -export const VERSION = "1.499.0"; +export const VERSION = "1.500.2"; const command = new Command() .name("wmill") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ce57c0026d..f8f110cdb1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.499.0", + "version": "1.500.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.499.0", + "version": "1.500.2", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -77,7 +77,7 @@ "windmill-parser-wasm-java": "^1.478.1", "windmill-parser-wasm-nu": "^1.474.1", "windmill-parser-wasm-php": "^1.429.0", - "windmill-parser-wasm-py": "^1.477.1", + "windmill-parser-wasm-py": "^1.499.0", "windmill-parser-wasm-regex": "^1.492.1", "windmill-parser-wasm-rust": "^1.429.0", "windmill-parser-wasm-ts": "^1.486.1", @@ -12952,9 +12952,9 @@ "integrity": "sha512-SGJAtNpfdRZftkGboxWsm/yQDnJBJodwPQUbX2cWk/aoNook6ULesZwsYtBC9WN1VH6TIskLiVPohMmu6jtXmw==" }, "node_modules/windmill-parser-wasm-py": { - "version": "1.477.1", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.477.1.tgz", - "integrity": "sha512-EY3mSMWpqFPzd7fsLg2/hSfQFU8HpW9nplFwm4JHHCDbcTpBzlvzjPJoHAAGO5kMzowAxjqi5ai/mXjeUWuiSg==" + "version": "1.499.0", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.499.0.tgz", + "integrity": "sha512-ur5SU+YTuYm1Hgo+SWohYRsmkVxIZPME7GzzRivPhkK5XBQcc7PsxlMjYkIvFJwufz+jX2t6p0aYNIdDXpMNbA==" }, "node_modules/windmill-parser-wasm-regex": { "version": "1.492.1", diff --git a/frontend/package.json b/frontend/package.json index cab3c1d9a6..bb6e71d536 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.499.0", + "version": "1.500.2", "scripts": { "dev": "vite dev", "build": "vite build", @@ -144,7 +144,7 @@ "windmill-parser-wasm-java": "^1.478.1", "windmill-parser-wasm-nu": "^1.474.1", "windmill-parser-wasm-php": "^1.429.0", - "windmill-parser-wasm-py": "^1.477.1", + "windmill-parser-wasm-py": "^1.499.0", "windmill-parser-wasm-regex": "^1.492.1", "windmill-parser-wasm-rust": "^1.429.0", "windmill-parser-wasm-ts": "^1.486.1", diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index f226430d83..0ee118010f 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -48,6 +48,7 @@ import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker' import type { PickableProperties } from './flows/previousResults' import { Triggers } from './triggers/triggers.svelte' + import { TestSteps } from './flows/testSteps.svelte' let flowCopilotContext: FlowCopilotContext = { shouldUpdatePropertyType: writable<{ @@ -436,7 +437,7 @@ const moving = writable<{ id: string } | undefined>(undefined) const history = initHistory(flowStore.val) - const testStepStore = writable>({}) + const testSteps = new TestSteps() const selectedIdStore = writable('settings-metadata') const triggersCount = writable(undefined) @@ -455,7 +456,7 @@ pathStore: writable(''), flowStateStore, flowStore, - testStepStore, + testSteps, saveDraft: () => {}, initialPathStore: writable(''), fakeInitialPath: '', @@ -715,7 +716,7 @@ noEditor on:applyArgs={(ev) => { if (ev.detail.kind === 'preprocessor') { - $testStepStore['preprocessor'] = ev.detail.args ?? {} + testSteps.setStepArgs('preprocessor', ev.detail.args ?? {}) $selectedIdStore = 'preprocessor' } else { previewArgsStore.val = ev.detail.args ?? {} diff --git a/frontend/src/lib/components/DynSelect.svelte b/frontend/src/lib/components/DynSelect.svelte index 0241ab90f5..93a3bf0ed3 100644 --- a/frontend/src/lib/components/DynSelect.svelte +++ b/frontend/src/lib/components/DynSelect.svelte @@ -1,26 +1,4 @@ - + + {#if helperScript} -
-
- {#key renderCount} - { - error = e.detail.details - }} - on:change={(e) => { - value = e.detail.value - }} - {value} - computeOnClick={value == undefined} - loadOptions={getItemsFromOptions} - inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles} - containerStyles={darkMode - ? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark - : SELECT_INPUT_DEFAULT_STYLE.containerStyles} - /> - {/key} -
- {#if error} -
error: {JSON.stringify(error)}
+ - -
- {#if !$enterpriseLicense} -
- Agent workers are only available in the enterprise edition. For evaluation purposes, you can - only use the tag `agent_test` tag and it is limited to 100 jobs. -
- {/if} - -
+ + Create + Blacklist + {#snippet content()} +
+ {#if selectedTab === 'create'} + Use HTTP agent workers only when the workers need to be deployed remotely OR with only + HTTP connectivity OR in untrusted environments. HTTP agent workers have more latency and + less capabilities than normal workers. +
+
+ +
+
+ {#if !$enterpriseLicense} +
+ Agent workers are only available in the enterprise edition. For evaluation purposes, + you can only use the tag `agent_test` tag and it is limited to 100 jobs. +
+ {/if} + +
-
- {#if !$enterpriseLicense} -
- Agent workers are only available in the enterprise edition. For evaluation purposes, you can - only use the tag `agent_test` tag and it is limited to 100 jobs. -
- {/if} -
- - { - if (token) { - navigator.clipboard.writeText(token) - sendUserToast('Copied to clipboard') - } - }} - placeholder="Select tags to generate a JWT token" - type="text" - disabled - value={token} - class="w-full pr-10 pl-3 py-2 text-sm text-gray-600 bg-gray-50 border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition truncate" - /> +
+ {#if !$enterpriseLicense} +
+ Agent workers are only available in the enterprise edition. For evaluation purposes, + you can only use the tag `agent_test` tag and it is limited to 100 jobs. +
+ {/if} +
+ { + e.preventDefault() + e.stopPropagation() + if (token) { + navigator.clipboard.writeText(token) + sendUserToast('Copied to clipboard') + } + }} + placeholder="Select tags to generate a JWT token" + type="text" + disabled + value={token} + class="w-full pr-10 pl-3 py-2 text-sm text-gray-600 bg-gray-50 border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition truncatere" + /> - - -
+ +
-
- Set the following environment variables: -
    -
  • MODE=agent
  • -
  • AGENT_TOKEN=<token>
  • -
  • BASE_INTERNAL_URL=<base url>
  • -
-

- to a worker to have it act as an HTTP agent worker. - INIT_SCRIPT, if needed, must be passed as an env variable. -

- - Ensure at least one normal worker is running and listening to the tags - flow and dependency - (or flow-<workspace> and dependency-<workspace> if - using workspace-specific default tags), because agent workers - cannot run dependency jobs - nor execute the - flow state machine. They can, however, run subjobs within flows. - - -
- Use the following API endpoint with a superadmin bearer token: - POST /api/agent_workers/create_agent_token -
+						
+ Set the following environment variables: +
    +
  • MODE=agent
  • +
  • AGENT_TOKEN=<token>
  • +
  • BASE_INTERNAL_URL=<base url>
  • +
+

+ to a worker to have it act as an HTTP agent worker. + INIT_SCRIPT, if needed, must be passed as an env variable. +

+ + Ensure at least one normal worker is running and listening to the tags + flow and dependency + (or flow-<workspace> and + dependency-<workspace> + if using workspace-specific default tags), because agent workers + cannot run dependency jobs + nor execute the + flow state machine. They can, however, run subjobs within flows. + + +
+ Use the following API endpoint with a superadmin bearer token: + POST /api/agent_workers/create_agent_token +
 	{`
+											>{`
 	  "worker_group": "agent",
 	  "tags": ["tag1", "tag2"],
 	  "exp": 1717334400
 	`}
-					
- The JSON response will contain the generated JWT token. + > +
+ The JSON response will contain the generated JWT token. +
+
+
+
- + {:else if selectedTab === 'blacklist'} +
+
+ {#if !$enterpriseLicense} +
+ Token blacklist management is only available in the enterprise edition. +
+ {:else} +
+ Add tokens to the blacklist to prevent them from being used by agent workers. + Blacklisted tokens may take up to 5 minutes to be effective because of caching. +
+ +
+
+ + +
+
+ +
+ + {#if !$superadmin} +
+ Only superadmins can manage the token blacklist. +
+ {/if} +
+ + +
+
+

Blacklisted Tokens

+ +
+ + {#if isLoadingBlacklist} +
Loading blacklisted tokens...
+ {:else if blacklistedTokens?.length === 0} +
+ No tokens are currently blacklisted. +
+ {:else} +
+ {#each blacklistedTokens ?? [] as blacklistedToken} +
+
+
+ {blacklistedToken.token} +
+ {#if blacklistedToken.expires_at} +
+ Expires: {new Date(blacklistedToken.expires_at).toLocaleString()} +
+ {/if} +
+ {#if $superadmin} + + {/if} +
+ {/each} +
+ {/if} +
+ {/if} +
+
+ {/if}
- -
+ {/snippet} + diff --git a/frontend/src/lib/components/ModulePreview.svelte b/frontend/src/lib/components/ModulePreview.svelte index ffa1530fcf..93ea296ec6 100644 --- a/frontend/src/lib/components/ModulePreview.svelte +++ b/frontend/src/lib/components/ModulePreview.svelte @@ -1,22 +1,13 @@ - jobDone()} +
@@ -130,7 +57,7 @@
{#if testIsLoading} - @@ -139,7 +66,7 @@ color="dark" btnClasses="truncate" size="sm" - on:click={() => runTest(stepArgs)} + on:click={runTestWithStepArgs} shortCut={{ Icon: CornerDownLeft }} @@ -149,5 +76,5 @@ {/if}
- +
diff --git a/frontend/src/lib/components/ModulePreviewForm.svelte b/frontend/src/lib/components/ModulePreviewForm.svelte index 1825d82252..926c5ccff3 100644 --- a/frontend/src/lib/components/ModulePreviewForm.svelte +++ b/frontend/src/lib/components/ModulePreviewForm.svelte @@ -5,77 +5,91 @@ import { RefreshCw } from 'lucide-svelte' import ArgInput from './ArgInput.svelte' import { Button } from './common' - import { getContext, untrack } from 'svelte' + import { getContext, onMount, untrack } from 'svelte' import type { FlowEditorContext } from './flows/types' import { evalValue } from './flows/utils' import type { FlowModule } from '$lib/gen' import type { PickableProperties } from './flows/previousResults' import type SimpleEditor from './SimpleEditor.svelte' import { getResourceTypes } from './resourceTypesStore' + import { twMerge } from 'tailwind-merge' interface Props { schema: Schema | { properties?: Record; required?: string[] } - args?: Record mod: FlowModule pickableProperties: PickableProperties | undefined isValid?: boolean autofocus?: boolean + focusArg?: string } let { schema, - args = $bindable({}), mod, pickableProperties, isValid = $bindable(true), - autofocus = false + autofocus = false, + focusArg = undefined }: Props = $props() - const { testStepStore } = getContext('FlowEditorContext') + const { testSteps, flowStateStore, flowStore, previewArgs } = + getContext('FlowEditorContext') let inputCheck: { [id: string]: boolean } = $state({}) $effect(() => { isValid = allTrue(inputCheck) ?? false }) - $effect(() => { - if (args == undefined || typeof args !== 'object') { - args = {} - } - }) - - function removeExtraKey() { - const nargs = {} - Object.keys(args ?? {}).forEach((key) => { - if (keys.includes(key)) { - nargs[key] = args[key] - } - }) - args = nargs - } - let keys: string[] = $state([]) $effect(() => { let lkeys = Object.keys(schema?.properties ?? {}) if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) { keys = lkeys - untrack(() => removeExtraKey()) + untrack(() => testSteps?.removeExtraKey(mod.id, keys)) } }) function plugIt(argName: string) { - args[argName] = structuredClone( - $state.snapshot(evalValue(argName, mod, testStepStore, pickableProperties, true)) + testSteps?.setEvaluatedStepArg( + mod.id, + argName, + $state.snapshot(evalValue(argName, mod, pickableProperties, true)) ) - try { - editor?.[argName]?.setCode(JSON.stringify(args[argName], null, 4)) - } catch { - //ignore - } } let editor: Record = $state({}) + // Animation and highlighting for focusArg + let animateArg: string | undefined = $state(undefined) + $effect(() => { + if (focusArg) { + // Add a slight delay to ensure the form is rendered + setTimeout(() => { + const argElement = document.querySelector(`[data-arg="${focusArg}"]`) + if (argElement) { + // Add highlight animation + animateArg = focusArg + argElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) + + // Focus the input if it exists + const input = argElement.querySelector('input, textarea, select') as + | HTMLInputElement + | HTMLTextAreaElement + | HTMLSelectElement + | null + if (input) { + input.focus() + } + + // Remove highlight after animation + setTimeout(() => { + animateArg = undefined + }, 2000) + } + }, 200) + } + }) + let resourceTypes: string[] | undefined = $state(undefined) async function loadResourceTypes() { @@ -83,21 +97,34 @@ } loadResourceTypes() + + let args = $state(>{}) + + onMount(() => { + testSteps?.updateStepArgs(mod.id, $flowStateStore, flowStore?.val, previewArgs?.val) + args = testSteps?.getStepArgs(mod.id) ?? { value: {} } + }) -
+
{#if keys.length > 0} {#each keys as argName, i (argName)} {#if Object.keys(schema.properties ?? {}).includes(argName)} -
- {#if typeof args == 'object' && schema?.properties?.[argName]} +
+ {#if typeof args.value == 'object' && schema?.properties?.[argName]} {/if} -
- -
+ {#if testSteps?.isArgManuallySet(mod.id, argName)} +
+ +
+ {/if}
{/if} {/each} diff --git a/frontend/src/lib/components/ModulePreviewResultViewer.svelte b/frontend/src/lib/components/ModulePreviewResultViewer.svelte index ae9556c177..a75a149a05 100644 --- a/frontend/src/lib/components/ModulePreviewResultViewer.svelte +++ b/frontend/src/lib/components/ModulePreviewResultViewer.svelte @@ -41,7 +41,7 @@ onUpdateMock }: Props = $props() - const { testStepStore } = getContext('FlowEditorContext') + const { testSteps } = getContext('FlowEditorContext') let selectedJob: Job | undefined = $state(undefined) let fetchingLastJob = false @@ -49,7 +49,7 @@ let jobProgressReset: () => void = $state(() => {}) let nlastJob = $derived.by(() => { - if (testJob) { + if (testJob && testJob.type === 'CompletedJob') { return { ...testJob, preview: true } } if (lastJob) { @@ -90,7 +90,7 @@ {disableHistory} > {#snippet copilot_fix()} - {#if lang && editor && diffEditor && $testStepStore[mod.id] && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)} + {#if lang && editor && diffEditor && testSteps.getStepArgs(mod.id) && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)} {/if} {/snippet} diff --git a/frontend/src/lib/components/ModuleTest.svelte b/frontend/src/lib/components/ModuleTest.svelte new file mode 100644 index 0000000000..705ef031ca --- /dev/null +++ b/frontend/src/lib/components/ModuleTest.svelte @@ -0,0 +1,137 @@ + + + + + jobDone()} + bind:scriptProgress + bind:this={testJobLoader} + bind:isLoading={ + () => testModulesState[mod.id]?.loading ?? false, + (v) => + (testModulesState[mod.id] = { + ...testModulesState[mod.id], + loading: v ?? false, + instances: testModulesState[mod.id]?.instances ?? 0 + }) + } + bind:job={testJob} +/> diff --git a/frontend/src/lib/components/apps/svelte-select/lib/SelectLegacy.svelte b/frontend/src/lib/components/apps/svelte-select/lib/SelectLegacy.svelte deleted file mode 100644 index 201a4ab7d7..0000000000 --- a/frontend/src/lib/components/apps/svelte-select/lib/SelectLegacy.svelte +++ /dev/null @@ -1,1043 +0,0 @@ - - - - - -
- {#if listOpen} - -
- {#if $$slots['list-prepend']}{/if} - {#if $$slots.list} - {:else if filteredItems.length > 0} - {#each filteredItems as item, i} -
handleHover(i)} - on:focus={() => handleHover(i)} - on:click|stopPropagation={() => handleItemClick({ item, i })} - on:keydown|preventDefault|stopPropagation - class="list-item" - tabindex="-1" - > -
- - {item ? truncate(item?.[label], 80) : ''} - -
-
- {/each} - {:else if !hideEmptyState} - -
No options
-
- {/if} - {#if $$slots['list-append']}{/if} -
-
- {/if} - - - {#if focused} - {ariaSelection} - - {ariaContext} - - {/if} - - -
- -
- -
- {#if hasValue} -
- - {value[label]} - -
- {/if} - - -
- -
- {#if loading} - - {/if} - - {#if showClear} - - {/if} - - {#if showChevron} - - {/if} -
- - - - - - {#if required && (!value || value.length === 0)} - - - - {/if} -
- - diff --git a/frontend/src/lib/components/apps/svelte-select/lib/index.js b/frontend/src/lib/components/apps/svelte-select/lib/index.js deleted file mode 100644 index 80d0787c5f..0000000000 --- a/frontend/src/lib/components/apps/svelte-select/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as default } from './SelectLegacy.svelte' diff --git a/frontend/src/lib/components/common/button/Button.svelte b/frontend/src/lib/components/common/button/Button.svelte index e342420233..f230d89798 100644 --- a/frontend/src/lib/components/common/button/Button.svelte +++ b/frontend/src/lib/components/common/button/Button.svelte @@ -49,6 +49,7 @@ portal?: string } | undefined = undefined + export let dropdownBtnClasses: string = '' type MenuItem = { label: string @@ -309,7 +310,8 @@ 'rounded-md m-0 p-0 center-center h-full', variant === 'border' ? 'border-0 border-r border-y ' : 'border-0', 'rounded-r-md !rounded-l-none', - size === 'xs2' ? '!w-8' : '!w-10' + size === 'xs2' ? '!w-8' : '!w-10', + dropdownBtnClasses )} > diff --git a/frontend/src/lib/components/copilot/chat/script/core.ts b/frontend/src/lib/components/copilot/chat/script/core.ts index d26210f7c7..23a5b31fd3 100644 --- a/frontend/src/lib/components/copilot/chat/script/core.ts +++ b/frontend/src/lib/components/copilot/chat/script/core.ts @@ -61,6 +61,76 @@ You should only use them if you need them to satisfy the user's instructions. Al const TS_INLINE_TYPE_INSTRUCTION = `When using resource types, you should use RT.ResourceType as parameter type. For other parameters, you should inline the objects types instead of defining them separately. This is because Windmill requires the types (other than resource types) to be inlined to generate a user friendly UI from the parameters.` +const TS_WINDMILL_CLIENT_CONTEXT = ` + +The windmill client (wmill) can be used to interact with Windmill from the script. Import it with \`import * as wmill from "windmill-client"\`. Key functions include: + +// Resource operations +wmill.getResource(path?: string): Promise // Get resource value by path +wmill.setResource(value: any, path?: string): Promise // Set resource value + +// State management (persistent across executions) +wmill.getState(): Promise // Get shared state +wmill.setState(state: any): Promise // Set shared state + +// Variables +wmill.getVariable(path: string): Promise // Get variable value +wmill.setVariable(path: string, value: string): Promise // Set variable value + +// Script execution +wmill.runScript(path: string, args?: Record): Promise // Run script synchronously +wmill.runScriptAsync(path: string, args?: Record): Promise // Run script async, returns job ID +wmill.waitJob(jobId: string): Promise // Wait for job completion and get result + +// S3 file operations (if S3 is configured) +wmill.loadS3File(s3object: S3Object): Promise // Load file content from S3 +wmill.writeS3File(s3object: S3Object, content: string | Blob): Promise // Write file to S3 + +// Flow operations +wmill.setFlowUserState(key: string, value: any): Promise // Set flow user state +wmill.getFlowUserState(key: string): Promise // Get flow user state +wmill.getResumeUrls(): Promise<{approvalPage: string, resume: string, cancel: string}> // Get approval URLs + +// Utilities +wmill.getWorkspace(): string // Get current workspace +wmill.databaseUrlFromResource(path: string): Promise // Get database URL from resource` + +const PYTHON_WINDMILL_CLIENT_CONTEXT = ` + +The windmill client (wmill) can be used to interact with Windmill from the script. Import it with \`import wmill\`. Key functions include: + +// Resource operations +wmill.get_resource(path: str) -> dict | None // Get resource value by path +wmill.set_resource(path: str, value: Any, resource_type: str = "any") -> None // Set resource value + +// State management (persistent across executions) +wmill.get_state() -> Any // Get shared state (deprecated, use flow user state) +wmill.set_state(value: Any) -> None // Set shared state +wmill.get_flow_user_state(key: str) -> Any // Get flow user state +wmill.set_flow_user_state(key: str, value: Any) -> None // Set flow user state + +// Variables +wmill.get_variable(path: str) -> str // Get variable value +wmill.set_variable(path: str, value: str, is_secret: bool = False) -> None // Set variable value + +// Script execution +wmill.run_script(path: str, args: dict = None, timeout = None) -> Any // Run script synchronously +wmill.run_script_async(path: str, args: dict = None, scheduled_in_secs: int = None) -> str // Run script async, returns job ID +wmill.wait_job(job_id: str, timeout = None) -> Any // Wait for job completion and get result + +// S3 file operations (if S3 is configured) +wmill.load_s3_file(s3object: S3Object, s3_resource_path: str = None) -> bytes // Load file content from S3 +wmill.write_s3_file(s3object: S3Object, file_content: bytes, s3_resource_path: str = None) -> S3Object // Write file to S3 + +// Flow operations +wmill.run_flow_async(path: str, args: dict = None) -> str // Run flow asynchronously +wmill.get_resume_urls(approver: str = None) -> dict // Get approval URLs for flow steps + +// Utilities +wmill.whoami() -> dict // Get current user information +wmill.get_job_status(job_id: str) -> str // Get job status ("RUNNING" | "WAITING" | "COMPLETED") +wmill.set_progress(value: int) -> None // Set job progress (0-100)` + const PYTHON_RESOURCE_TYPE_SYSTEM = `On Windmill, credentials and configuration are stored in resources and passed as parameters to main. If you need credentials, you should add a parameter to \`main\` with the corresponding resource type. You need to **redefine** the type of the resources that are needed before the main function as TypedDict, but only include them if they are actually needed to achieve the function purpose. @@ -108,7 +178,9 @@ export const SUPPORTED_CHAT_SCRIPT_LANGUAGES = [ 'snowflake', 'mssql', 'graphql', - 'powershell' + 'powershell', + 'csharp', + 'java' ] export function getLangContext( @@ -124,7 +196,9 @@ export function getLangContext( : TS_RESOURCE_TYPE_SYSTEM + (allowResourcesFetch ? `To query the RT namespace, you can use the \`search_resource_types\` tool.\n` - : '')) + TS_INLINE_TYPE_INSTRUCTION + : '')) + + TS_INLINE_TYPE_INSTRUCTION + + TS_WINDMILL_CLIENT_CONTEXT const mainFunctionName = isPreprocessor ? 'preprocessor' : 'main' @@ -152,7 +226,8 @@ export function getLangContext( (isPreprocessor ? PYTHON_PREPROCESSOR_INSTRUCTION : PYTHON_RESOURCE_TYPE_SYSTEM + - `${allowResourcesFetch ? `\nTo query the available resource types, you can use the \`search_resource_types\` tool.` : ''}`) + `${allowResourcesFetch ? `\nTo query the available resource types, you can use the \`search_resource_types\` tool.` : ''}`) + + PYTHON_WINDMILL_CLIENT_CONTEXT ) case 'php': return ( @@ -210,6 +285,10 @@ If you want to handle async functions (e.g., using tokio), you need to keep the return 'The user is coding in GraphQL. If needed, add the needed arguments as query parameters.' case 'powershell': return 'The user is coding in PowerShell. On Windmill, arguments can be obtained by calling the param function on the first line of the script like that: `param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)`' + case 'csharp': + return 'The user is coding in C#. On Windmill, it is expected the script contains a public static Main method inside a class. The class name is irrelevant. NuGet packages can be added using the format: #r "nuget: PackageName, Version" at the top of the script. The Main method signature should be: public static ReturnType Main(parameter types...)' + case 'java': + return 'The user is coding in Java. On Windmill, it is expected the script contains a Main public class and a public static main() method. The return type can be Object or void. Dependencies can be added using the format: //requirements://groupId:artifactId:version at the top of the script. The method signature should be: public static Object main(parameter types...)' default: return '' } diff --git a/frontend/src/lib/components/copilot/prompts/editPrompt.ts b/frontend/src/lib/components/copilot/prompts/editPrompt.ts index 0ffee29772..6aec87dd35 100644 --- a/frontend/src/lib/components/copilot/prompts/editPrompt.ts +++ b/frontend/src/lib/components/copilot/prompts/editPrompt.ts @@ -49,6 +49,12 @@ export const EDIT_PROMPT = { "frontend": { "prompt": "Here's my client-side javascript code: \n```javascript\n{code}\n```\n\nYou can access the context object with the ctx global variable. \nThe app state is a store that can be used to store data. You can access and update the state object with the state global variable like this: state.foo = 'bar'\nYou can use the goto function to navigate to a specific URL: goto(path: string, newTab?: boolean)\nYou can use the openModal and closeModal functions to open and close a modal: openModal(id: string), closeModal(id: string)\nYou can use the setTab function to manually set the tab of a Tab component: setTab(id: string, index: string)\nYou can use the recompute function to recompute a component: recompute(id: string)\nYou can use the getAgGrid function to get the ag-grid instance of a table: getAgGrid(id: string)\nYou can use the setSelectedIndex function to select a row in a table or an AG Grid table: setSelectedIndex(id: string, index: number)\nThe setValue function is meant to set or force the value of a component: setValue(id: string, value: any)\nYou can validate a specific field of a form: validate(id: string, key: string)\nYou can validate all fields of a form: validateAll(id: string, key: string)\nYou can invalidate a specific field of a form: invalidate(id: string, key: string, error: string)\n\nMy instructions: {description}" }, + "csharp": { + "prompt": "Here's my C# code: \n```csharp\n{code}\n```\n\nYou have to write C# code with a public static Main method inside a class. The class name is irrelevant. Specify the parameter types. Do not call the main function. You should generally return the result.\nNuGet packages can be added using the format: #r \"nuget: PackageName, Version\" at the top of the script.\nThe Main method signature should be: public static ReturnType Main(parameter types...)\nArguments are used to generate the input specification and create the frontend UI for the script.\n\nMy instructions: {description}" + }, + "java": { + "prompt": "Here's my Java code: \n```java\n{code}\n```\n\nYou have to write Java code with a Main public class and a public static main() method. The return type can be Object or void. Do not call the main function. You should generally return the result.\nDependencies can be added using the format: //requirements://groupId:artifactId:version at the top of the script.\nThe method signature should be: public static Object main(parameter types...)\nArguments are used to generate the input specification and create the frontend UI for the script.\n\nMy instructions: {description}" + }, "transformer": { "prompt": "Here's my client-side javascript code: \n```javascript\n{code}\n```\n\n\nThe code should process the variable `result` according to my instructions.\nThe variable `result` is available globally.\nAt the end of the code, the processed result has to be returned.\n\n\nYou can access the context object with the ctx global variable. \nThe app state is a store that can be used to store data. You can access and update the state object with the state global variable like this: state.foo = 'bar'\nYou can use the goto function to navigate to a specific URL: goto(path: string, newTab?: boolean)\nYou can use the openModal and closeModal functions to open and close a modal: openModal(id: string), closeModal(id: string)\nYou can use the setTab function to manually set the tab of a Tab component: setTab(id: string, index: string)\nYou can use the recompute function to recompute a component: recompute(id: string)\nYou can use the getAgGrid function to get the ag-grid instance of a table: getAgGrid(id: string)\nYou can use the setSelectedIndex function to select a row in a table or an AG Grid table: setSelectedIndex(id: string, index: number)\nThe setValue function is meant to set or force the value of a component: setValue(id: string, value: any)\nYou can validate a specific field of a form: validate(id: string, key: string)\nYou can validate all fields of a form: validateAll(id: string, key: string)\nYou can invalidate a specific field of a form: invalidate(id: string, key: string, error: string)\n\n\nMy instructions: {description}" } diff --git a/frontend/src/lib/components/copilot/prompts/fixPrompt.ts b/frontend/src/lib/components/copilot/prompts/fixPrompt.ts index e7f0697c26..4fc6ed3227 100644 --- a/frontend/src/lib/components/copilot/prompts/fixPrompt.ts +++ b/frontend/src/lib/components/copilot/prompts/fixPrompt.ts @@ -45,6 +45,12 @@ export const FIX_PROMPT = { }, "php": { "prompt": "Here's my php code: \n```php\n{code}\n```\n\nYou have to write a function in php called \"main\". Specify the parameter types. Do not call the main function. You should generally return the result. The script must start with `\n{resourceTypes}\n\nYou need to define the type of the resources that are needed before the main function, but only include them if they are actually needed to achieve the function purpose.\nBefore defining each type, check if the class already exists using class_exists.\nThe resource type name has to be exactly as specified.\nIf you need to import libraries, you need to specify them as comments in the following manner before the main function:\n```\n// require:\n// mylibrary/mylibrary\n// myotherlibrary/myotherlibrary@optionalversion\n```\nNo need to require autoload, it is already done.\n\nI get the following error: {error}\nFix my code." + }, + "csharp": { + "prompt": "Here's my C# code: \n```csharp\n{code}\n```\n\nYou have to write C# code with a public static Main method inside a class. The class name is irrelevant. Specify the parameter types. Do not call the main function. You should generally return the result.\nNuGet packages can be added using the format: #r \"nuget: PackageName, Version\" at the top of the script.\nThe Main method signature should be: public static ReturnType Main(parameter types...)\nArguments are used to generate the input specification and create the frontend UI for the script.\n\nI get the following error: {error}\nFix my code." + }, + "java": { + "prompt": "Here's my Java code: \n```java\n{code}\n```\n\nYou have to write Java code with a Main public class and a public static main() method. The return type can be Object or void. Do not call the main function. You should generally return the result.\nDependencies can be added using the format: //requirements://groupId:artifactId:version at the top of the script.\nThe method signature should be: public static Object main(parameter types...)\nArguments are used to generate the input specification and create the frontend UI for the script.\n\nI get the following error: {error}\nFix my code." } } }; \ No newline at end of file diff --git a/frontend/src/lib/components/copilot/prompts/genPrompt.ts b/frontend/src/lib/components/copilot/prompts/genPrompt.ts index eb7ebd210f..a318c55e12 100644 --- a/frontend/src/lib/components/copilot/prompts/genPrompt.ts +++ b/frontend/src/lib/components/copilot/prompts/genPrompt.ts @@ -51,6 +51,12 @@ export const GEN_PROMPT = { }, "transformer": { "prompt": "Write client-side javascript code that should process the variable `result` according to the following instructions: {description}.\nThe variable `result` is available globally.\nAt the end of the code, the processed result has to be returned.\n\n\nYou can access the context object with the ctx global variable. \nThe app state is a store that can be used to store data. You can access and update the state object with the state global variable like this: state.foo = 'bar'\nYou can use the goto function to navigate to a specific URL: goto(path: string, newTab?: boolean)\nYou can use the openModal and closeModal functions to open and close a modal: openModal(id: string), closeModal(id: string)\nYou can use the setTab function to manually set the tab of a Tab component: setTab(id: string, index: string)\nYou can use the recompute function to recompute a component: recompute(id: string)\nYou can use the getAgGrid function to get the ag-grid instance of a table: getAgGrid(id: string)\nYou can use the setSelectedIndex function to select a row in a table or an AG Grid table: setSelectedIndex(id: string, index: number)\nThe setValue function is meant to set or force the value of a component: setValue(id: string, value: any)\nYou can validate a specific field of a form: validate(id: string, key: string)\nYou can validate all fields of a form: validateAll(id: string, key: string)\nYou can invalidate a specific field of a form: invalidate(id: string, key: string, error: string)\n" + }, + "csharp": { + "prompt": "\nYou have to write C# code with a public static Main method inside a class. The class name is irrelevant. Specify the parameter types. Do not call the main function. You should generally return the result.\nNuGet packages can be added using the format: #r \"nuget: PackageName, Version\" at the top of the script.\nThe Main method signature should be: public static ReturnType Main(parameter types...)\nArguments are used to generate the input specification and create the frontend UI for the script.\n\nMy instructions: {description}" + }, + "java": { + "prompt": "\nYou have to write Java code with a Main public class and a public static main() method. The return type can be Object or void. Do not call the main function. You should generally return the result.\nDependencies can be added using the format: //requirements://groupId:artifactId:version at the top of the script.\nThe method signature should be: public static Object main(parameter types...)\nArguments are used to generate the input specification and create the frontend UI for the script.\n\nMy instructions: {description}" } } }; \ No newline at end of file diff --git a/frontend/src/lib/components/flows/FlowEditor.svelte b/frontend/src/lib/components/flows/FlowEditor.svelte index e63e7a69da..6b17f49de6 100644 --- a/frontend/src/lib/components/flows/FlowEditor.svelte +++ b/frontend/src/lib/components/flows/FlowEditor.svelte @@ -31,6 +31,10 @@ }) | undefined onDeployTrigger?: (trigger: Trigger) => void + onTestUpTo?: ((id: string) => void) | undefined + onEditInput?: ((moduleId: string, key: string) => void) | undefined + forceTestTab?: Record + highlightArg?: Record } let { @@ -43,7 +47,11 @@ smallErrorHandler = false, newFlow = false, savedFlow = undefined, - onDeployTrigger = () => {} + onDeployTrigger = () => {}, + onTestUpTo = undefined, + onEditInput = undefined, + forceTestTab, + highlightArg }: Props = $props() let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state() @@ -92,6 +100,8 @@ } aiChatManager.generateStep(detail.moduleId, detail.lang, detail.instructions) }} + {onTestUpTo} + {onEditInput} /> {/if}
@@ -112,6 +122,8 @@ on:applyArgs on:testWithArgs {onDeployTrigger} + {forceTestTab} + {highlightArg} /> {/if} diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte index 030d0792a1..41b3d15827 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte @@ -25,6 +25,8 @@ }) | undefined onDeployTrigger?: (trigger: Trigger) => void + forceTestTab?: Record + highlightArg?: Record } let { @@ -33,7 +35,9 @@ newFlow = false, disabledFlowInputs = false, savedFlow = undefined, - onDeployTrigger = () => {} + onDeployTrigger = () => {}, + forceTestTab, + highlightArg }: Props = $props() const { @@ -138,6 +142,8 @@ previousModule={flowStore.val.value.modules[index - 1]} {enableAi} savedModule={savedFlow?.value.modules[index]} + {forceTestTab} + {highlightArg} /> {/each} {/key} diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index 8f50ae5838..9cb9507344 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -77,6 +77,8 @@ noEditor: boolean enableAi: boolean savedModule?: FlowModule | undefined + forceTestTab?: boolean + highlightArg?: string } let { @@ -89,7 +91,9 @@ scriptTemplate = 'script', noEditor, enableAi, - savedModule = undefined + savedModule = undefined, + forceTestTab = false, + highlightArg = undefined }: Props = $props() let tag: string | undefined = $state(undefined) @@ -268,6 +272,20 @@ onDestroy(() => { $currentEditor = undefined }) + + // Handle force test tab prop with animation + $effect(() => { + if (forceTestTab) { + selected = 'test' + // Add a smooth transition to the test tab + setTimeout(() => { + const testTab = document.querySelector('[value="test"]') + if (testTab) { + testTab.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + } + }, 100) + } + }) @@ -524,6 +542,7 @@ bind:testJob bind:testIsLoading bind:scriptProgress + focusArg={highlightArg} /> {:else if selected === 'advanced'} + highlightArg?: Record } let { @@ -48,7 +50,9 @@ enableAi = false, savedModule = undefined, parentModule = $bindable(), - previousModule = undefined + previousModule = undefined, + forceTestTab, + highlightArg }: Props = $props() function initializePrimaryScheduleForTriggerScript(module: FlowModule) { @@ -196,6 +200,8 @@ {scriptTemplate} {enableAi} {savedModule} + forceTestTab={forceTestTab?.[flowModule.id]} + highlightArg={highlightArg?.[flowModule.id]} /> {/if} {:else if flowModule.value.type === 'forloopflow' || flowModule.value.type == 'whileloopflow'} @@ -210,6 +216,8 @@ ? savedModule.value.modules[index] : undefined} {enableAi} + {forceTestTab} + {highlightArg} /> {/each} {:else if flowModule.value.type === 'branchone'} @@ -229,6 +237,8 @@ ? savedModule.value.default[index] : undefined} {enableAi} + {forceTestTab} + {highlightArg} /> {/each} {/if} @@ -252,6 +262,8 @@ ? savedModule.value.branches[branchIndex]?.modules[index] : undefined} {enableAi} + {forceTestTab} + {highlightArg} /> {/each} {/if} @@ -271,6 +283,8 @@ savedModule={savedModule?.value.type === 'branchall' ? savedModule.value.branches[branchIndex]?.modules[index] : undefined} + {forceTestTab} + {highlightArg} /> {/each} {/if} diff --git a/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte b/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte index f053c6cb62..c3e80d836f 100644 --- a/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte +++ b/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte @@ -70,6 +70,12 @@ 'triggers' ].includes($selectedId) || $selectedId?.includes('branch') + + export function testUpTo() { + if (upToDisabled) return + previewMode = 'upTo' + previewOpen = true + } {#if !upToDisabled} @@ -78,10 +84,7 @@ disabled={upToDisabled} color="light" variant="border" - on:click={() => { - previewMode = 'upTo' - previewOpen = !previewOpen - }} + on:click={testUpTo} startIcon={{ icon: Play }} > Test up to  diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte index e0f8e3e17b..9b05735105 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte @@ -14,7 +14,9 @@ Square, SkipForward, Pin, - X + X, + Play, + Loader2 } from 'lucide-svelte' import { createEventDispatcher, getContext, untrack } from 'svelte' import { fade } from 'svelte/transition' @@ -23,6 +25,7 @@ import { twMerge } from 'tailwind-merge' import IdEditorInput from '$lib/components/IdEditorInput.svelte' import { dfs } from '../dfs' + import { dfs as dfsPreviousResults } from '../previousResults' import { Drawer } from '$lib/components/common' import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte' import { getDependeeAndDependentComponents } from '../flowExplorer' @@ -32,6 +35,8 @@ import OutputPicker from '$lib/components/flows/propPicker/OutputPicker.svelte' import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte' import type { FlowState } from '$lib/components/flows/flowState' + import { Button } from '$lib/components/common' + import ModuleTest from '$lib/components/ModuleTest.svelte' interface Props { selected?: boolean @@ -63,7 +68,10 @@ alwaysShowOutputPicker?: boolean loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined icon?: import('svelte').Snippet + onTestUpTo?: ((id: string) => void) | undefined + inputTransform?: Record | undefined onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void + onEditInput?: (moduleId: string, key: string) => void } let { @@ -91,7 +99,10 @@ alwaysShowOutputPicker = false, loopStatus = undefined, icon, - onUpdateMock + onTestUpTo, + inputTransform, + onUpdateMock, + onEditInput }: Props = $props() let pickableIds: Record | undefined = $state(undefined) @@ -116,11 +127,15 @@ let newId: string = $state(id ?? '') + let moduleTest: ModuleTest | undefined = $state(undefined) + let testIsLoading = $state(false) let hover = $state(false) let connectingData: any | undefined = $state(undefined) let lastJob: any | undefined = $state(undefined) let outputPicker: OutputPicker | undefined = $state(undefined) let historyOpen = $state(false) + let testJob: any | undefined = $state(undefined) + let outputPickerBarOpen = $state(false) let flowStateStore = $derived(flowEditorContext?.flowStateStore) @@ -158,10 +173,24 @@ flowStateStore && $flowStateStore && untrack(() => updateLastJob($flowStateStore)) }) + let nlastJob = $derived.by(() => { + if (testJob) { + return { ...testJob, preview: true } + } + if (lastJob) { + return { ...lastJob, preview: false } + } + return undefined + }) + let isConnectingCandidate = $derived( !!id && !!$flowPropPickerConfig && !!pickableIds && Object.keys(pickableIds).includes(id) ) + const outputPickerVisible = $derived( + editMode && (isConnectingCandidate || alwaysShowOutputPicker) && !!id + ) + const icon_render = $derived(icon) @@ -223,22 +252,36 @@ {/if} +{#if deletable && id && flowEditorContext?.flowStore && outputPickerVisible} + {@const flowStore = flowEditorContext?.flowStore.val} + {@const mod = flowStore?.value ? dfsPreviousResults(id, flowStore, false)[0] : undefined} + {#if mod && $flowStateStore[id]} + + {/if} +{/if} +
(hover = true)} onmouseleave={() => (hover = false)} onpointerdown={stopPropagation(preventDefault(() => dispatch('pointerdown')))} > -
+
+
{#if retry}
- {#if editMode && (isConnectingCandidate || alwaysShowOutputPicker)} + {#if outputPickerVisible} {#snippet children({ allowCopy, isConnecting, selectConnection })} {/snippet} @@ -389,10 +438,60 @@
{#if deletable} +
+ {#if (hover || selected) && outputPickerVisible} +
+ {#if !testIsLoading} + + {:else} + + {/if} +
+ {/if} +
+ {/if} + + {/if} +{/snippet} + +{#snippet editKey(key: string)} + +{/snippet} diff --git a/frontend/src/lib/components/flows/propPicker/OutputPicker.svelte b/frontend/src/lib/components/flows/propPicker/OutputPicker.svelte index 864602718f..b62326352f 100644 --- a/frontend/src/lib/components/flows/propPicker/OutputPicker.svelte +++ b/frontend/src/lib/components/flows/propPicker/OutputPicker.svelte @@ -1,10 +1,12 @@ - { +
{ e.preventDefault() e.stopPropagation() }} - bind:this={popover} - allowFullScreen - contentClasses="overflow-hidden resize rounded-md" - contentStyle={`width: calc(${MIN_WIDTH}px); min-width: calc(${MIN_WIDTH}px); height: calc(${MIN_HEIGHT}px); min-height: calc(${MIN_HEIGHT}px);`} - extraProps={{ 'data-prop-picker': true }} - closeOnOtherPopoverOpen - class="outline-none" > - {#snippet trigger({ isOpen })} -
{ - e.preventDefault() - e.stopPropagation() - }} - data-prop-picker - title={`${isOpen ? 'Close' : 'Open'} step output`} - > -
- +
+
+
+ {#if showInput} + { + e.preventDefault() + e.stopPropagation() + }} + allowFullScreen + contentClasses="overflow-hidden resize" + contentStyle={`width: calc(${MIN_WIDTH}px); min-width: calc(${MIN_WIDTH}px); height: calc(${MIN_HEIGHT}px); min-height: calc(${MIN_HEIGHT}px); `} + extraProps={{ 'data-prop-picker': true }} + closeOnOtherPopoverOpen + disableFocusTrap + class="flex-1 h-full" + bind:isOpen={inputOpen} + bind:this={inputPopover} > -
+ In + + {/snippet} + {#snippet content()} + + {/snippet} + + {/if} + { + e.preventDefault() + e.stopPropagation() + }} + bind:this={popover} + allowFullScreen + contentClasses="overflow-hidden resize" + contentStyle={`width: calc(${MIN_WIDTH}px); min-width: calc(${MIN_WIDTH}px); height: calc(${MIN_HEIGHT}px); min-height: calc(${MIN_HEIGHT}px); `} + extraProps={{ 'data-prop-picker': true }} + closeOnOtherPopoverOpen + class="flex-1 h-full" + bind:isOpen={outputOpen} + > + {#snippet trigger({ isOpen })} + - -
- -
+ + + {/snippet} + {#snippet content()} + {@render children?.({ + allowCopy: !$flowPropPickerConfig, + isConnecting: showConnecting, + selectConnection + })} + {/snippet} +
- {/snippet} - {#snippet content()} - {@render children?.({ - allowCopy: !$flowPropPickerConfig, - isConnecting: showConnecting, - selectConnection - })} - {/snippet} - +
+