From b535c13be5ce6a473d3729f8ac72a33cd57c656a Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 24 May 2025 13:26:38 +0200 Subject: [PATCH] squashed 2 --- .github/workflows/aider-after-review.yaml | 129 +- .github/workflows/aider-external.yaml | 70 +- .github/workflows/aider.yaml | 310 +++- .github/workflows/claude.yml | 6 +- .github/workflows/create-docs.yml | 28 +- backend/.vscode/settings.json | 3 +- backend/ee-repo-ref.txt | 6 +- backend/src/main.rs | 25 +- backend/src/monitor.rs | 2 +- backend/windmill-api/openapi.yaml | 9 + backend/windmill-api/src/jobs.rs | 16 +- .../src/postgres_triggers/handler.rs | 5 +- .../windmill-api/src/postgres_triggers/mod.rs | 5 +- .../src/postgres_triggers/trigger.rs | 76 +- backend/windmill-api/src/scripts.rs | 11 +- backend/windmill-api/src/utils.rs | 13 - backend/windmill-common/src/agent_workers.rs | 4 +- backend/windmill-common/src/jwt.rs | 1 - backend/windmill-common/src/scripts.rs | 61 +- backend/windmill-common/src/utils.rs | 78 +- backend/windmill-common/src/worker.rs | 20 +- backend/windmill-queue/src/jobs.rs | 21 +- backend/windmill-worker/src/agent_workers.rs | 12 +- backend/windmill-worker/src/common.rs | 1 + backend/windmill-worker/src/handle_child.rs | 1 + .../windmill-worker/src/result_processor.rs | 387 ++++- backend/windmill-worker/src/worker.rs | 98 +- backend/windmill-worker/src/worker_flow.rs | 16 +- backend/windmill-worker/src/worker_utils.rs | 4 + frontend/package-lock.json | 45 +- frontend/package.json | 5 +- frontend/src/app.html | 2 +- .../AgentWorkerExtraBehavior.svelte | 36 + .../components/DBManagerDrawerButton.svelte | 4 +- .../src/lib/components/Description.svelte | 4 +- frontend/src/lib/components/Dev.svelte | 14 +- .../src/lib/components/DisplayResult.svelte | 10 +- .../components/DisplayResultControlBar.svelte | 2 +- frontend/src/lib/components/DropdownV2.svelte | 19 +- .../src/lib/components/DropdownV2Inner.svelte | 18 +- .../src/lib/components/FlowBuilder.svelte | 325 ++-- .../components/HttpAgentWorkerDrawer.svelte | 129 +- frontend/src/lib/components/Label.svelte | 6 +- frontend/src/lib/components/Path.svelte | 46 +- .../src/lib/components/PrimarySchedule.svelte | 86 + .../lib/components/RunPageSchedules.svelte | 245 +++ .../src/lib/components/SchedulePanel.svelte | 53 - .../src/lib/components/ScriptBuilder.svelte | 301 ++-- .../src/lib/components/ScriptPicker.svelte | 2 +- frontend/src/lib/components/Section.svelte | 4 +- frontend/src/lib/components/SqlRepl.svelte | 4 +- .../lib/components/StringTypeNarrowing.svelte | 2 +- .../src/lib/components/TagsToListenTo.svelte | 87 +- frontend/src/lib/components/Toggle.svelte | 18 +- .../src/lib/components/TooltipInner.svelte | 29 - frontend/src/lib/components/WorkerRepl.svelte | 295 ++++ .../apps/components/display/dbtable/utils.ts | 4 +- frontend/src/lib/components/common/Url.svelte | 22 - .../components/common/button/Button.svelte | 9 +- .../src/lib/components/common/button/model.ts | 1 - .../ConfirmationModal.svelte | 19 +- .../DraftTriggersConfirmationModal.svelte | 178 -- frontend/src/lib/components/common/index.ts | 1 - .../components/common/table/FlowRow.svelte | 2 +- .../components/common/table/ScriptRow.svelte | 2 +- .../toggleButton-v2/ToggleButton.svelte | 1 - .../toggleButton-v2/ToggleButtonGroup.svelte | 2 +- frontend/src/lib/components/dbOps.ts | 14 +- .../components/details/ClipboardPanel.svelte | 5 +- .../details/CopyableCodeBlock.svelte | 10 +- .../details/DetailPageDetailPanel.svelte | 33 +- .../details/DetailPageHeader.svelte | 16 +- .../details/DetailPageLayout.svelte | 76 +- .../details/EmailTriggerCaptures.svelte | 70 - .../details/EmailTriggerConfigSection.svelte | 35 + .../details/EmailTriggerPanel.svelte | 34 +- .../lib/components/flows/FlowEditor.svelte | 4 - .../components/flows/common/FlowCard.svelte | 4 +- .../flows/content/FlowEditorPanel.svelte | 17 +- .../flows/content/FlowModuleWrapper.svelte | 33 +- .../flows/content/FlowPathViewer.svelte | 14 +- .../flows/map/FlowModuleSchemaMap.svelte | 4 +- .../flows/map/InsertModuleButton.svelte | 153 +- .../flows/map/InsertModuleInner.svelte | 167 -- .../flows/propPicker/StepHistory.svelte | 1 - .../src/lib/components/flows/scheduleUtils.ts | 64 +- .../graph/renderers/nodes/TriggersNode.svelte | 53 +- .../renderers/triggers/TriggersBadge.svelte | 381 +---- .../renderers/triggers/TriggersWrapper.svelte | 133 +- .../components/icons/GoogleCloudIcon.svelte | 7 +- frontend/src/lib/components/jobs/utils.ts | 14 +- .../meltComponents/MeltButton.svelte | 13 +- .../lib/components/meltComponents/Menu.svelte | 9 +- .../components/meltComponents/Tooltip.svelte | 31 +- .../src/lib/components/runs/RunRow.svelte | 2 +- .../schema/AddPropertyFormV2.svelte | 6 +- .../src/lib/components/table/DataTable.svelte | 6 +- frontend/src/lib/components/triggers.ts | 42 +- .../triggers/AddTriggersButton.svelte | 133 -- .../components/triggers/CaptureSection.svelte | 508 +----- .../components/triggers/CaptureTable.svelte | 6 +- .../components/triggers/CaptureWrapper.svelte | 297 ++-- .../triggers/TriggerEditorToolbar.svelte | 137 -- .../components/triggers/TriggerLabel.svelte | 43 - .../components/triggers/TriggersEditor.svelte | 579 +++---- .../triggers/TriggersEditorSection.svelte | 135 ++ .../components/triggers/TriggersTable.svelte | 139 -- .../triggers/TriggersWrapper.svelte | 280 ++- .../components/triggers/gcp/GcpCapture.svelte | 71 - .../triggers/gcp/GcpTriggerEditor.svelte | 11 +- .../gcp/GcpTriggerEditorConfigSection.svelte | 104 +- .../triggers/gcp/GcpTriggerEditorInner.svelte | 459 ++--- .../triggers/gcp/GcpTriggerPanel.svelte | 159 +- .../src/lib/components/triggers/gcp/utils.ts | 52 - .../http/RouteBodyTransformerOption.svelte | 44 +- .../triggers/http/RouteCapture.svelte | 87 - .../triggers/http/RouteEditor.svelte | 16 +- .../http/RouteEditorConfigSection.svelte | 115 +- .../triggers/http/RouteEditorInner.svelte | 800 ++++----- .../triggers/http/RoutesPanel.svelte | 154 +- .../src/lib/components/triggers/http/utils.ts | 51 - .../triggers/kafka/KafkaCapture.svelte | 58 - .../triggers/kafka/KafkaTriggerEditor.svelte | 8 +- .../kafka/KafkaTriggerEditorInner.svelte | 447 ++--- .../kafka/KafkaTriggersConfigSection.svelte | 290 +++- .../triggers/kafka/KafkaTriggersPanel.svelte | 168 +- .../lib/components/triggers/kafka/utils.ts | 43 - .../triggers/mqtt/MqttCapture.svelte | 58 - .../mqtt/MqttEditorConfigSection.svelte | 25 +- .../triggers/mqtt/MqttTriggerEditor.svelte | 8 +- .../mqtt/MqttTriggerEditorInner.svelte | 445 ++--- .../triggers/mqtt/MqttTriggersPanel.svelte | 164 +- .../src/lib/components/triggers/mqtt/utils.ts | 48 - .../triggers/nats/NatsCapture.svelte | 58 - .../triggers/nats/NatsTriggerEditor.svelte | 8 +- .../nats/NatsTriggerEditorInner.svelte | 439 ++--- .../nats/NatsTriggersConfigSection.svelte | 318 +++- .../triggers/nats/NatsTriggersPanel.svelte | 170 +- .../src/lib/components/triggers/nats/utils.ts | 48 - .../triggers/postgres/PostgresCapture.svelte | 58 - .../PostgresEditorConfigSection.svelte | 141 ++ .../postgres/PostgresTriggerEditor.svelte | 14 +- .../PostgresTriggerEditorInner.svelte | 955 +++++------ .../postgres/PostgresTriggersPanel.svelte | 166 +- .../postgres/PublicationPicker.svelte | 12 +- .../triggers/postgres/RelationPicker.svelte | 17 +- .../triggers/postgres/SlotPicker.svelte | 5 +- .../lib/components/triggers/postgres/utils.ts | 59 +- .../triggers/schedules/ScheduleEditor.svelte | 10 +- .../schedules/ScheduleEditorInner.svelte | 1511 ++++++++--------- .../components/triggers/sqs/SqsCapture.svelte | 52 - .../triggers/sqs/SqsTriggerEditor.svelte | 8 +- .../sqs/SqsTriggerEditorConfigSection.svelte | 35 +- .../triggers/sqs/SqsTriggerEditorInner.svelte | 426 ++--- .../triggers/sqs/SqsTriggerPanel.svelte | 162 +- .../src/lib/components/triggers/sqs/utils.ts | 46 - .../components/triggers/testingBadge.svelte | 9 - .../components/triggers/triggers.svelte.ts | 456 ----- frontend/src/lib/components/triggers/utils.ts | 529 ------ .../triggers/webhook/WebhooksCapture.svelte | 80 - .../webhook/WebhooksConfigSection.svelte | 332 ++-- .../triggers/webhook/WebhooksPanel.svelte | 60 +- .../websocket/WebsocketCapture.svelte | 55 - .../WebsocketEditorConfigSection.svelte | 37 +- .../websocket/WebsocketTriggerEditor.svelte | 27 +- .../WebsocketTriggerEditorInner.svelte | 867 ++++------ .../websocket/WebsocketTriggersPanel.svelte | 164 +- .../components/triggers/websocket/utils.ts | 46 - frontend/src/lib/script_helpers.ts | 19 +- frontend/src/lib/utils.ts | 56 +- .../(root)/(logged)/flows/add/+page.svelte | 15 +- .../flows/edit/[...path]/+page.svelte | 29 +- .../(logged)/flows/get/[...path]/+page.svelte | 160 +- .../(root)/(logged)/gcp_triggers/+page.svelte | 2 +- .../(logged)/kafka_triggers/+page.svelte | 2 +- .../(logged)/mqtt_triggers/+page.svelte | 2 +- .../(logged)/nats_triggers/+page.svelte | 2 +- .../(logged)/postgres_triggers/+page.svelte | 2 +- .../(root)/(logged)/routes/+page.svelte | 2 +- .../(root)/(logged)/schedules/+page.svelte | 2 +- .../(root)/(logged)/scripts/add/+page.svelte | 7 +- .../scripts/edit/[...path]/+page.svelte | 11 +- .../scripts/get/[...hash]/+page.svelte | 167 +- .../(root)/(logged)/sqs_triggers/+page.svelte | 2 +- .../(logged)/websocket_triggers/+page.svelte | 2 +- .../(root)/(logged)/workers/+page.svelte | 90 +- frontend/src/routes/flows/dev/+page.svelte | 14 +- 187 files changed, 8563 insertions(+), 10446 deletions(-) create mode 100644 frontend/src/lib/components/AgentWorkerExtraBehavior.svelte create mode 100644 frontend/src/lib/components/PrimarySchedule.svelte create mode 100644 frontend/src/lib/components/RunPageSchedules.svelte delete mode 100644 frontend/src/lib/components/SchedulePanel.svelte delete mode 100644 frontend/src/lib/components/TooltipInner.svelte create mode 100644 frontend/src/lib/components/WorkerRepl.svelte delete mode 100644 frontend/src/lib/components/common/Url.svelte delete mode 100644 frontend/src/lib/components/common/confirmationModal/DraftTriggersConfirmationModal.svelte delete mode 100644 frontend/src/lib/components/details/EmailTriggerCaptures.svelte delete mode 100644 frontend/src/lib/components/flows/map/InsertModuleInner.svelte delete mode 100644 frontend/src/lib/components/triggers/AddTriggersButton.svelte delete mode 100644 frontend/src/lib/components/triggers/TriggerEditorToolbar.svelte delete mode 100644 frontend/src/lib/components/triggers/TriggerLabel.svelte create mode 100644 frontend/src/lib/components/triggers/TriggersEditorSection.svelte delete mode 100644 frontend/src/lib/components/triggers/TriggersTable.svelte delete mode 100644 frontend/src/lib/components/triggers/gcp/GcpCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/gcp/utils.ts delete mode 100644 frontend/src/lib/components/triggers/http/RouteCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/kafka/KafkaCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/kafka/utils.ts delete mode 100644 frontend/src/lib/components/triggers/mqtt/MqttCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/mqtt/utils.ts delete mode 100644 frontend/src/lib/components/triggers/nats/NatsCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/nats/utils.ts delete mode 100644 frontend/src/lib/components/triggers/postgres/PostgresCapture.svelte create mode 100644 frontend/src/lib/components/triggers/postgres/PostgresEditorConfigSection.svelte delete mode 100644 frontend/src/lib/components/triggers/sqs/SqsCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/sqs/utils.ts delete mode 100644 frontend/src/lib/components/triggers/testingBadge.svelte delete mode 100644 frontend/src/lib/components/triggers/triggers.svelte.ts delete mode 100644 frontend/src/lib/components/triggers/utils.ts delete mode 100644 frontend/src/lib/components/triggers/webhook/WebhooksCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/websocket/WebsocketCapture.svelte delete mode 100644 frontend/src/lib/components/triggers/websocket/utils.ts diff --git a/.github/workflows/aider-after-review.yaml b/.github/workflows/aider-after-review.yaml index 87d48ddb4c..bbaf2e8c20 100644 --- a/.github/workflows/aider-after-review.yaml +++ b/.github/workflows/aider-after-review.yaml @@ -5,42 +5,12 @@ on: types: [submitted] jobs: - check-membership: + auto-fix-review: if: github.event.review.state == 'changes_requested' && contains(github.event.pull_request.title, '[Aider PR]') - runs-on: ubicloud-standard-2 - outputs: - is_member: ${{ steps.check-membership.outputs.is_member }} - steps: - - name: Check organization membership - id: check-membership - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - REVIEWER: ${{ github.event.review.user.login }} - ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} - 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/$REVIEWER") - - if [ "$STATUS" -eq 204 ]; then - echo "is_member=true" >> $GITHUB_OUTPUT - else - echo "is_member=false" >> $GITHUB_OUTPUT - fi - - check-and-prepare: - needs: check-membership - if: github.event.review.state == 'changes_requested' && contains(github.event.pull_request.title, '[Aider PR]') && needs.check-membership.outputs.is_member == 'true' - runs-on: ubicloud-standard-2 + runs-on: ubicloud-standard-8 permissions: contents: write pull-requests: write - outputs: - prompt_content: ${{ steps.prepare_prompt.outputs.prompt_content }} env: GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} @@ -49,39 +19,96 @@ jobs: WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }} steps: - - name: Acknowledge Request + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git User + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Checkout PR Branch env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} run: | - echo "Commenting on PR #${{ github.event.pull_request.number }} to acknowledge the /aider command." - gh pr comment ${{ github.event.pull_request.number }} --body "🤖 Aider is starting to work on your request. Please be patient, this might take a few minutes." --repo $GITHUB_REPOSITORY + echo "PR review trigger: Checking out PR branch..." + PR_NUMBER=${{ github.event.pull_request.number }} + PR_HEAD_REF=$(gh pr view $PR_NUMBER --json headRefName -q .headRefName --repo $GITHUB_REPOSITORY) + if [[ -z "$PR_HEAD_REF" || "$PR_HEAD_REF" == "null" ]]; then + echo "::error::Could not determine PR head branch for PR #$PR_NUMBER via gh CLI." + exit 1 + fi + echo "Checking out PR head branch: $PR_HEAD_REF for PR #$PR_NUMBER" + git fetch origin "refs/heads/${PR_HEAD_REF}:refs/remotes/origin/${PR_HEAD_REF}" --no-tags + git checkout "$PR_HEAD_REF" + echo "Successfully checked out branch $(git rev-parse --abbrev-ref HEAD)" - - name: Prepare prompt for Aider - id: prepare_prompt + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Aider and Dependencies + run: | + python -m pip install aider-install; aider-install + pip install -U google-generativeai + sudo apt-get update && sudo apt-get install -y jq + + - name: Generate Prompt from Review + id: generate_prompt shell: bash - env: - GITHUB_REPOSITORY: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REVIEW_BODY: ${{ github.event.review.body }} run: | - REVIEW_BODY_ESCAPED="${REVIEW_BODY//\\/\\\\}" - REVIEW_BODY_ESCAPED="${REVIEW_BODY_ESCAPED//\"/\\\"}" + mkdir -p .github/aider + PROMPT_FILE_PATH=".github/aider/review-prompt.txt" + # Get PR review body + REVIEW_BODY="${{ github.event.review.body }}" + REVIEW_BODY_Q=$(printf '%q' "$REVIEW_BODY") + + PR_NUMBER="${{ github.event.pull_request.number }}" + + # Get PR description for context NOT USED FOR NOW + # PR_DETAILS=$(gh pr view $PR_NUMBER --json title,body --repo $GITHUB_REPOSITORY) + # PR_TITLE=$(echo "$PR_DETAILS" | jq -r .title) + # PR_BODY=$(echo "$PR_DETAILS" | jq -r .body) + + # Get all PR review comments ALL_REVIEW_COMMENTS=$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/comments) - - FORMATTED_COMMENTS=$(jq -r '[.[] | {diff_hunk: .diff_hunk, path: .path, body: .body}]' <<< "$ALL_REVIEW_COMMENTS") + /repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/comments \ + | jq '[.[] | {diff_hunk: .diff_hunk, path: .path, body: .body}]') BASE_PROMPT="Fix the following issues in the PR based on the review feedback. The review body is prepended with REVIEW. The review comments are prepended with REVIEW_COMMENTS. The review body and comments are separated by a blank line." + COMPLETE_PROMPT=$(printf "%s\nREVIEW:\n%s\nREVIEW_COMMENTS:\n%s" \ + "$BASE_PROMPT" "$REVIEW_BODY_Q" "$ALL_REVIEW_COMMENTS") + echo "$COMPLETE_PROMPT" > "$PROMPT_FILE_PATH" + echo "PROMPT_FILE_PATH=$PROMPT_FILE_PATH" >> $GITHUB_OUTPUT - COMPLETE_PROMPT="${BASE_PROMPT}"$'\n'"REVIEW:"$'\n'"${REVIEW_BODY_ESCAPED}"$'\n'"REVIEW_COMMENTS:"$'\n'"${FORMATTED_COMMENTS}" + - name: Probe Chat for Relevant Files + id: probe_files + env: + PROMPT_CONTENT_FILE: ${{ steps.generate_prompt.outputs.PROMPT_FILE_PATH }} + run: | + echo "Running probe-chat to find relevant files..." + if [[ ! -f "$PROMPT_CONTENT_FILE" ]]; then + echo "::error::Prompt file $PROMPT_CONTENT_FILE not found!" + exit 1 + fi + PROMPT_CONTENT=$(cat "$PROMPT_CONTENT_FILE") + if [ -z "$PROMPT_CONTENT" ]; then + echo "::error::Prompt content is empty!" + exit 1 + fi - echo "prompt_content<> $GITHUB_OUTPUT - echo "$COMPLETE_PROMPT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + PROMPT_ESCAPED=$(jq -Rs . <<< "$PROMPT_CONTENT") run-aider: needs: [check-membership, check-and-prepare] diff --git a/.github/workflows/aider-external.yaml b/.github/workflows/aider-external.yaml index cfdb03d352..86ef7c466d 100644 --- a/.github/workflows/aider-external.yaml +++ b/.github/workflows/aider-external.yaml @@ -5,21 +5,19 @@ on: types: [external_issue_fix] jobs: - check-and-prepare: - runs-on: ubicloud-standard-2 + auto-fix: + runs-on: ubicloud-standard-8 permissions: contents: write pull-requests: write - outputs: - issue_title: ${{ steps.determine_inputs.outputs.ISSUE_TITLE }} - issue_body: ${{ steps.determine_inputs.outputs.ISSUE_BODY }} - instruction: ${{ steps.determine_inputs.outputs.INSTRUCTION }} + issues: write env: GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }} +<<<<<<< Updated upstream:.github/workflows/aider-external.yaml LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }} @@ -44,29 +42,53 @@ jobs: "https://discord.com/api/v10/channels/${{ github.event.client_payload.channel_id }}/messages" \ -d "{\"content\":\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\"}" fi +======= - - name: Determine inputs for Aider - id: determine_inputs - shell: bash - env: - ISSUE_TITLE: ${{ github.event.client_payload.issue_title }} - ISSUE_BODY: ${{ github.event.client_payload.issue_body }} - INSTRUCTION: ${{ github.event.client_payload.instruction }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit +>>>>>>> Stashed changes:.github/workflows/linear-issue.yaml + + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git User run: | - echo "Determining inputs for Aider..." + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" - echo "ISSUE_TITLE<> "$GITHUB_OUTPUT" - echo "$ISSUE_TITLE" >> "$GITHUB_OUTPUT" - echo "EOF_AIDER_TITLE" >> "$GITHUB_OUTPUT" + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" - echo "ISSUE_BODY<> "$GITHUB_OUTPUT" - echo "$ISSUE_BODY" >> "$GITHUB_OUTPUT" - echo "EOF_AIDER_BODY" >> "$GITHUB_OUTPUT" + - name: Install Aider and Dependencies + run: | + python -m pip install aider-install; aider-install + pip install -U google-generativeai + sudo apt-get update && sudo apt-get install -y jq - echo "INSTRUCTION<> "$GITHUB_OUTPUT" - echo "$INSTRUCTION" >> "$GITHUB_OUTPUT" - echo "EOF_AIDER_INSTRUCTION" >> "$GITHUB_OUTPUT" - echo "Finished determining inputs." + - name: Create Prompt for Aider + id: create_prompt + shell: bash + run: | + PROMPT_FILE_PATH=".github/aider/issue-prompt.txt" + mkdir -p .github/aider + + ISSUE_TITLE="${{ github.event.client_payload.issue_title }}" + INSTRUCTION="${{ github.event.client_payload.instruction }}" + ISSUE_BODY=$(printf '%q' "${{ github.event.client_payload.issue_body }}") + + echo "Processing issue with title: $ISSUE_TITLE" + + JSON_PAYLOAD=$(jq -n \ + --arg title "$ISSUE_TITLE" \ + --arg body "$ISSUE_BODY" \ + '{"body":{"issue_title":$title,"issue_body":$body}}') run-aider: needs: check-and-prepare diff --git a/.github/workflows/aider.yaml b/.github/workflows/aider.yaml index e7609597e3..4454bde051 100644 --- a/.github/workflows/aider.yaml +++ b/.github/workflows/aider.yaml @@ -5,40 +5,12 @@ on: types: [created] jobs: - check-membership: - runs-on: ubicloud-standard-2 + auto-fix: + runs-on: ubicloud-standard-8 if: | github.event_name == 'issue_comment' && contains(github.event.comment.body, '/aider') && !contains(github.event.comment.user.login, '[bot]') - outputs: - is_member: ${{ steps.check-membership.outputs.is_member }} - steps: - - name: Check organization membership - id: check-membership - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - COMMENTER: ${{ github.event.comment.user.login }} - ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} - 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 - - check-and-prepare: - needs: check-membership - runs-on: ubicloud-standard-2 - if: needs.check-membership.outputs.is_member == 'true' permissions: contents: write pull-requests: write @@ -49,23 +21,53 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }} - outputs: - issue_title: ${{ steps.determine_inputs.outputs.ISSUE_TITLE }} - issue_body: ${{ steps.determine_inputs.outputs.ISSUE_BODY }} - comment_content: ${{ steps.determine_inputs.outputs.COMMENT_CONTENT }} - pr_branch: ${{ steps.checkout_pr.outputs.PR_BRANCH }} steps: - - name: Acknowledge Request + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git User + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Checkout PR Branch + if: github.event_name == 'issue_comment' && github.event.issue.pull_request env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} run: | - echo "Commenting on issue/PR #${{ github.event.issue.number }} to acknowledge the /aider command." - gh issue comment ${{ github.event.issue.number }} --body "🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes." --repo $GITHUB_REPOSITORY + echo "Issue comment trigger: Checking out PR branch..." + PR_NUMBER=${{ github.event.issue.number }} + PR_HEAD_REF=$(gh pr view $PR_NUMBER --json headRefName -q .headRefName --repo $GITHUB_REPOSITORY) + if [[ -z "$PR_HEAD_REF" || "$PR_HEAD_REF" == "null" ]]; then + echo "::error::Could not determine PR head branch for PR #$PR_NUMBER via gh CLI." + exit 1 + fi + echo "Checking out PR head branch: $PR_HEAD_REF for PR #$PR_NUMBER" + git fetch origin "refs/heads/${PR_HEAD_REF}:refs/remotes/origin/${PR_HEAD_REF}" --no-tags + git checkout "$PR_HEAD_REF" + echo "Successfully checked out branch $(git rev-parse --abbrev-ref HEAD)" - - name: Determine inputs for Aider - id: determine_inputs + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Aider and Dependencies + run: | + python -m pip install aider-install; aider-install + pip install -U google-generativeai + sudo apt-get update && sudo apt-get install -y jq + + - name: Determine Prompt for Aider + id: determine_prompt shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -74,21 +76,16 @@ jobs: GITHUB_REPOSITORY: ${{ github.repository }} LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} run: | - echo "Determining inputs for Aider..." - ISSUE_TITLE_VAL="" - ISSUE_BODY_VAL="" + PROMPT_FILE_PATH=".github/aider/issue-prompt.txt" + mkdir -p .github/aider + # Determine if this is a PR comment or regular issue comment if [[ ! -z "${{ github.event.issue.pull_request }}" ]]; then echo "This is a comment on a Pull Request" - PR_NUMBER="$ISSUE_NUMBER" + PR_NUMBER="${{ github.event.issue.number }}" - PR_BODY_JSON=$(gh pr view "$PR_NUMBER" --json body --repo "$GITHUB_REPOSITORY") - if [[ $? -ne 0 ]]; then - echo "Error fetching PR body for PR #$PR_NUMBER" - PR_BODY_VAL="" - else - PR_BODY_VAL=$(jq -r '.body // ""' <<< "$PR_BODY_JSON") - fi + # Get PR description to check for issue references + PR_BODY=$(gh pr view $PR_NUMBER --json body -q .body --repo $GITHUB_REPOSITORY) if [[ ! -z "$PR_BODY_VAL" ]]; then REFERENCED_ISSUE="" @@ -119,29 +116,214 @@ jobs: ISSUE_TITLE_VAL=$(jq -r '.title // ""' <<< "$ISSUE_DETAILS_JSON") ISSUE_BODY_VAL=$(jq -r '.body // ""' <<< "$ISSUE_DETAILS_JSON") fi + printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ + "$BASE_PROMPT" "$PROCESSED_ISSUE_PROMPT" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH" + else + echo "::warning::API call failed (HTTP $HTTP_CODE). Using PR comment with issue context." + printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ + "$BASE_PROMPT" "$ISSUE_BODY_Q" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH" fi + rm -f /tmp/api_response.txt else - echo "PR body is empty or could not be fetched." + echo "No referenced issue found in PR description, using comment content only" + # Use comment content directly as with regular issue comments + echo '${{ github.event.comment.body }}' > /tmp/raw_comment.txt + RAW_COMMENT_BODY=$(cat /tmp/raw_comment.txt) + COMMENT_CONTENT=$(echo "$RAW_COMMENT_BODY" | sed 's|^/aider||' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + if [[ -z "$COMMENT_CONTENT" ]]; then + echo "::error::Comment with /aider provided, but no instruction found after it. Cannot proceed." + printf "Error: /aider command found but no instruction followed." > "$PROMPT_FILE_PATH" + exit 1 + else + echo "Using comment content as prompt." + printf '%s' "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH" + fi fi else echo "This is a comment on a regular issue" - ISSUE_DETAILS_JSON=$(gh issue view "$ISSUE_NUMBER" --json title,body --repo "$GITHUB_REPOSITORY") - if [[ $? -ne 0 ]]; then - echo "Error fetching issue details for #$ISSUE_NUMBER" + # Fetch the issue details + ISSUE_NUMBER="${{ github.event.issue.number }}" + ISSUE_DETAILS=$(gh issue view $ISSUE_NUMBER --json title,body --repo $GITHUB_REPOSITORY) + ISSUE_TITLE=$(echo "$ISSUE_DETAILS" | jq -r .title) + ISSUE_BODY=$(echo "$ISSUE_DETAILS" | jq -r .body) + + # Store raw comment body in a file first to avoid shell interpretation issues + echo '${{ github.event.comment.body }}' > /tmp/raw_comment.txt + # Extract the command part safely + RAW_COMMENT_BODY=$(cat /tmp/raw_comment.txt) + # Remove the /aider prefix and trim whitespace + COMMENT_CONTENT=$(echo "$RAW_COMMENT_BODY" | sed 's|^/aider||' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + if [[ -z "$COMMENT_CONTENT" ]]; then + echo "::error::Comment with /aider provided, but no instruction found after it. Cannot proceed." + printf "Error: /aider command found but no instruction followed." > "$PROMPT_FILE_PATH" + exit 1 else - ISSUE_TITLE_VAL=$(jq -r '.title // ""' <<< "$ISSUE_DETAILS_JSON") - ISSUE_BODY_VAL=$(jq -r '.body // ""' <<< "$ISSUE_DETAILS_JSON") + echo "Sending issue content and issue comment to external API…" + + ISSUE_TITLE_Q=$(printf '%q' "$ISSUE_TITLE") + ISSUE_BODY_Q=$(printf '%q' "$ISSUE_BODY") + COMMENT_CONTENT_Q=$(printf '%q' "$COMMENT_CONTENT") + + JSON_PAYLOAD=$(jq -n \ + --arg title "$ISSUE_TITLE_Q" \ + --arg body "$ISSUE_BODY_Q" \ + --arg comment "$COMMENT_CONTENT_Q" \ + '{"body":{"issue_title":$title,"issue_body":$body,"issue_comment":$comment}}') + + API_RESULT=$(curl -s -w "\n%{http_code}" \ + -X POST "https://app.windmill.dev/api/w/windmill-labs/jobs/run_wait_result/p/f/ai/quiet_script" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $WINDMILL_TOKEN" \ + --data-binary "$JSON_PAYLOAD" \ + --max-time 90) + + HTTP_CODE=$(echo "$API_RESULT" | tail -n1) + BODY=$(echo "$API_RESULT" | sed '$d') + + echo "$BODY" > /tmp/api_response.txt + + BASE_PROMPT="Try to fix the following issue based on the instruction given by the user. The issue is prepended with the word ISSUE. The instruction is prepended with the word INSTRUCTION. The issue and instruction are separated by a blank line." + if [[ "$HTTP_CODE" -eq 200 ]]; then + PROCESSED_ISSUE_PROMPT=$(jq -r '.effective_body // empty' /tmp/api_response.txt) + if [[ -z "$PROCESSED_ISSUE_PROMPT" || "$PROCESSED_ISSUE_PROMPT" == "null" ]]; then + PROCESSED_ISSUE_PROMPT="" + fi + printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ + "$BASE_PROMPT" "$PROCESSED_ISSUE_PROMPT" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH" + else + echo "::warning::API call failed (HTTP $HTTP_CODE). Using PR comment with issue context." + printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ + "$BASE_PROMPT" "$ISSUE_BODY_Q" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH" + fi + + rm -f /tmp/api_response.txt + fi + fi + echo "Prompt determined and written to $PROMPT_FILE_PATH" + echo "PROMPT_FILE_PATH=$PROMPT_FILE_PATH" >> $GITHUB_OUTPUT + + - name: Probe Chat for Relevant Files + id: probe_files + env: + PROMPT_CONTENT_FILE: ${{ steps.determine_prompt.outputs.PROMPT_FILE_PATH }} + run: | + echo "Running probe-chat to find relevant files..." + if [[ ! -f "$PROMPT_CONTENT_FILE" ]]; then + echo "::error::Prompt file $PROMPT_CONTENT_FILE not found!" + exit 1 + fi + PROMPT_CONTENT=$(cat "$PROMPT_CONTENT_FILE") + if [ -z "$PROMPT_CONTENT" ]; then + echo "::error::Prompt content is empty!" + exit 1 + fi + + PROMPT_ESCAPED=$(jq -Rs . <<< "$PROMPT_CONTENT") + + MESSAGE_FOR_PROBE=$(jq -n --arg prompt_escaped "$PROMPT_ESCAPED" \ + '{ "message": "I'\''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST.\\nREQUEST: \($prompt_escaped). Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: [\"file1.py\", \"file2.py\"]" }' | jq -r .message) + + set -o pipefail + PROBE_OUTPUT=$(npx --yes @buger/probe-chat@latest --max-iterations 50 --model-name gemini-2.5-pro-preview-05-06 --message "$MESSAGE_FOR_PROBE") || { + echo "::error::probe-chat command failed. Output:" + echo "$PROBE_OUTPUT" + exit 1 + } + set +o pipefail + echo "Probe-chat raw output:" + echo "$PROBE_OUTPUT" + + JSON_FILES=$(echo "$PROBE_OUTPUT" | sed -n '/^\s*\[/,$p' | sed '/^\s*\]/q') + echo "Extracted JSON block:" + echo "$JSON_FILES" + + FILES_LIST=$(echo "$JSON_FILES" | jq -e -r '[.[] | select(type == "string" and . != "" and . != null and (endswith("/") | not))] | map(@sh) | join(" ")' || echo "") + + if [[ -z "$FILES_LIST" ]]; then + echo "::warning::probe-chat did not identify any relevant files." + exit 1 + fi + + echo "Formatted files list for aider: $FILES_LIST" + echo "FILES_TO_EDIT=$FILES_LIST" >> $GITHUB_ENV + + - name: Run Aider with external prompt + run: | + echo "Files identified by probe-chat: ${{ env.FILES_TO_EDIT }}" + aider \ + --read CLAUDE.md \ + --read backend/CLAUDE.md \ + --read frontend/CLAUDE.md \ + ${{ env.FILES_TO_EDIT }} \ + --model gemini/gemini-2.5-pro-preview-05-06 \ + --message-file .github/aider/issue-prompt.txt \ + --yes \ + --no-check-update \ + --auto-commits \ + --no-analytics \ + --no-gitignore \ + | tee .github/aider/aider-output.txt || true + echo "Aider command completed. Output saved to .github/aider/aider-output.txt" + + - name: Clean up prompt file + if: always() + run: rm -f .github/aider/issue-prompt.txt + + - name: Commit and Push Changes + id: commit_and_push + if: ${{ success() }} + run: | + if [[ -z "${{ github.event.issue.pull_request }}" ]]; then + BRANCH_NAME="aider-fix-issue-${{ github.event.issue.number }}" + + # Check if branch exists remotely + if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then + echo "Branch $BRANCH_NAME already exists remotely, fetching it" + git fetch origin $BRANCH_NAME + git checkout $BRANCH_NAME + git pull origin $BRANCH_NAME + else + echo "Creating new branch $BRANCH_NAME" + git checkout -b $BRANCH_NAME + fi + + echo "Created/checked out branch $BRANCH_NAME for issue #${{ github.event.issue.number }}" + git push origin $BRANCH_NAME + echo "Pushed to branch $BRANCH_NAME" + echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "CHANGES_APPLIED_MESSAGE=Aider changes pushed to branch $BRANCH_NAME." >> $GITHUB_OUTPUT + else + CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + echo "Attempting to push changes to PR branch $CURRENT_BRANCH_NAME for PR #${{ github.event.issue.number }}" + if git push origin $CURRENT_BRANCH_NAME; then + echo "Push to $CURRENT_BRANCH_NAME successful (or no new changes to push)." + echo "CHANGES_APPLIED_MESSAGE=Aider changes (if any) pushed to PR branch $CURRENT_BRANCH_NAME." >> $GITHUB_OUTPUT + echo "PR_BRANCH_NAME=$CURRENT_BRANCH_NAME" >> $GITHUB_OUTPUT + else + echo "::warning::Push to PR branch $CURRENT_BRANCH_NAME failed." + echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $CURRENT_BRANCH_NAME." >> $GITHUB_OUTPUT fi fi - echo "ISSUE_TITLE<> "$GITHUB_OUTPUT" - echo "$ISSUE_TITLE_VAL" >> "$GITHUB_OUTPUT" - echo "EOF_AIDER_TITLE" >> "$GITHUB_OUTPUT" + - name: Create Pull Request + if: success() && github.event_name == 'issue_comment' && !github.event.issue.pull_request && steps.commit_and_push.outputs.PR_BRANCH_NAME != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_BRANCH: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }} + ISSUE_NUM: ${{ github.event.issue.number }} + run: | + # Create PR description in a temporary file to avoid command line length limits + cat > /tmp/pr-description.md << EOL + This PR was created automatically by Aider to fix issue #${ISSUE_NUM}. - echo "ISSUE_BODY<> "$GITHUB_OUTPUT" - echo "$ISSUE_BODY_VAL" >> "$GITHUB_OUTPUT" - echo "EOF_AIDER_BODY" >> "$GITHUB_OUTPUT" + ## Aider Output + \`\`\` + $(cat .github/aider/aider-output.txt || echo "No output available") + \`\`\` + EOL CLEAN_COMMENT="${COMMENT_BODY/\/aider/}" CLEAN_COMMENT="${CLEAN_COMMENT#"${CLEAN_COMMENT%%[![:space:]]*}"}" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 66eff9dc57..0348e09547 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -49,9 +49,11 @@ jobs: fi claude-code-action: - needs: check-membership if: | - needs.check-membership.outputs.is_member == 'true' + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/aider')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/aider')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '/aider')) || + (github.event_name == 'issues' && contains(github.event.issue.body, '/aider')) runs-on: ubicloud-standard-8 permissions: contents: read diff --git a/.github/workflows/create-docs.yml b/.github/workflows/create-docs.yml index 209883d24c..6c280ae5f9 100644 --- a/.github/workflows/create-docs.yml +++ b/.github/workflows/create-docs.yml @@ -3,34 +3,8 @@ on: types: [created] jobs: - check-membership: - if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && github.event.comment.user.type != 'Bot' }} - runs-on: ubicloud-standard-2 - outputs: - is_member: ${{ steps.check-membership.outputs.is_member }} - steps: - - name: Check organization membership - id: check-membership - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMMENTER: ${{ github.event.comment.user.login }} - run: | - ORG="windmill-labs" - STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: token $GH_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 - trigger-docs: - needs: check-membership - if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && needs.check-membership.outputs.is_member == 'true' }} + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') }} uses: windmill-labs/windmilldocs/.github/workflows/create-docs.yml@main with: pr_number: ${{ github.event.issue.number }} diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json index 6ebd36cf07..817f8da4f9 100644 --- a/backend/.vscode/settings.json +++ b/backend/.vscode/settings.json @@ -11,5 +11,6 @@ "remote.autoForwardPorts": true, "conventionalCommits.scopes": [ "restructring triggers, decoding trigger message on work" - ] + ], + "rust-analyzer.cargo.features": ["license", "enterprise", "agent_worker_server"] } diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 9b39544645..baf3c8cf48 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1,5 @@ -72e6260ca886628cf1ba271bc058e6ecfdecdae5 \ No newline at end of file +<<<<<<< Updated upstream +72e6260ca886628cf1ba271bc058e6ecfdecdae5 +======= +0cf6424295774980cd04f10986b826fbe09542a0 +>>>>>>> Stashed changes diff --git a/backend/src/main.rs b/backend/src/main.rs index 8c451f63a0..2f5f9117eb 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -15,6 +15,7 @@ use monitor::{ send_logs_to_object_store, WORKERS_NAMES, }; use rand::Rng; +use serde_json::{Map, Value}; use sqlx::postgres::PgListener; use std::{ collections::HashMap, @@ -48,10 +49,13 @@ use windmill_common::{ SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING, }, + jwt::decode_without_verify, scripts::ScriptLang, stats_ee::schedule_stats, triggers::TriggerKind, - utils::{hostname, rd_string, Mode, GIT_VERSION, MODE_AND_ADDONS}, + utils::{ + rd_string, Mode, AGENT_JWT_PREFIX, AGENT_TOKEN, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS, + }, worker::{ reload_custom_tags_setting, Connection, HUB_CACHE_DIR, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP, }, @@ -261,7 +265,7 @@ async fn windmill_main() -> anyhow::Result<()> { tracing::error!("Failed to install rustls crypto provider"); } - let hostname = hostname(); + let hostname = HOSTNAME.to_owned(); let mode_and_addons = MODE_AND_ADDONS.clone(); let mode = mode_and_addons.mode; @@ -675,12 +679,27 @@ Windmill Community Edition {GIT_VERSION} let base_internal_url = base_internal_rx.await?; if worker_mode { let mut workers = vec![]; + + let suffix_to_append = match mode { + Mode::Agent => { + let decoded_token = decode_without_verify::>( + AGENT_TOKEN.trim_start_matches(AGENT_JWT_PREFIX), + )?; + let suffix_to_append = decoded_token["suffix"].as_str(); + suffix_to_append.map(|s| s.to_owned()) + } + _ => None, + }; + for i in 0..num_workers { - let suffix: String = if i == 0 && first_suffix.as_ref().is_some() { + let mut suffix = if i == 0 && first_suffix.is_some() { first_suffix.as_ref().unwrap().clone() } else { windmill_common::utils::worker_suffix(&hostname, &rd_string(5)) }; + if let Some(suffix_to_append) = suffix_to_append.as_ref() { + suffix = format!("{}_{}", suffix, &suffix_to_append); + } let worker_conn = WorkerConn { conn: if i == 0 || mode != Mode::Agent { conn.clone() diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 5c4d166beb..ecce80e901 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -1986,7 +1986,7 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, worker None, error::Error::ExecutionErr(error_message), true, - same_worker_tx_never_used, + Some(&same_worker_tx_never_used), "", worker_name, send_result_never_used, diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index fd3e81f209..fdcee4cf9d 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4498,6 +4498,13 @@ paths: in: query schema: type: boolean + - name: languages + in: query + description: | + Filter to only include scripts written in the given languages. + Accepts multiple values as a comma-separated list. + schema: + type: string responses: "200": description: All scripts @@ -11142,6 +11149,8 @@ paths: type: array items: type: string + suffix: + type: string exp: type: integer required: diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 5c2041fd85..2e2121650f 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -3154,11 +3154,12 @@ async fn check_tag_available_for_workspace( let tags = get_scope_tags(authed); if let Some(tags) = tags { - if !tags.contains(&tag.as_str()) { + if !tags.contains(&tag.as_str()) && !authed.is_admin { return Err(Error::BadRequest(format!( - "Tag {tag} is not available in your scope" + "Tag '{tag}' is not available in your scope. Only admins can use tags outside of the allowed set: {:?}", + tags ))); - } + } } let custom_tags_per_w = CUSTOM_TAGS_PER_WORKSPACE.read().await; @@ -3172,11 +3173,14 @@ async fn check_tag_available_for_workspace( .contains(&w_id.to_string()) { Ok(()) - } else { + } else if !authed.is_admin { return Err(error::Error::BadRequest(format!( - "Tag {tag} cannot be used on workspace {w_id}: (CUSTOM_TAGS: {:?})", - custom_tags_per_w + "Tag '{tag}' cannot be used on workspace '{w_id}' by non-admin users. \ + Only admins are allowed to use tags that are not included in the allowed CUSTOM_TAGS: {:?}", + custom_tags_per_w.0 ))); + } else { + Ok(()) } } else { Ok(()) diff --git a/backend/windmill-api/src/postgres_triggers/handler.rs b/backend/windmill-api/src/postgres_triggers/handler.rs index 933582670c..cf13a967d1 100644 --- a/backend/windmill-api/src/postgres_triggers/handler.rs +++ b/backend/windmill-api/src/postgres_triggers/handler.rs @@ -25,7 +25,7 @@ use windmill_common::error::Error; use windmill_common::{ db::UserDB, error::{self, JsonResult, Result}, - utils::{not_found_if_none, paginate, Pagination, StripPath, empty_as_none}, + utils::{not_found_if_none, paginate, Pagination, StripPath}, worker::CLOUD_HOSTED, }; use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; @@ -46,8 +46,7 @@ pub struct Postgres { pub dbname: String, #[serde(default)] pub sslmode: String, - #[serde(default, deserialize_with = "empty_as_none")] - pub root_certificate_pem: Option, + pub root_certificate_pem: String, } #[derive(Debug, Clone, FromRow, Serialize, Deserialize)] diff --git a/backend/windmill-api/src/postgres_triggers/mod.rs b/backend/windmill-api/src/postgres_triggers/mod.rs index d557ca67cc..0be19d6c4c 100644 --- a/backend/windmill-api/src/postgres_triggers/mod.rs +++ b/backend/windmill-api/src/postgres_triggers/mod.rs @@ -84,8 +84,8 @@ pub async fn get_raw_postgres_connection( } }; - let options = if let Some(root_certificate_pem) = &db.root_certificate_pem { - options.ssl_root_cert_from_pem(root_certificate_pem.as_bytes().to_vec()) + let options = if !db.root_certificate_pem.is_empty() { + options.ssl_root_cert_from_pem(db.root_certificate_pem.as_bytes().to_vec()) } else { options }; @@ -96,6 +96,7 @@ pub async fn get_raw_postgres_connection( options } }; + Ok(PgConnection::connect_with(&options).await?) } diff --git a/backend/windmill-api/src/postgres_triggers/trigger.rs b/backend/windmill-api/src/postgres_triggers/trigger.rs index 71daffb4a9..aff7a30589 100644 --- a/backend/windmill-api/src/postgres_triggers/trigger.rs +++ b/backend/windmill-api/src/postgres_triggers/trigger.rs @@ -19,10 +19,10 @@ use crate::{ use bytes::{BufMut, Bytes, BytesMut}; use chrono::TimeZone; use futures::{pin_mut, SinkExt, StreamExt}; -use native_tls::{Certificate, TlsConnector}; +use native_tls::TlsConnector; use pg_escape::{quote_identifier, quote_literal}; use rand::seq::SliceRandom; -use rust_postgres::{config::SslMode, Client, Config, CopyBothDuplex, NoTls, SimpleQueryMessage}; +use rust_postgres::{config::SslMode, Client, Config, CopyBothDuplex, SimpleQueryMessage}; use rust_postgres_native_tls::MakeTlsConnector; use serde::Deserialize; use serde_json::value::RawValue; @@ -79,45 +79,6 @@ enum Error { Tls(#[from] native_tls::Error), } -fn build_tls_connector( - ssl_mode: SslMode, - root_certificate_pem: Option<&String>, -) -> Result, Error> { - let get_tls_builder_for_verify = |root_certificate: Option<&String>| { - let mut builder = TlsConnector::builder(); - if let Some(root_certificate) = root_certificate { - let root_certificate_pem = Certificate::from_pem(root_certificate.as_bytes()).map_err(|e| { - Error::Common(error::Error::BadConfig(format!("Invalid Certs: {e:#}"))) - })?; - builder.add_root_certificate(root_certificate_pem); - } - Ok::<_, Error>(builder) - }; - let connector = match ssl_mode { - SslMode::Disable => return Ok(None), - SslMode::Require | SslMode::Prefer => { - let mut builder = TlsConnector::builder(); - builder.danger_accept_invalid_certs(true); - builder.danger_accept_invalid_hostnames(true); - builder - } - - SslMode::VerifyCa => { - let mut builder = get_tls_builder_for_verify(root_certificate_pem)?; - builder.danger_accept_invalid_hostnames(true); - builder - } - - SslMode::VerifyFull => { - let builder = get_tls_builder_for_verify(root_certificate_pem)?; - builder - } - _ => unreachable!(), - }; - - Ok(Some(MakeTlsConnector::new(connector.build()?))) -} - pub struct PostgresSimpleClient(Client); impl PostgresSimpleClient { @@ -151,27 +112,20 @@ impl PostgresSimpleClient { config.password(&database.password); } - let connector = build_tls_connector(ssl_mode, database.root_certificate_pem.as_ref())?; + if !database.root_certificate_pem.is_empty() { + config.ssl_root_cert(database.root_certificate_pem.as_bytes()); + } - let client = if let Some(connector) = connector { - let (client, connection) = config.connect(connector).await?; - tokio::spawn(async move { - if let Err(e) = connection.await { - tracing::debug!("{:#?}", e); - }; - tracing::info!("Successfully Connected into database"); - }); - client - } else { - let (client, connection) = config.connect(NoTls).await?; - tokio::spawn(async move { - if let Err(e) = connection.await { - tracing::debug!("{:#?}", e); - }; - tracing::info!("Successfully Connected into database"); - }); - client - }; + let connector = MakeTlsConnector::new(TlsConnector::new()?); + + let (client, connection) = config.connect(connector).await?; + + tokio::spawn(async move { + if let Err(e) = connection.await { + tracing::debug!("{:#?}", e); + }; + tracing::info!("Successfully Connected into database"); + }); Ok(PostgresSimpleClient(client)) } diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 90da2187dc..25e890015a 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -206,7 +206,6 @@ async fn list_scripts( Query(lq): Query, ) -> JsonResult> { let (per_page, offset) = paginate(pagination); - let mut sqlb = SqlBuilder::select_from("script as o") .fields(&[ "hash", @@ -321,6 +320,16 @@ async fn list_scripts( .fields(&["dm.deployment_msg"]); } + if let Some(languages) = lq.languages { + sqlb.and_where_in( + "language", + &languages + .iter() + .map(|language| quote(language.as_str())) + .collect_vec(), + ); + } + let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?; let mut tx = user_db.begin(&authed).await?; let rows = sqlx::query_as::<_, ListableScript>(&sql) diff --git a/backend/windmill-api/src/utils.rs b/backend/windmill-api/src/utils.rs index 5eef389138..b59d5d4d7f 100644 --- a/backend/windmill-api/src/utils.rs +++ b/backend/windmill-api/src/utils.rs @@ -5,9 +5,6 @@ * Please see the included NOTICE for copyright information and * LICENSE-AGPL for a copy of the license. */ - -use std::fmt::Display; - use axum::{body::Body, response::Response}; use regex::Regex; use serde::{Deserialize, Deserializer}; @@ -38,16 +35,6 @@ pub enum RunnableKind { Flow, } -impl Display for RunnableKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let runnable_kind = match self { - RunnableKind::Script => "script", - RunnableKind::Flow => "flow" - }; - write!(f, "{}", runnable_kind) - } -} - pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> { let is_admin = is_super_admin_email(db, email).await?; diff --git a/backend/windmill-common/src/agent_workers.rs b/backend/windmill-common/src/agent_workers.rs index 7c91ec2165..0d7a0810ed 100644 --- a/backend/windmill-common/src/agent_workers.rs +++ b/backend/windmill-common/src/agent_workers.rs @@ -11,12 +11,11 @@ use std::time::Duration; use reqwest_middleware::ClientBuilder; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; -use crate::{jwt::decode_without_verify, worker::HttpClient}; +use crate::{jwt::decode_without_verify, utils::{AGENT_JWT_PREFIX, AGENT_TOKEN}, worker::HttpClient}; lazy_static! { pub static ref BASE_INTERNAL_URL: String = std::env::var("BASE_INTERNAL_URL").unwrap_or("http://localhost:8080".to_string()); - pub static ref AGENT_TOKEN: String = std::env::var("AGENT_TOKEN").unwrap_or_default(); pub static ref DECODED_AGENT_TOKEN: Option = { if AGENT_TOKEN.is_empty() { None @@ -35,7 +34,6 @@ pub struct AgentAuth { pub exp: Option, } -pub const AGENT_JWT_PREFIX: &str = "jwt_agent_"; pub fn build_agent_http_client(worker_suffix: &str) -> HttpClient { let client = ClientBuilder::new( reqwest::Client::builder() diff --git a/backend/windmill-common/src/jwt.rs b/backend/windmill-common/src/jwt.rs index 8ebdb8dacb..624c6ab847 100644 --- a/backend/windmill-common/src/jwt.rs +++ b/backend/windmill-common/src/jwt.rs @@ -20,7 +20,6 @@ pub async fn encode_with_internal_secret(claims: T) -> error::Resu &jsonwebtoken::EncodingKey::from_secret(jwt_secret.as_bytes()), ) .map_err(to_anyhow)?; - Ok(token) } diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index e0391767f8..8e75d4d83e 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -9,6 +9,7 @@ use std::{ fmt::{self, Display}, hash::{Hash, Hasher}, + str::FromStr, }; use crate::{ @@ -21,6 +22,7 @@ use crate::worker::HUB_CACHE_DIR; use anyhow::Context; use backon::ConstantBuilder; use backon::{BackoffBuilder, Retryable}; +use itertools::Itertools; use serde::de::Error as _; use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize}; @@ -84,6 +86,40 @@ impl ScriptLang { } } +impl FromStr for ScriptLang { + type Err = Error; + fn from_str(s: &str) -> Result { + let language = match s.to_lowercase().as_str() { + "bun" => ScriptLang::Bun, + "bunnative" => ScriptLang::Bunnative, + "nativets" => ScriptLang::Nativets, + "deno" => ScriptLang::Deno, + "python3" => ScriptLang::Python3, + "go" => ScriptLang::Go, + "bash" => ScriptLang::Bash, + "powershell" => ScriptLang::Powershell, + "postgresql" => ScriptLang::Postgresql, + "mysql" => ScriptLang::Mysql, + "bigquery" => ScriptLang::Bigquery, + "snowflake" => ScriptLang::Snowflake, + "mssql" => ScriptLang::Mssql, + "graphql" => ScriptLang::Graphql, + "oracledb" => ScriptLang::OracleDB, + "php" => ScriptLang::Php, + "rust" => ScriptLang::Rust, + "ansible" => ScriptLang::Ansible, + "csharp" => ScriptLang::CSharp, + "nu" => ScriptLang::Nu, + "java" => ScriptLang::Java, + language => { + return Err(anyhow::anyhow!("{} is currently not supported", language).into()) + } + }; + + Ok(language) + } +} + #[derive(Eq, PartialEq, Debug, Hash, Clone, Copy, sqlx::Type)] #[sqlx(transparent)] pub struct ScriptHash(pub i64); @@ -366,7 +402,7 @@ where deserializer.deserialize_any(StringOrArrayVisitor) } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct ListScriptQuery { pub path_start: Option, pub path_exact: Option, @@ -383,6 +419,29 @@ pub struct ListScriptQuery { pub include_without_main: Option, pub include_draft_only: Option, pub with_deployment_msg: Option, + #[serde(default, deserialize_with = "from_seq")] + pub languages: Option>, +} + +fn from_seq<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let s = ::deserialize(deserializer)?; + + let languages: Vec = s + .split(",") + .map(ScriptLang::from_str) + .try_collect() + .map_err(|e| serde::de::Error::custom(e.to_string()))?; + + let languages = if languages.is_empty() { + None + } else { + Some(languages) + }; + + Ok(languages) } pub fn to_i64(s: &str) -> crate::error::Result { diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 3f1a1bd385..89c30c5363 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -14,11 +14,13 @@ use crate::error::{to_anyhow, Error, Result}; use crate::global_settings::UNIQUE_ID_SETTING; use crate::DB; use anyhow::Context; +use axum::http::{HeaderName, HeaderValue}; use gethostname::gethostname; use git_version::git_version; use chrono::Utc; use croner::Cron; +use hyper::HeaderMap; use rand::{distr::Alphanumeric, rng, Rng}; use reqwest::Client; use semver::Version; @@ -33,6 +35,10 @@ pub const DEFAULT_PER_PAGE: usize = 1000; pub const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version"); +pub const AGENT_JWT_PREFIX: &str = "jwt_agent_"; +pub const WORKER_NAME_PREFIX: &str = "wk"; +pub const AGENT_WORKER_NAME_PREFIX: &str = "ag"; + use crate::CRITICAL_ALERT_MUTE_UI_ENABLED; use std::panic::{self, AssertUnwindSafe, Location}; use std::sync::atomic::Ordering; @@ -53,6 +59,12 @@ lazy_static::lazy_static! { } ).unwrap_or(Version::new(0, 1, 0)); + pub static ref HOSTNAME :String = std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| { + gethostname() + .to_str() + .map(|x| x.to_string()) + .unwrap_or_else(|| rd_string(5)) + }); pub static ref MODE_AND_ADDONS: ModeAndAddons = { let mut search_addon = false; @@ -120,6 +132,33 @@ lazy_static::lazy_static! { }; } +lazy_static::lazy_static! { + pub static ref AGENT_TOKEN: String = std::env::var("AGENT_TOKEN").unwrap_or_default(); +} + +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum AgentWorkerSuffix { + EnableLiveShell, +} + +impl TryFrom<&str> for AgentWorkerSuffix { + type Error = Error; + fn try_from(value: &str) -> std::result::Result { + let suffix = match value { + "enable.live.shell" => AgentWorkerSuffix::EnableLiveShell, + suffix => { + return Err(Error::Anyhow { + error: anyhow::anyhow!("Unknown suffix: {}", suffix), + location: "utils.rs@149".to_string(), + }) + } + }; + + Ok(suffix) + } +} + #[derive(Clone)] pub struct ModeAndAddons { pub indexer: bool, @@ -167,15 +206,6 @@ pub async fn require_admin_or_devops( Ok(()) } -pub fn hostname() -> String { - std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| { - gethostname() - .to_str() - .map(|x| x.to_string()) - .unwrap_or_else(|| rd_string(5)) - }) -} - fn instance_name(hostname: &str) -> String { hostname .replace(" ", "") @@ -190,11 +220,22 @@ pub fn worker_suffix(hostname: &str, rd_string: &str) -> String { format!("{}-{}", instance_name(hostname), rd_string) } +pub fn check_if_ag_worker_has_specific_suffix( + suffix_to_check: AgentWorkerSuffix, + suffix: &str, +) -> bool { + suffix + .split('_') + .step_by(1) + .filter_map(|sub_suffix| TryInto::::try_into(sub_suffix).ok()) + .any(|parsed_suffix| parsed_suffix == suffix_to_check) +} + pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str) -> String { if is_agent { - format!("ag-{}-{}", worker_group, suffix) + format!("{}-{}-{}", AGENT_WORKER_NAME_PREFIX, worker_group, suffix) } else { - format!("wk-{}-{}", worker_group, suffix) + format!("{}-{}-{}", WORKER_NAME_PREFIX, worker_group, suffix) } } @@ -747,3 +788,18 @@ impl Future for WarnAfterFuture { } } } + +pub fn from_iter_to_header_map<'header_name, 'header_value, T>(headers: T) -> Result +where + T: IntoIterator, +{ + let mut map = HeaderMap::new(); + + for (name_str, value_str) in headers { + let name = HeaderName::from_str(name_str).map_err(to_anyhow)?; + let value = HeaderValue::from_str(value_str).map_err(to_anyhow)?; + map.insert(name, value); + } + + Ok(map) +} diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 2d1e295432..ec3698458c 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use axum::http::HeaderMap; use bytes::Bytes; use const_format::concatcp; use itertools::Itertools; @@ -34,7 +35,7 @@ use crate::{ pub const DEFAULT_CLOUD_TIMEOUT: u64 = 900; pub const DEFAULT_SELFHOSTED_TIMEOUT: u64 = 604800; // 7 days - +pub const AGENT_WORKER_SHELL_TAG_HEADER_NAME: &'static str = "ag-wk-shell-tag"; lazy_static::lazy_static! { pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| { #[cfg(not(feature = "enterprise"))] @@ -158,12 +159,20 @@ impl HttpClient { pub async fn post( &self, url: &str, + headers: Option, body: &T, ) -> anyhow::Result { - let response = self + let response_builder = self .0 .post(format!("{}{}", *BASE_INTERNAL_URL, url)) - .json(body) + .json(body); + + let response_builder = match headers { + Some(headers) => response_builder.headers(headers), + None => response_builder, + }; + + let response = response_builder .send() .await .map_err(|e| anyhow::anyhow!(e))?; @@ -307,7 +316,7 @@ pub async fn store_suspended_pull_query(wc: &WorkerConfig) { } pub fn make_pull_query(tags: &[String]) -> String { - format_pull_query(format!( + let query = format_pull_query(format!( "SELECT id FROM v2_job_queue WHERE running = false AND tag IN ({}) AND scheduled_for <= now() @@ -315,7 +324,8 @@ pub fn make_pull_query(tags: &[String]) -> String { FOR UPDATE SKIP LOCKED LIMIT 1", tags.iter().map(|x| format!("'{x}'")).join(", ") - )) + )); + query } pub async fn store_pull_query(wc: &WorkerConfig) { diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index de64071c42..e3f0c713c1 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -391,6 +391,7 @@ pub async fn append_logs( if let Err(e) = client .post::<_, String>( &format!("/api/w/{}/agent_workers/push_logs/{}", workspace.as_ref(), job_id), + None, &logs.as_ref(), ) .await { @@ -980,7 +981,6 @@ pub async fn add_completed_job( is_flow_step = queued_job.is_flow_step(), language = ?queued_job.script_lang, scheduled_for = ?queued_job.scheduled_for, - workspace_id = ?queued_job.workspace_id, success, "inserted completed job: {} (success: {success})", queued_job.id @@ -2123,11 +2123,15 @@ pub struct PulledJob { pub permissioned_as_folders: Option>, } +<<<<<<< Updated upstream // NOTE: // Precomputed by the server // Used to offload work from agent workers to server #[derive(Serialize, Deserialize)] +======= +#[derive(Debug, Serialize, Deserialize)] +>>>>>>> Stashed changes pub enum PrecomputedAgentInfo { Bun { local: String, remote: String }, Python { @@ -2138,7 +2142,7 @@ pub enum PrecomputedAgentInfo { requirements: Option }, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct JobAndPerms { pub job: MiniPulledJob, pub raw_code: Option, @@ -2290,16 +2294,20 @@ pub async fn pull( db: &Pool, suspend_first: bool, worker_name: &str, - query_o: Option<(String, String)>, + query_o: Option<&(String, String)>, #[cfg(feature = "benchmark")] bench: &mut BenchmarkIter, ) -> windmill_common::error::Result { loop { - if let Some((query_suspended, query_no_suspend)) = query_o.as_ref() { + if let Some((query_suspended, query_no_suspend)) = query_o { let njob = { - let job = sqlx::query_as::<_, PulledJob>(query_suspended) + let job = if query_suspended.is_empty() { + None + } else { + sqlx::query_as::<_, PulledJob>(query_suspended) .bind(worker_name) .fetch_optional(db) - .await?; + .await? + }; if let Some(job) = job { PulledJobResult { job: Some(job), suspended: true } } else { @@ -2549,7 +2557,6 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<'c>( tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; return Ok((None, false)); } - let r = if suspend_first { // tracing::info!("Pulling job with query: {}", query); sqlx::query_as::<_, PulledJob>(&query) diff --git a/backend/windmill-worker/src/agent_workers.rs b/backend/windmill-worker/src/agent_workers.rs index 192ae7ae37..f19963575f 100644 --- a/backend/windmill-worker/src/agent_workers.rs +++ b/backend/windmill-worker/src/agent_workers.rs @@ -1,3 +1,4 @@ +use reqwest::header::HeaderMap; use uuid::Uuid; use windmill_common::{agent_workers::QueueInitJob, worker::HttpClient}; use windmill_queue::{JobAndPerms, JobCompleted}; @@ -6,14 +7,20 @@ pub async fn queue_init_job(client: &HttpClient, content: &str) -> anyhow::Resul client .post( "/api/agent_workers/queue_init_job", + None, &QueueInitJob { content: content.to_string() }, ) .await .and_then(|x: String| Uuid::parse_str(&x).map_err(|e| anyhow::anyhow!(e))) } -pub async fn pull_job(client: &HttpClient) -> anyhow::Result> { - client.post("/api/agent_workers/pull_job", &()).await +pub async fn pull_job( + client: &HttpClient, + headers: Option, +) -> anyhow::Result> { + client + .post("/api/agent_workers/pull_job", headers, &()) + .await } pub async fn send_result(client: &HttpClient, jc: JobCompleted) -> anyhow::Result { @@ -23,6 +30,7 @@ pub async fn send_result(client: &HttpClient, jc: JobCompleted) -> anyhow::Resul "/api/w/{}/agent_workers/send_result/{}", jc.job.workspace_id, jc.job.id ), + None, &jc, ) .await diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index bec5f4c7ef..47bae5ff97 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -494,6 +494,7 @@ pub async fn update_worker_ping_for_failed_init_script( if let Err(e) = client .post::<_, ()>( UPDATE_PING_URL, + None, &Ping { last_job_executed: Some(last_job_id), last_job_workspace_id: None, diff --git a/backend/windmill-worker/src/handle_child.rs b/backend/windmill-worker/src/handle_child.rs index 3eb9e95dc9..436c2fe9f4 100644 --- a/backend/windmill-worker/src/handle_child.rs +++ b/backend/windmill-worker/src/handle_child.rs @@ -227,6 +227,7 @@ pub async fn handle_child( if let Err(err) = client .post::<_, ()>( &format!("/api/agent_workers/set_job_cancelled/{}", job_id), + None, &JobCancelled { canceled_by: "timeout".to_string(), reason: format!("duration > {}", timeout_duration.as_secs()), diff --git a/backend/windmill-worker/src/result_processor.rs b/backend/windmill-worker/src/result_processor.rs index 2ca1539b85..14a9bc565b 100644 --- a/backend/windmill-worker/src/result_processor.rs +++ b/backend/windmill-worker/src/result_processor.rs @@ -9,6 +9,7 @@ use std::{ atomic::{AtomicBool, AtomicU16, Ordering}, Arc, }, + time::Duration, }; use tracing::{field, Instrument}; #[cfg(not(feature = "otel"))] @@ -20,8 +21,10 @@ use windmill_common::{ add_time, error::{self, Error}, jobs::JobKind, - utils::WarnAfterExt, - worker::{to_raw_value, Connection, WORKER_GROUP}, + utils::{from_iter_to_header_map, WarnAfterExt}, + worker::{ + make_pull_query, to_raw_value, Connection, AGENT_WORKER_SHELL_TAG_HEADER_NAME, WORKER_GROUP, + }, KillpillSender, DB, }; @@ -29,23 +32,24 @@ use windmill_common::{ use windmill_common::bench::{BenchmarkInfo, BenchmarkIter}; use windmill_queue::{ - append_logs, get_queued_job, CanceledBy, JobCompleted, MiniPulledJob, + append_logs, get_queued_job, pull, CanceledBy, JobAndPerms, JobCompleted, MiniPulledJob, WrappedError, }; -use serde_json::{json, value::RawValue}; +use serde_json::{json, value::RawValue, Value}; -use tokio::task::JoinHandle; +use tokio::{fs::DirBuilder, task::JoinHandle, time::Instant}; use windmill_queue::{add_completed_job, add_completed_job_error}; use crate::{ bash_executor::ANSI_ESCAPE_RE, - common::{error_to_value, read_result, save_in_cache}, + common::{error_to_value, read_result, save_in_cache, OccupancyMetrics}, + handle_queued_job, otel_ee::add_root_flow_job_to_otlp, worker_flow::update_flow_status_after_job_completion, - AuthedClient, JobCompletedReceiver, JobCompletedSender, SameWorkerSender, SendResult, - UpdateFlow, INIT_SCRIPT_TAG, + AuthedClient, JobCompletedReceiver, JobCompletedSender, NextJob, SameWorkerSender, SendResult, + UpdateFlow, INIT_SCRIPT_TAG, KEEP_JOB_DIR, SAME_WORKER_REQUIREMENTS, SLEEP_QUEUE, }; async fn process_jc( @@ -54,7 +58,7 @@ async fn process_jc( base_internal_url: &str, db: &DB, worker_dir: &str, - same_worker_tx: &SameWorkerSender, + same_worker_tx: Option<&SameWorkerSender>, job_completed_sender: &JobCompletedSender, #[cfg(feature = "benchmark")] bench: &mut BenchmarkIter, ) { @@ -105,7 +109,7 @@ async fn process_jc( &base_internal_url, &db, &worker_dir, - &same_worker_tx, + same_worker_tx, &worker_name, job_completed_sender.clone(), #[cfg(feature = "benchmark")] @@ -119,6 +123,291 @@ async fn process_jc( } } +enum JobCompletedRx { + JobCompleted(SendResult), + Killpill, +} + +const NAP_TIME_DURATION: u64 = 15; + +const RESET: u64 = 2 * 60; + +pub fn start_interactive_worker_shell( + conn: Connection, + hostname: String, + worker_name: String, + mut killpill_rx: tokio::sync::broadcast::Receiver<()>, + base_internal_url: String, + worker_dir: String, +) { + tokio::spawn(async move { + let mut occupancy_metrics = OccupancyMetrics::new(Instant::now()); + let mut has_been_killed = false; + + let (job_completed_tx, job_completed_rx) = JobCompletedSender::new(&conn, 10); + if let Some(db) = conn.as_sql() { + let db = db.clone(); + let job_completed_tx = job_completed_tx.clone(); + let worker_name = worker_name.clone(); + let base_internal_url = base_internal_url.clone(); + let worker_dir = worker_dir.clone(); + tokio::spawn(async move { + let job_completed_rx = job_completed_rx.unwrap(); + let JobCompletedReceiver { bounded_rx, mut killpill_rx, unbounded_rx } = + job_completed_rx; + while let Some(sr) = { + if has_been_killed { + unbounded_rx + .try_recv() + .ok() + .map(JobCompletedRx::JobCompleted) + .or_else(|| { + bounded_rx.try_recv().ok().map(JobCompletedRx::JobCompleted) + }) + } else { + tokio::select! { + biased; + result = unbounded_rx.recv_async() => { + result.ok().map(JobCompletedRx::JobCompleted) + } + result = bounded_rx.recv_async() => { + result.ok().map(JobCompletedRx::JobCompleted) + } + + _ = killpill_rx.recv() => { + Some(JobCompletedRx::Killpill) + } + } + } + } { + #[cfg(feature = "benchmark")] + let mut bench = BenchmarkIter::new(); + + match sr { + JobCompletedRx::JobCompleted(SendResult::JobCompleted(jc)) => { + process_jc( + jc, + &worker_name, + &base_internal_url, + &db, + &worker_dir, + None, + &job_completed_tx, + #[cfg(feature = "benchmark")] + &mut bench, + ) + .await; + } + JobCompletedRx::Killpill => { + has_been_killed = true; + } + _ => {} + } + } + }); + } + let mut killpill_rx2 = killpill_rx.resubscribe(); + let mut last_executed_job: Option = + Instant::now().checked_sub(Duration::from_millis(2500)); + + #[cfg(feature = "benchmark")] + let mut bench = BenchmarkIter::new(); + + loop { + #[cfg(feature = "enterprise")] + { + if let Ok(_) = killpill_rx.try_recv() { + tracing::info!(worker = %worker_name, hostname = %hostname, "killpill received on worker waiting for valid key"); + break; + } + } + + if let Ok(_) = killpill_rx.try_recv() { + break; + } else { + let pulled_job = match &conn { + Connection::Sql(db) => { + let query = ("".to_string(), make_pull_query(&[hostname.to_owned()])); + + #[cfg(feature = "benchmark")] + let mut bench = windmill_common::bench::BenchmarkIter::new(); + let job = pull( + &db, + false, + &worker_name, + Some(&query), + #[cfg(feature = "benchmark")] + &mut bench, + ) + .await; + + job.map(|x| x.job.map(NextJob::Sql)) + } + Connection::Http(client) => { + let header_map = from_iter_to_header_map([( + AGENT_WORKER_SHELL_TAG_HEADER_NAME, + hostname.as_str(), + )]) + .ok(); + + if header_map.is_none() { + tracing::error!( + "An error has occured header map is none, it should not happen" + ); + } + crate::agent_workers::pull_job(&client, header_map) + .await + .map_err(|e| error::Error::InternalErr(e.to_string())) + .map(|x| x.map(|y| NextJob::Http(y))) + } + }; + match pulled_job { + Ok(Some(job)) => { + tracing::debug!(worker = %worker_name, hostname = %hostname, "started handling of job {}", job.id); + + let job_dir = format!("{worker_dir}/{}", job.id); + + DirBuilder::new() + .recursive(true) + .create(&job_dir) + .await + .expect("could not create job dir"); + + let target = &format!("{job_dir}/shared"); + + DirBuilder::new() + .recursive(true) + .create(target) + .await + .expect("could not create shared dir"); + + let JobAndPerms { + job, + raw_code, + raw_lock, + raw_flow, + parent_runnable_path, + token, + precomputed_agent_info: precomputed_bundle, + } = match (job, &conn) { + (NextJob::Sql(job), Connection::Sql(db)) => { + job.get_job_and_perms(db).await + } + (NextJob::Sql(_), Connection::Http(_)) => { + panic!("sql job on http connection") + } + (NextJob::Http(job), _) => job, + }; + + // let token = create_token(&db, &job, job_perms).await; + let authed_client = AuthedClient { + base_internal_url: base_internal_url.to_string(), + token, + workspace: job.workspace_id.to_string(), + force_client: None, + }; + + let arc_job = Arc::new(job); + add_time!(bench, "handle_queued_job START"); + + let span = tracing::span!(tracing::Level::INFO, "job", + job_id = %arc_job.id, workspace_id = %arc_job.workspace_id, worker = %worker_name, hostname = %hostname, tag = %arc_job.tag, + language = "bash", otel.name = "job"); + + windmill_common::otel_ee::set_span_parent(&span, &arc_job.id); + // span.context().span().add_event_with_timestamp("job created".to_string(), arc_job.created_at.into(), vec![]); + + match handle_queued_job( + arc_job.clone(), + raw_code, + raw_lock, + raw_flow, + parent_runnable_path, + &conn, + &authed_client, + &hostname, + &worker_name, + &worker_dir, + &job_dir, + None, + &base_internal_url, + job_completed_tx.clone(), + &mut occupancy_metrics, + &mut killpill_rx2, + precomputed_bundle, + #[cfg(feature = "benchmark")] + &mut bench, + ) + .instrument(span) + .await + { + Err(err) => match &conn { + Connection::Sql(db) => { + let _ = handle_non_flow_job_error( + db, + arc_job.as_ref(), + 0, + None, + error_to_value(err), + &worker_name, + ) + .await; + } + Connection::Http(_) => { + job_completed_tx + .send_job( + JobCompleted { + preprocessed_args: None, + job: arc_job.clone(), + result: Arc::new( + windmill_common::worker::to_raw_value( + &error_to_value(err), + ), + ), + result_columns: None, + mem_peak: 0, + canceled_by: None, + success: false, + cached_res_path: None, + token: authed_client.token.clone(), + duration: None, + }, + false, + ) + .await + .expect("send job completed"); + } + }, + _ => {} + } + + if !KEEP_JOB_DIR.load(Ordering::Relaxed) && !arc_job.same_worker { + let _ = tokio::fs::remove_dir_all(job_dir).await; + } + + last_executed_job = Some(Instant::now()); + } + Ok(None) => { + let now = Instant::now(); + match last_executed_job { + Some(last) if now.duration_since(last).as_secs() > RESET => { + tokio::time::sleep(Duration::from_secs(NAP_TIME_DURATION)).await; + } + _ => { + tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE)).await; + } + } + } + + Err(err) => { + tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err); + } + }; + } + } + }); +} + pub fn start_background_processor( job_completed_rx: JobCompletedReceiver, job_completed_sender: JobCompletedSender, @@ -140,10 +429,6 @@ pub fn start_background_processor( #[cfg(feature = "benchmark")] let mut infos = BenchmarkInfo::new(); - enum JobCompletedRx { - JobCompleted(SendResult), - Killpill, - } //if we have been killed, we want to drain the queue of jobs while let Some(sr) = { if has_been_killed && same_worker_queue_size.load(Ordering::SeqCst) == 0 { @@ -186,7 +471,7 @@ pub fn start_background_processor( &base_internal_url, &db, &worker_dir, - &same_worker_tx, + Some(&same_worker_tx), &job_completed_sender, #[cfg(feature = "benchmark")] &mut bench, @@ -241,7 +526,7 @@ pub fn start_background_processor( success, Arc::new(result), true, - same_worker_tx.clone(), + &same_worker_tx, &worker_dir, stop_early_override, &worker_name, @@ -273,16 +558,18 @@ pub fn start_background_processor( }) } -async fn send_job_completed( - job_completed_tx: JobCompletedSender, - jc: JobCompleted, - -) { - job_completed_tx +async fn send_job_completed(job_completed_tx: JobCompletedSender, jc: JobCompleted) { + let result = job_completed_tx .send_job(jc, true) .with_context(windmill_common::otel_ee::otel_ctx()) - .await - .expect("send job completed") + .await; + + match result { + Ok(()) => tracing::debug!("send job completed"), + Err(err) => { + tracing::error!("An error occurend while sending job completed: {:#?}", err) + } + } } pub async fn process_result( @@ -301,7 +588,6 @@ pub async fn process_result( ) -> error::Result { match result { Ok(result) => { - send_job_completed( job_completed_tx, JobCompleted { @@ -391,7 +677,7 @@ pub async fn handle_receive_completed_job( base_internal_url: &str, db: &DB, worker_dir: &str, - same_worker_tx: &SameWorkerSender, + same_worker_tx: Option<&SameWorkerSender>, worker_name: &str, job_completed_tx: JobCompletedSender, #[cfg(feature = "benchmark")] bench: &mut BenchmarkIter, @@ -459,7 +745,7 @@ pub async fn process_completed_job( client: &AuthedClient, db: &DB, worker_dir: &str, - same_worker_tx: SameWorkerSender, + same_worker_tx: Option<&SameWorkerSender>, worker_name: &str, job_completed_tx: JobCompletedSender, #[cfg(feature = "benchmark")] bench: &mut BenchmarkIter, @@ -535,7 +821,7 @@ pub async fn process_completed_job( true, result, false, - same_worker_tx.clone(), + &same_worker_tx.expect(SAME_WORKER_REQUIREMENTS).to_owned(), &worker_dir, None, worker_name, @@ -575,7 +861,7 @@ pub async fn process_completed_job( false, Arc::new(serde_json::value::to_raw_value(&result).unwrap()), false, - same_worker_tx, + &same_worker_tx.expect(SAME_WORKER_REQUIREMENTS).to_owned(), &worker_dir, None, worker_name, @@ -592,6 +878,34 @@ pub async fn process_completed_job( return Ok(None); } +async fn handle_non_flow_job_error( + db: &DB, + job: &MiniPulledJob, + mem_peak: i32, + canceled_by: Option, + err: Value, + worker_name: &str, +) -> Result { + append_logs( + &job.id, + &job.workspace_id, + format!("Unexpected error during job execution:\n{err:#?}"), + &db.into(), + ) + .await; + add_completed_job_error( + db, + job, + mem_peak, + canceled_by, + err, + worker_name, + false, + None, + ) + .await +} + #[tracing::instrument(name = "job_error", level = "info", skip_all, fields(job_id = %job.id))] pub async fn handle_job_error( db: &DB, @@ -601,7 +915,7 @@ pub async fn handle_job_error( canceled_by: Option, err: Error, unrecoverable: bool, - same_worker_tx: SameWorkerSender, + same_worker_tx: Option<&SameWorkerSender>, worker_dir: &str, worker_name: &str, job_completed_tx: JobCompletedSender, @@ -610,22 +924,13 @@ pub async fn handle_job_error( let err = error_to_value(err); let update_job_future = || async { - append_logs( - &job.id, - &job.workspace_id, - format!("Unexpected error during job execution:\n{err:#?}"), - &db.into(), - ) - .await; - add_completed_job_error( + handle_non_flow_job_error( db, job, mem_peak, canceled_by.clone(), err.clone(), worker_name, - false, - None, ) .await }; @@ -654,7 +959,7 @@ pub async fn handle_job_error( false, Arc::new(serde_json::value::to_raw_value(&wrapped_error).unwrap()), unrecoverable, - same_worker_tx, + &same_worker_tx.expect(SAME_WORKER_REQUIREMENTS).clone(), worker_dir, None, worker_name, diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 92fc0f0351..3976ed79d8 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -17,7 +17,9 @@ use windmill_common::{ cache::{future::FutureCachedExt, ScriptData, ScriptMetadata}, schema::{should_validate_schema, SchemaValidator}, scripts::PREVIEW_IS_TAR_CODEBASE_HASH, - utils::WarnAfterExt, + utils::{ + check_if_ag_worker_has_specific_suffix, AgentWorkerSuffix, WarnAfterExt, WORKER_NAME_PREFIX, + }, worker::{ write_file, Connection, HttpClient, MAX_TIMEOUT, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, TMP_DIR, @@ -112,7 +114,9 @@ use crate::{ job_logger::NO_LOGS_AT_ALL, js_eval::{eval_fetch_timeout, transpile_ts}, pg_executor::do_postgresql, - result_processor::{process_result, start_background_processor}, + result_processor::{ + process_result, start_background_processor, start_interactive_worker_shell, + }, schema::schema_validator_from_main_arg_sig, worker_flow::handle_flow, worker_lockfiles::{ @@ -244,6 +248,8 @@ lazy_static::lazy_static! { const DOTNET_DEFAULT_PATH: &str = "C:\\Program Files\\dotnet\\dotnet.exe"; #[cfg(unix)] const DOTNET_DEFAULT_PATH: &str = "/usr/bin/dotnet"; +pub const SAME_WORKER_REQUIREMENTS: &'static str = + "SameWorkerSender is required because this job may be part of a flow"; lazy_static::lazy_static! { @@ -388,6 +394,31 @@ lazy_static::lazy_static! { pub static ref WIN_ENVS: Envs = vec![]; } +#[derive(Debug)] +pub enum NextJob { + Sql(PulledJob), + Http(JobAndPerms), +} + +impl NextJob { + pub fn job(self) -> MiniPulledJob { + match self { + NextJob::Sql(job) => job.job, + NextJob::Http(job) => job.job, + } + } +} + +impl std::ops::Deref for NextJob { + type Target = MiniPulledJob; + fn deref(&self) -> &Self::Target { + match self { + NextJob::Sql(job) => &job.job, + NextJob::Http(job) => &job.job, + } + } +} + //only matter if CLOUD_HOSTED pub const MAX_RESULT_SIZE: usize = 1024 * 1024 * 2; // 2MB @@ -1124,6 +1155,29 @@ pub async fn run_worker( let job_completed_processor_is_done = Arc::new(AtomicBool::new(matches!(conn, Connection::Http(_)))); + let is_agent_worker_allowed_to_enable_live_shell = || { + worker_name + .split("-") + .last() + .map(|suffix| { + check_if_ag_worker_has_specific_suffix(AgentWorkerSuffix::EnableLiveShell, suffix) + }) + .unwrap_or(false) + }; + + + if worker_name.starts_with(WORKER_NAME_PREFIX) || is_agent_worker_allowed_to_enable_live_shell() { + tracing::debug!("Worker {} now listening for host tag", &worker_name); + start_interactive_worker_shell( + conn.clone(), + hostname.to_owned(), + worker_name.clone(), + killpill_rx.resubscribe(), + base_internal_url.to_owned(), + worker_dir.clone(), + ); + } + let send_result = match (conn, job_completed_rx) { (Connection::Sql(db), Some(job_completed_receiver)) => Some(start_background_processor( job_completed_receiver, @@ -1250,7 +1304,7 @@ pub async fn run_worker( if !valid_key { tracing::error!( worker = %worker_name, hostname = %hostname, - "Invalid license key, workers require a valid license key, sleeping for 30s waiting for valid key to be set" + "Invalid license key, workers require a valid license key, sleeping for 10s waiting for valid key to be set" ); tokio::time::sleep(Duration::from_secs(10)).await; continue; @@ -1320,29 +1374,6 @@ pub async fn run_worker( } else { tracing::info!("benchmark not finished, still pulling jobs {}", infos.iters); } - enum NextJob { - Sql(PulledJob), - Http(JobAndPerms), - } - - impl NextJob { - pub fn job(self) -> MiniPulledJob { - match self { - NextJob::Sql(job) => job.job, - NextJob::Http(job) => job.job, - } - } - } - - impl std::ops::Deref for NextJob { - type Target = MiniPulledJob; - fn deref(&self) -> &Self::Target { - match self { - NextJob::Sql(job) => &job.job, - NextJob::Http(job) => &job.job, - } - } - } let next_job = { // println!("2: {:?}", instant.elapsed()); @@ -1383,6 +1414,7 @@ pub async fn run_worker( "/api/agent_workers/same_worker_job/{}", same_worker_job.job_id ), + None, &same_worker_job, ) .await @@ -1488,7 +1520,7 @@ pub async fn run_worker( } job.map(|x| x.job.map(NextJob::Sql)) } - Connection::Http(client) => crate::agent_workers::pull_job(&client) + Connection::Http(client) => crate::agent_workers::pull_job(&client, None) .await .map_err(|e| error::Error::InternalErr(e.to_string())) .map(|x| x.map(|y| NextJob::Http(y))), @@ -1536,6 +1568,7 @@ pub async fn run_worker( } } } + if matches!(job.kind, JobKind::Noop) { add_time!(bench, "send job completed START"); job_completed_tx @@ -1733,7 +1766,7 @@ pub async fn run_worker( &worker_name, &worker_dir, &job_dir, - same_worker_tx.clone(), + Some(same_worker_tx.clone()), base_internal_url, job_completed_tx.clone(), &mut occupancy_metrics, @@ -1756,7 +1789,7 @@ pub async fn run_worker( None, err, false, - same_worker_tx.clone(), + Some(&same_worker_tx), &worker_dir, &worker_name, job_completed_tx.clone(), @@ -2006,7 +2039,7 @@ pub struct PreviousResult<'a> { pub previous_result: Option<&'a RawValue>, } -async fn handle_queued_job( +pub async fn handle_queued_job( job: Arc, raw_code: Option, raw_lock: Option, @@ -2018,7 +2051,7 @@ async fn handle_queued_job( worker_name: &str, worker_dir: &str, job_dir: &str, - same_worker_tx: SameWorkerSender, + same_worker_tx: Option, base_internal_url: &str, job_completed_tx: JobCompletedSender, occupancy_metrics: &mut OccupancyMetrics, @@ -2167,7 +2200,7 @@ async fn handle_queued_job( db, &client, None, - same_worker_tx, + &same_worker_tx.expect(SAME_WORKER_REQUIREMENTS), worker_dir, job_completed_tx.clone(), worker_name, @@ -2336,7 +2369,6 @@ async fn handle_queued_job( { return Ok(false); } - process_result( job, result.map(|x| Arc::new(x)), diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 0af2bfcc72..85e053317c 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -80,7 +80,7 @@ pub async fn update_flow_status_after_job_completion( success: bool, result: Arc>, unrecoverable: bool, - same_worker_tx: SameWorkerSender, + same_worker_tx: &SameWorkerSender, worker_dir: &str, stop_early_override: Option, worker_name: &str, @@ -109,7 +109,7 @@ pub async fn update_flow_status_after_job_completion( rec.success, rec.result, unrecoverable, - same_worker_tx.clone(), + same_worker_tx, worker_dir, rec.stop_early_override, rec.skip_error_handler, @@ -134,7 +134,7 @@ pub async fn update_flow_status_after_job_completion( error: json!(e.to_string()), }))), true, - same_worker_tx.clone(), + same_worker_tx, worker_dir, rec.stop_early_override, rec.skip_error_handler, @@ -226,7 +226,7 @@ pub async fn update_flow_status_after_job_completion_internal( mut success: bool, result: Arc>, unrecoverable: bool, - same_worker_tx: SameWorkerSender, + same_worker_tx: &SameWorkerSender, worker_dir: &str, stop_early_override: Option, skip_error_handler: bool, @@ -1289,7 +1289,7 @@ pub async fn update_flow_status_after_job_completion_internal( db, client, Some(nresult.clone()), - same_worker_tx.clone(), + same_worker_tx, worker_dir, job_completed_tx, worker_name, @@ -1586,7 +1586,7 @@ pub async fn handle_flow( db: &sqlx::Pool, client: &AuthedClient, last_result: Option>>, - same_worker_tx: SameWorkerSender, + same_worker_tx: &SameWorkerSender, worker_dir: &str, job_completed_tx: JobCompletedSender, worker_name: &str, @@ -1643,7 +1643,7 @@ pub async fn handle_flow( db, client, last_result.clone(), - same_worker_tx.clone(), + same_worker_tx, worker_dir, worker_name, ) @@ -1736,7 +1736,7 @@ async fn push_next_flow_job( db: &sqlx::Pool, client: &AuthedClient, last_job_result: Option>>, - same_worker_tx: SameWorkerSender, + same_worker_tx: &SameWorkerSender, worker_dir: &str, worker_name: &str, ) -> error::Result { diff --git a/backend/windmill-worker/src/worker_utils.rs b/backend/windmill-worker/src/worker_utils.rs index e1ea849999..4da318d37b 100644 --- a/backend/windmill-worker/src/worker_utils.rs +++ b/backend/windmill-worker/src/worker_utils.rs @@ -125,6 +125,7 @@ async fn update_worker_ping_full_inner( client .post::<_, ()>( UPDATE_PING_URL, + None, &Ping { last_job_executed: None, last_job_workspace_id: None, @@ -190,6 +191,7 @@ pub async fn insert_ping( client .post::<_, ()>( UPDATE_PING_URL, + None, &Ping { last_job_executed: None, last_job_workspace_id: None, @@ -249,6 +251,7 @@ pub async fn update_worker_ping_from_job( client .post::( UPDATE_PING_URL, + None, &Ping { last_job_executed: Some(job_id.clone()), last_job_workspace_id: Some(w_id.to_string()), @@ -287,6 +290,7 @@ pub async fn ping_job_status( client .post( &format!("/api/agent_workers/ping_job_status/{}", job_id), + None, &PingJobStatus { mem_peak, current_mem }, ) .await diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 899306ebf0..f605ce6d4f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "@redocly/json-to-json-schema": "^0.0.1", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", "@windmill-labs/svelte-dnd-action": "^0.9.48", + "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^0.1.15", "ag-charts-community": "^9.0.1", "ag-charts-enterprise": "^9.0.1", @@ -79,6 +80,8 @@ "windmill-parser-wasm-ts": "^1.486.1", "windmill-parser-wasm-yaml": "^1.429.0", "windmill-sql-datatype-parser-wasm": "^1.318.0", + "xterm": "^5.3.0", + "xterm-readline": "^1.1.2", "y-monaco": "^0.1.4", "y-websocket": "^1.5.4", "yaml": "^2.3.4", @@ -3795,6 +3798,22 @@ "svelte": ">=3.23.0 || ^5.0.0-next.0" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT", + "peer": true + }, "node_modules/@xyflow/svelte": { "version": "0.1.39", "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.39.tgz", @@ -3953,7 +3972,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5485,8 +5503,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encoding-down": { "version": "6.3.0", @@ -7179,7 +7196,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -11163,7 +11179,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -11192,7 +11207,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -13029,6 +13043,25 @@ "node": ">=0.4" } }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + }, + "node_modules/xterm-readline": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/xterm-readline/-/xterm-readline-1.1.2.tgz", + "integrity": "sha512-1+W2nVuQvCYz9OUYwFBiolrSQUui51aDDyacKXt4PuxeBHqzvabQEJ2kwdBDzsmOjz5BwlDTAjJmYpH2OGqLFA==", + "license": "MIT", + "dependencies": { + "string-width": "^4" + }, + "peerDependencies": { + "@xterm/xterm": "^5.5.0" + } + }, "node_modules/y-leveldb": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 51c7820491..4d2948726d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -81,18 +81,19 @@ "dependencies": { "@aws-crypto/sha256-js": "^4.0.0", "@codingame/monaco-vscode-configuration-service-override": "~16.1.1", + "@codingame/monaco-vscode-editor-api": "~16.1.1", "@codingame/monaco-vscode-standalone-css-language-features": "~16.1.1", "@codingame/monaco-vscode-standalone-html-language-features": "^16.1.1", "@codingame/monaco-vscode-standalone-json-language-features": "~16.1.1", "@codingame/monaco-vscode-standalone-languages": "~16.1.1", "@codingame/monaco-vscode-standalone-typescript-language-features": "~16.1.1", - "@codingame/monaco-vscode-editor-api": "~16.1.1", "@json2csv/plainjs": "^7.0.6", "@leeoniya/ufuzzy": "^1.0.8", "@popperjs/core": "^2.11.6", "@redocly/json-to-json-schema": "^0.0.1", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", "@windmill-labs/svelte-dnd-action": "^0.9.48", + "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^0.1.15", "ag-charts-community": "^9.0.1", "ag-charts-enterprise": "^9.0.1", @@ -148,6 +149,8 @@ "windmill-parser-wasm-ts": "^1.486.1", "windmill-parser-wasm-yaml": "^1.429.0", "windmill-sql-datatype-parser-wasm": "^1.318.0", + "xterm": "^5.3.0", + "xterm-readline": "^1.1.2", "y-monaco": "^0.1.4", "y-websocket": "^1.5.4", "yaml": "^2.3.4", diff --git a/frontend/src/app.html b/frontend/src/app.html index 718800105a..f7fb8efe20 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -25,7 +25,7 @@ %sveltekit.head% - +
diff --git a/frontend/src/lib/components/AgentWorkerExtraBehavior.svelte b/frontend/src/lib/components/AgentWorkerExtraBehavior.svelte new file mode 100644 index 0000000000..5e5ed1162c --- /dev/null +++ b/frontend/src/lib/components/AgentWorkerExtraBehavior.svelte @@ -0,0 +1,36 @@ + + +
+ { + enable_ssh_repl_like = !enable_ssh_repl_like + if (enable_ssh_repl_like) { + suffix = concatAgentSuffix(suffix, AgentKnownSuffix.ENABLE_LIVE_SHELL) + return + } + suffix = removeAgentSuffix(suffix, AgentKnownSuffix.ENABLE_LIVE_SHELL) + }} + options={{ + right: "Enable live shell on worker's host machine", + rightTooltip: + "Allow you to open a live shell and interact with the agent worker's host machine" + }} + class="py-1" + /> +
diff --git a/frontend/src/lib/components/DBManagerDrawerButton.svelte b/frontend/src/lib/components/DBManagerDrawerButton.svelte index 1120f4211f..d0a1b68624 100644 --- a/frontend/src/lib/components/DBManagerDrawerButton.svelte +++ b/frontend/src/lib/components/DBManagerDrawerButton.svelte @@ -18,7 +18,7 @@ import { Alert } from './common' import { dbDeleteTableActionWithPreviewScript, dbTableOpsWithPreviewScripts } from './dbOps' import { makeCreateTableQuery } from './apps/components/display/dbtable/queries/createTable' - import { runPreviewJobAndPollResult } from './jobs/utils' + import { runScriptAndPollResult } from './jobs/utils' import { Pane, Splitpanes } from 'svelte-splitpanes' import SqlRepl from './SqlRepl.svelte' import SimpleAgTable from './SimpleAgTable.svelte' @@ -208,7 +208,7 @@ previewSql: (values) => makeCreateTableQuery(values, resourceType, selectedSchemaKey), async onConfirm(values) { - await runPreviewJobAndPollResult({ + await runScriptAndPollResult({ workspace: $workspaceStore, requestBody: { args: { database: '$res:' + resourcePath }, diff --git a/frontend/src/lib/components/Description.svelte b/frontend/src/lib/components/Description.svelte index 0074928f53..59e233aaa8 100644 --- a/frontend/src/lib/components/Description.svelte +++ b/frontend/src/lib/components/Description.svelte @@ -1,10 +1,8 @@ -
+
{#if link} Learn more diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index 7a91c3b34e..9e72afec65 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -47,11 +47,10 @@ parseTypescriptDeps } from '$lib/relative_imports' import Tooltip from './Tooltip.svelte' - import type { TriggerContext } from './triggers' + import type { ScheduleTrigger, TriggerContext } from './triggers' import { workspaceAIClients } from './copilot/lib' import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker' import type { PickableProperties } from './flows/previousResults' - import { Triggers } from './triggers/triggers.svelte' $: token = $page.url.searchParams.get('wm_token') ?? undefined $: workspace = $page.url.searchParams.get('workspace') ?? undefined $: themeDarkRaw = $page.url.searchParams.get('activeColorTheme') @@ -494,13 +493,20 @@ const testStepStore = writable>({}) const selectedIdStore = writable('settings-metadata') + const selectedTriggerStore = writable< + 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll' + >('webhooks') + const primaryScheduleStore = writable(undefined) const triggersCount = writable(undefined) setContext('TriggerContext', { + primarySchedule: primaryScheduleStore, + selectedTrigger: selectedTriggerStore, triggersCount: triggersCount, simplifiedPoll: writable(false), - showCaptureHint: writable(undefined), - triggersState: new Triggers() + defaultValues: writable(undefined), + captureOn: writable(undefined), + showCaptureHint: writable(undefined) }) setContext('FlowEditorContext', { selectedId: selectedIdStore, diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index c03cfbf597..aa39489723 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -35,7 +35,6 @@ import PdfViewer from './display/PdfViewer.svelte' import type { DisplayResultUi } from './custom_ui' import { getContext, hasContext, createEventDispatcher, onDestroy } from 'svelte' - import { toJsonStr } from '$lib/utils' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' export let result: any @@ -270,6 +269,15 @@ let jsonViewer: Drawer let s3FileViewer: S3FilePicker + function toJsonStr(result: any) { + try { + // console.log(result) + return JSON.stringify(result ?? null, null, 4) ?? 'null' + } catch (e) { + return 'error stringifying object: ' + e.toString() + } + } + function checkIfHasBigInt(result: any) { if (typeof result === 'number' && Number.isInteger(result) && !Number.isSafeInteger(result)) { return true diff --git a/frontend/src/lib/components/DisplayResultControlBar.svelte b/frontend/src/lib/components/DisplayResultControlBar.svelte index bfcdf24c85..ea5920357d 100644 --- a/frontend/src/lib/components/DisplayResultControlBar.svelte +++ b/frontend/src/lib/components/DisplayResultControlBar.svelte @@ -26,7 +26,7 @@ } -
+
{#if customUi?.disableDownload !== true} {#if open && !hidePopup} -
- {#if customMenu} - - {:else} -
- -
- {/if} +
+
+ +
{/if} diff --git a/frontend/src/lib/components/DropdownV2Inner.svelte b/frontend/src/lib/components/DropdownV2Inner.svelte index 3c657d7678..1566e146d0 100644 --- a/frontend/src/lib/components/DropdownV2Inner.svelte +++ b/frontend/src/lib/components/DropdownV2Inner.svelte @@ -4,15 +4,10 @@ import { twMerge } from 'tailwind-merge' import type { MenubarMenuElements } from '@melt-ui/svelte' import type { Item } from '$lib/utils' + export let items: Item[] | (() => Item[]) | (() => Promise) = [] + export let meltItem: MenubarMenuElements['item'] - interface Props { - items?: Item[] | (() => Item[]) | (() => Promise) - meltItem: MenubarMenuElements['item'] - } - - let { items = [], meltItem }: Props = $props() - - let computedItems: Item[] | undefined = $state(undefined) + let computedItems: Item[] | undefined = undefined async function computeItems() { if (typeof items === 'function') { computedItems = ((await items()) ?? []).filter((item) => !item.hide) @@ -43,12 +38,9 @@ item={meltItem} > {#if item.icon} - + {/if} -

- {item.displayName} -

- {@render item.extra?.()} + {item.displayName} {/each}
diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index b28eb4bcec..8854901c0f 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -1,6 +1,7 @@ @@ -1248,16 +1263,6 @@ currentValue={$flowStore} /> - t.draftConfig)} - isFlow={true} - on:canceled={() => { - draftTriggersModalOpen = false - }} - on:confirmed={handleDraftTriggersConfirmed} -/> - {#key renderCount} {#if !$userStore?.operator} @@ -1327,9 +1332,7 @@
+
+
+
+ + +
+
+ +
+
+ + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts index 0c3fce722f..05b30ec1f2 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts @@ -8,7 +8,7 @@ import { } from 'graphql' import { tryEvery } from '$lib/utils' import { stringifySchema } from '$lib/components/copilot/lib' -import { runPreviewJobAndPollResult } from '$lib/components/jobs/utils' +import { runScriptAndPollResult } from '$lib/components/jobs/utils' export enum ColumnIdentity { ByDefault = 'By Default', @@ -114,7 +114,7 @@ export async function loadAllTablesMetaData( } try { - let result = (await runPreviewJobAndPollResult({ + let result = (await runScriptAndPollResult({ workspace: workspace, requestBody: { language: getLanguageByResourceType(resourceType), diff --git a/frontend/src/lib/components/common/Url.svelte b/frontend/src/lib/components/common/Url.svelte deleted file mode 100644 index 2adb226eb4..0000000000 --- a/frontend/src/lib/components/common/Url.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
- {label} - - -
diff --git a/frontend/src/lib/components/common/button/Button.svelte b/frontend/src/lib/components/common/button/Button.svelte index c013443598..13fff1ffef 100644 --- a/frontend/src/lib/components/common/button/Button.svelte +++ b/frontend/src/lib/components/common/button/Button.svelte @@ -198,7 +198,6 @@ this={startIcon.icon} class={twMerge(startIcon?.classes, iconOnlyPadding[size])} size={lucideIconSize} - {...startIcon.props} /> {/if} @@ -247,7 +246,6 @@ this={startIcon.icon} class={twMerge(startIcon?.classes, iconOnlyPadding[size])} size={lucideIconSize} - {...startIcon.props} /> {/if} @@ -290,13 +288,12 @@
- +
diff --git a/frontend/src/lib/components/common/button/model.ts b/frontend/src/lib/components/common/button/model.ts index 4b497f6ebf..430cbfcfaa 100644 --- a/frontend/src/lib/components/common/button/model.ts +++ b/frontend/src/lib/components/common/button/model.ts @@ -10,7 +10,6 @@ export namespace ButtonType { icon?: any | undefined classes?: string faIcon?: any | undefined - props?: any } export const FontSizeClasses: Record = { diff --git a/frontend/src/lib/components/common/confirmationModal/ConfirmationModal.svelte b/frontend/src/lib/components/common/confirmationModal/ConfirmationModal.svelte index 6bb4f1f0e5..d334dc8c4c 100644 --- a/frontend/src/lib/components/common/confirmationModal/ConfirmationModal.svelte +++ b/frontend/src/lib/components/common/confirmationModal/ConfirmationModal.svelte @@ -4,7 +4,6 @@ import { fade } from 'svelte/transition' import Button from '../button/Button.svelte' import { AlertTriangle, CornerDownLeft, Loader2, RefreshCcw } from 'lucide-svelte' - import { twMerge } from 'tailwind-merge' type Props = { title: string @@ -13,7 +12,6 @@ loading?: boolean open?: boolean type?: 'danger' | 'reload' - showIcon?: boolean } const { @@ -22,8 +20,7 @@ keyListen = true, loading = false, open = false, - type: _type, - showIcon = true + type: _type }: Props = $props() const type = $derived(_type ?? 'danger') @@ -95,14 +92,12 @@ )} >
- {#if showIcon} -
- -
- {/if} -
+
+ +
+

{title}

diff --git a/frontend/src/lib/components/common/confirmationModal/DraftTriggersConfirmationModal.svelte b/frontend/src/lib/components/common/confirmationModal/DraftTriggersConfirmationModal.svelte deleted file mode 100644 index 15dee9fdf8..0000000000 --- a/frontend/src/lib/components/common/confirmationModal/DraftTriggersConfirmationModal.svelte +++ /dev/null @@ -1,178 +0,0 @@ - - - dispatch('canceled')} - on:confirmed={() => dispatch('confirmed', { selectedTriggers })} -> -
-
- {`${isFlow ? 'Your flow' : 'Your script'} has draft triggers. Select which draft triggers to deploy with the ${isFlow ? 'flow' : 'script'}. Undeployed - draft triggers will be permanently deleted.`} -
- -
5 ? 'h-[300px]' : ''}> - - - - Triggers to deploy - - - - - {#each draftTriggers as trigger} - {@const SvelteComponent = triggerIconMap[trigger.type]} - {@const permission = checkSavePermissions(trigger)} - {@const isSelectedTrigger = isSelected(selectedTriggers, trigger)} - - -
-
- - {#if trigger.isPrimary} - - {/if} -
-
- -
-
- - - - {#if permission === 'deploy'} -
- toggleTrigger(trigger, e.detail)} - > - - - -
- {:else if permission === 'admin-only'} -
- Admin only -
- {:else if permission === 'invalid-config'} -
- Invalid config -
- {/if} - - - {/each} - - {#if draftTriggers.length === 0} - - - No draft triggers found - - - {/if} - -
-
-
-
diff --git a/frontend/src/lib/components/common/index.ts b/frontend/src/lib/components/common/index.ts index 4dd5670ba0..57d201ce15 100644 --- a/frontend/src/lib/components/common/index.ts +++ b/frontend/src/lib/components/common/index.ts @@ -17,7 +17,6 @@ export { default as Tabs } from './tabs/Tabs.svelte' export { default as Breadcrumb } from './breadcrumb/Breadcrumb.svelte' export { default as FileInput } from './fileInput/FileInput.svelte' export { default as Section } from '../Section.svelte' -export { default as Url } from './Url.svelte' export * from './alert/model' export * from './badge/model' diff --git a/frontend/src/lib/components/common/table/FlowRow.svelte b/frontend/src/lib/components/common/table/FlowRow.svelte index 78f2ca6959..e40ccc56bc 100644 --- a/frontend/src/lib/components/common/table/FlowRow.svelte +++ b/frontend/src/lib/components/common/table/FlowRow.svelte @@ -76,7 +76,7 @@ {#if menuOpen} - goto('/schedules')} bind:this={scheduleEditor} /> + goto('/schedules')} bind:this={scheduleEditor} /> {/if} diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index 286e0778ab..d0bd0fadfe 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -88,7 +88,7 @@ {#if menuOpen} - goto('/schedules')} bind:this={scheduleEditor} /> + goto('/schedules')} bind:this={scheduleEditor} /> {/if}
- +
diff --git a/frontend/src/lib/components/dbOps.ts b/frontend/src/lib/components/dbOps.ts index ff37853a7a..7f68c67bfd 100644 --- a/frontend/src/lib/components/dbOps.ts +++ b/frontend/src/lib/components/dbOps.ts @@ -4,7 +4,7 @@ import { type DbType } from './apps/components/display/dbtable/utils' import { makeSelectQuery } from './apps/components/display/dbtable/queries/select' -import { runPreviewJobAndPollResult } from './jobs/utils' +import { runScriptAndPollResult } from './jobs/utils' import { makeCountQuery } from './apps/components/display/dbtable/queries/count' import { makeUpdateQuery } from './apps/components/display/dbtable/queries/update' import { makeDeleteQuery } from './apps/components/display/dbtable/queries/delete' @@ -55,7 +55,7 @@ export function dbTableOpsWithPreviewScripts({ colDefs, getCount: async ({ quicksearch }) => { const countQuery = makeCountQuery(resourceType, tableKey, undefined, colDefs) - const result = await runPreviewJobAndPollResult({ + const result = await runScriptAndPollResult({ workspace, requestBody: { args: { database: '$res:' + resourcePath, quicksearch }, @@ -68,7 +68,7 @@ export function dbTableOpsWithPreviewScripts({ }, getRows: async (params) => { const query = makeSelectQuery(tableKey, colDefs, undefined, resourceType as DbType) - let items = (await runPreviewJobAndPollResult({ + let items = (await runScriptAndPollResult({ workspace, requestBody: { args: { database: '$res:' + resourcePath, ...params }, @@ -85,7 +85,7 @@ export function dbTableOpsWithPreviewScripts({ onUpdate: async ({ values }, colDef, newValue) => { const updateQuery = makeUpdateQuery(tableKey, colDef, colDefs, resourceType) - await runPreviewJobAndPollResult({ + await runScriptAndPollResult({ workspace, requestBody: { args: { @@ -101,7 +101,7 @@ export function dbTableOpsWithPreviewScripts({ onDelete: async ({ values }) => { const deleteQuery = makeDeleteQuery(tableKey, colDefs, resourceType) - await runPreviewJobAndPollResult({ + await runScriptAndPollResult({ workspace, requestBody: { args: { database: '$res:' + resourcePath, ...values }, @@ -112,7 +112,7 @@ export function dbTableOpsWithPreviewScripts({ }, onInsert: async ({ values }) => { const insertQuery = makeInsertQuery(tableKey, colDefs, resourceType) - runPreviewJobAndPollResult({ + runScriptAndPollResult({ workspace, requestBody: { args: { database: '$res:' + resourcePath, ...values }, @@ -155,7 +155,7 @@ export function dbDeleteTableActionWithPreviewScript({ successText: `Table '${tableKey}' deleted successfully`, action: async () => { const deleteQuery = makeDeleteTableQuery(tableKey, resourceType) - await runPreviewJobAndPollResult({ + await runScriptAndPollResult({ workspace, requestBody: { args: { database: '$res:' + resourcePath }, diff --git a/frontend/src/lib/components/details/ClipboardPanel.svelte b/frontend/src/lib/components/details/ClipboardPanel.svelte index cc2e16e086..c0a230f1f7 100644 --- a/frontend/src/lib/components/details/ClipboardPanel.svelte +++ b/frontend/src/lib/components/details/ClipboardPanel.svelte @@ -16,10 +16,7 @@
{ diff --git a/frontend/src/lib/components/details/CopyableCodeBlock.svelte b/frontend/src/lib/components/details/CopyableCodeBlock.svelte index e9e5c13808..1a98f81412 100644 --- a/frontend/src/lib/components/details/CopyableCodeBlock.svelte +++ b/frontend/src/lib/components/details/CopyableCodeBlock.svelte @@ -12,7 +12,7 @@
{ if (disabled) { @@ -22,10 +22,6 @@ copyToClipboard(code) }} > -
- -
-
- -
+ +
diff --git a/frontend/src/lib/components/details/DetailPageDetailPanel.svelte b/frontend/src/lib/components/details/DetailPageDetailPanel.svelte index 722020e0a3..64433c1f47 100644 --- a/frontend/src/lib/components/details/DetailPageDetailPanel.svelte +++ b/frontend/src/lib/components/details/DetailPageDetailPanel.svelte @@ -3,8 +3,24 @@ import HighlightTheme from '../HighlightTheme.svelte' import FlowViewerInner from '../FlowViewerInner.svelte' + import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte' + export let triggerSelected: + | 'webhooks' + | 'emails' + | 'schedules' + | 'cli' + | 'routes' + | 'websockets' + | 'postgres' + | 'scheduledPoll' + | 'kafka' + | 'mqtt' + | 'sqs' + | 'gcp' + | 'nats' = 'webhooks' export let flow_json: any | undefined = undefined + export let simplfiedPoll: boolean = false export let isOperator: boolean = false @@ -36,8 +52,21 @@ - - + + + + + + + + + + + + + + + {#if flow_json} diff --git a/frontend/src/lib/components/details/DetailPageHeader.svelte b/frontend/src/lib/components/details/DetailPageHeader.svelte index 20fb3ff693..abf4a004bf 100644 --- a/frontend/src/lib/components/details/DetailPageHeader.svelte +++ b/frontend/src/lib/components/details/DetailPageHeader.svelte @@ -5,7 +5,7 @@ import ErrorHandlerToggleButton from './ErrorHandlerToggleButton.svelte' import { twMerge } from 'tailwind-merge' import { userStore } from '$lib/stores' - import { createEventDispatcher, getContext, tick } from 'svelte' + import { createEventDispatcher, getContext } from 'svelte' import type { TriggerContext } from '../triggers' import { Calendar } from 'lucide-svelte' @@ -23,7 +23,7 @@ color?: 'red' } - const { triggersCount, triggersState } = getContext('TriggerContext') + const { triggersCount, selectedTrigger } = getContext('TriggerContext') export let mainButtons: MainButton[] = [] export let menuItems: MenuItemButton[] = [] @@ -52,20 +52,16 @@ tag: {tag} {/if} - {#if triggersState?.triggers?.some((t) => t.isPrimary && !t.isDraft)} - {@const primarySchedule = triggersState.triggers.findIndex( - (t) => t.isPrimary && !t.isDraft - )} + {#if $triggersCount?.primary_schedule} - close(null)} on:insert on:new on:pickFlow on:pickScript /> +
{ + e.stopPropagation() + }} + role="none" + bind:clientWidth={width} + bind:clientHeight={height} + > +
+ close(null)} + {disableAi} + on:insert + bind:funcDesc + {preFilter} + {loading} + /> + {#if selectedKind != 'preprocessor' && selectedKind != 'flow'} + + {/if} +
+ +
+ {#if kind === 'script'} +
+ { + selectedKind = 'script' + }} + /> + {#if customUi?.triggers != false && allowTrigger} + { + selectedKind = 'trigger' + }} + /> + {/if} + { + selectedKind = 'approval' + }} + /> + {#if customUi?.flowNode != false} + { + selectedKind = 'flow' + }} + /> + {/if} + {#if stop} + { + selectedKind = 'script' + }} + /> + {/if} + + { + close(null) + dispatch('new', { kind: 'forloop' }) + }} + /> + { + close(null) + dispatch('new', { kind: 'whileloop' }) + }} + /> + { + close(null) + dispatch('new', { kind: 'branchone' }) + }} + /> + { + close(null) + dispatch('new', { kind: 'branchall' }) + }} + /> +
+ {/if} + + { + close(null) + }} + on:new + on:pickScript + on:pickFlow + {preFilter} + {small} + {displayPath} + /> +
+
diff --git a/frontend/src/lib/components/flows/map/InsertModuleInner.svelte b/frontend/src/lib/components/flows/map/InsertModuleInner.svelte deleted file mode 100644 index cbc3870338..0000000000 --- a/frontend/src/lib/components/flows/map/InsertModuleInner.svelte +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - -
{ - e.stopPropagation() - }} - role="none" - bind:clientWidth={width} - bind:clientHeight={height} -> -
- dispatch('close')} - {disableAi} - on:insert - bind:funcDesc - {preFilter} - {loading} - /> - {#if selectedKind != 'preprocessor' && selectedKind != 'flow'} - - {/if} -
- -
- {#if kind === 'script'} -
- { - selectedKind = 'script' - }} - /> - {#if customUi?.triggers != false && allowTrigger} - { - selectedKind = 'trigger' - }} - /> - {/if} - { - selectedKind = 'approval' - }} - /> - {#if customUi?.flowNode != false} - { - selectedKind = 'flow' - }} - /> - {/if} - {#if stop} - { - selectedKind = 'script' - }} - /> - {/if} - - { - dispatch('close') - dispatch('new', { kind: 'forloop' }) - }} - /> - { - dispatch('close') - dispatch('new', { kind: 'whileloop' }) - }} - /> - { - dispatch('close') - dispatch('new', { kind: 'branchone' }) - }} - /> - { - dispatch('close') - dispatch('new', { kind: 'branchall' }) - }} - /> -
- {/if} - - { - dispatch('close') - }} - on:new - on:pickScript - on:pickFlow - {preFilter} - {small} - {displayPath} - /> -
-
diff --git a/frontend/src/lib/components/flows/propPicker/StepHistory.svelte b/frontend/src/lib/components/flows/propPicker/StepHistory.svelte index 56f1f3c3ae..779d844e82 100644 --- a/frontend/src/lib/components/flows/propPicker/StepHistory.svelte +++ b/frontend/src/lib/components/flows/propPicker/StepHistory.svelte @@ -70,7 +70,6 @@ return undefined } } - $: infiniteList && !noHistory && initLoadInputs() function handleSelect(e: CustomEvent) { diff --git a/frontend/src/lib/components/flows/scheduleUtils.ts b/frontend/src/lib/components/flows/scheduleUtils.ts index 6d77a87e51..bc81db7483 100644 --- a/frontend/src/lib/components/flows/scheduleUtils.ts +++ b/frontend/src/lib/components/flows/scheduleUtils.ts @@ -1,7 +1,6 @@ import { ScheduleService, type Schedule, type TriggersCount } from '$lib/gen' import type { ScheduleTrigger } from '../triggers' import type { Writable } from 'svelte/store' -import { writable } from 'svelte/store' import { get } from 'svelte/store' import { sendUserToast } from '$lib/utils' @@ -39,8 +38,7 @@ export async function loadSchedules( initialPrimarySchedule: Writable, workspace: string, triggersCount: Writable, - loadPrimarySchedule: boolean = false, - isDeployed: Writable = writable(undefined) + loadPrimarySchedule: boolean = false ) { if (!path || path == '') { schedules.set([]) @@ -55,9 +53,6 @@ export async function loadSchedules( isFlow }) const primary = allSchedules.find((s) => s.path == path) - if (primary) { - isDeployed.set(true) - } let remotePrimarySchedule: ScheduleTrigger | false | undefined = undefined if (loadPrimarySchedule && primary) { remotePrimarySchedule = await loadSchedule(path, workspace) @@ -69,7 +64,7 @@ export async function loadSchedules( cron: primary.schedule, timezone: primary.timezone, enabled: primary.enabled - } + } : false } primarySchedule.update((ps) => (ps === undefined || forceRefresh ? remotePrimarySchedule : ps)) @@ -142,58 +137,3 @@ export async function saveSchedule( } } } - -export async function saveScheduleFromCfg( - scheduleCfg: Record, - edit: boolean, - workspace: string -): Promise { - const requestBody = { - schedule: scheduleCfg.schedule, - timezone: scheduleCfg.timezone, - args: scheduleCfg.args, - on_failure: scheduleCfg.on_failure, - on_failure_times: scheduleCfg.on_failure_times, - on_failure_exact: scheduleCfg.on_failure_exact, - on_failure_extra_args: scheduleCfg.on_failure_extra_args, - on_recovery: scheduleCfg.on_recovery, - on_recovery_times: scheduleCfg.on_recovery_times, - on_recovery_extra_args: scheduleCfg.on_recovery_extra_args, - on_success: scheduleCfg.on_success, - on_success_extra_args: scheduleCfg.on_success_extra_args, - ws_error_handler_muted: scheduleCfg.ws_error_handler_muted, - retry: scheduleCfg.retry, - summary: scheduleCfg.summary, - description: scheduleCfg.description, - no_flow_overlap: scheduleCfg.no_flow_overlap, - tag: scheduleCfg.tag, - paused_until: scheduleCfg.paused_until, - cron_version: scheduleCfg.cron_version - } - try { - if (edit) { - await ScheduleService.updateSchedule({ - workspace, - path: scheduleCfg.path, - requestBody: requestBody - }) - sendUserToast(`Schedule ${scheduleCfg.path} updated`) - } else { - await ScheduleService.createSchedule({ - workspace, - requestBody: { - path: scheduleCfg.path, - script_path: scheduleCfg.script_path, - is_flow: scheduleCfg.is_flow, - ...requestBody, - enabled: true - } - }) - sendUserToast(`Schedule ${scheduleCfg.path} created`) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte index 667c0e0904..3479292e8e 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte @@ -2,15 +2,13 @@ import NodeWrapper from './NodeWrapper.svelte' import TriggersWrapper from '../triggers/TriggersWrapper.svelte' import { type GraphEventHandlers, type SimplifiableFlow } from '../../graphBuilder' - import type { FlowModule, TriggersCount } from '$lib/gen' + import type { FlowModule } from '$lib/gen' import { getContext } from 'svelte' import type { Writable } from 'svelte/store' import { Maximize2, Minimize2, Calendar } from 'lucide-svelte' import { getStateColor, getStateHoverColor } from '../../util' import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers' import VirtualItemWrapper from '$lib/components/flows/map/VirtualItemWrapper.svelte' - import { type Trigger, type TriggerType } from '$lib/components/triggers/utils' - import { tick } from 'svelte' export let data: { path: string @@ -28,24 +26,11 @@ selectedId: Writable }>('FlowGraphContext') - const { triggersCount, triggersState } = getContext('TriggerContext') - - function getScheduleCfg(primary: Trigger | undefined, triggersCount: TriggersCount | undefined) { - return primary?.draftConfig - ? { - enabled: primary?.draftConfig?.enabled, - schedule: primary?.draftConfig?.schedule - } - : primary?.lightConfig - ? { enabled: primary?.lightConfig?.enabled, schedule: primary?.lightConfig?.schedule } - : { - enabled: !!triggersCount?.primary_schedule, - schedule: triggersCount?.primary_schedule?.schedule - } - } + const { primarySchedule, triggersCount, selectedTrigger } = + getContext('TriggerContext') - + {#if data.simplifiableFlow?.simplifiedFlow != true} { data?.eventHandlers.insert({ modules: data.modules, @@ -73,24 +57,14 @@ data?.eventHandlers?.simplifyFlow(true) }} on:openScheduledPoll={(e) => { - const primarySchedule = triggersState.triggers.findIndex((t) => t.isPrimary && !t.isDraft) - triggersState.selectedTriggerIndex = primarySchedule + $selectedTrigger = 'scheduledPoll' }} - on:select={() => data?.eventHandlers?.select('triggers')} - onSelect={async (triggerIndex: number) => { + on:select={(e) => { data?.eventHandlers?.select('triggers') - await tick() - triggersState.selectedTriggerIndex = triggerIndex }} on:delete={(e) => { data.eventHandlers.delete(e, '') }} - onAddDraftTrigger={async (type: TriggerType) => { - const newTrigger = triggersState.addDraftTrigger(triggersCount, type) - data?.eventHandlers?.select('triggers') - await tick() - triggersState.selectedTriggerIndex = newTrigger - }} selected={$selectedId == 'triggers'} newItem={data.newFlow} modules={data.modules} @@ -106,23 +80,22 @@ data?.eventHandlers?.select(e.detail) }} > - {#if triggersState.triggers.some((t) => t.isPrimary) || $triggersCount?.primary_schedule} - {@const { enabled, schedule } = getScheduleCfg( - triggersState.triggers.find((t) => t.isPrimary), - $triggersCount - )} + {#if $primarySchedule || ($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule)}
- Schedule every {schedule} - {enabled ? '' : ' (disabled)'} + Schedule every {$primarySchedule?.cron ?? $triggersCount?.primary_schedule?.schedule} + {$primarySchedule?.enabled || + ($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule) + ? '' + : ' (disabled)'}
{:else} - + kind="trigger" + index={0} + {modules} + class={twMerge( + 'hover:bg-surface-hover rounded-md border text-xs w-[23px] h-[23px] relative center-center cursor-pointer bg-surface outline-0' + )} + /> {/if}
- -{#snippet triggerScriptPicker()} -
- { - dispatch('openScheduledPoll') - }} - on:close={() => { - addTriggersButton?.close() - }} - kind="trigger" - index={0} - {modules} - /> -
-{/snippet} diff --git a/frontend/src/lib/components/icons/GoogleCloudIcon.svelte b/frontend/src/lib/components/icons/GoogleCloudIcon.svelte index 65a725c8f6..398fb378ee 100644 --- a/frontend/src/lib/components/icons/GoogleCloudIcon.svelte +++ b/frontend/src/lib/components/icons/GoogleCloudIcon.svelte @@ -1,10 +1,11 @@ { - const uuid = await JobService.runScriptPreview(data) - + const uuid = (isRunScriptByPathData(data) ? await JobService.runScriptByPath(data) : await JobService.runScriptPreview(data)) as string let attempts = 0 while (attempts < maxRetries) { try { diff --git a/frontend/src/lib/components/meltComponents/MeltButton.svelte b/frontend/src/lib/components/meltComponents/MeltButton.svelte index 5d4eeecccc..be528e38fe 100644 --- a/frontend/src/lib/components/meltComponents/MeltButton.svelte +++ b/frontend/src/lib/components/meltComponents/MeltButton.svelte @@ -1,21 +1,12 @@ -
- +
{/if} diff --git a/frontend/src/lib/components/meltComponents/Tooltip.svelte b/frontend/src/lib/components/meltComponents/Tooltip.svelte index 71f41fad04..45c9591857 100644 --- a/frontend/src/lib/components/meltComponents/Tooltip.svelte +++ b/frontend/src/lib/components/meltComponents/Tooltip.svelte @@ -1,11 +1,12 @@ - goto('/schedules')} bind:this={scheduleEditor} /> + goto('/schedules')} bind:this={scheduleEditor} /> diff --git a/frontend/src/lib/components/schema/AddPropertyFormV2.svelte b/frontend/src/lib/components/schema/AddPropertyFormV2.svelte index 1dd5352614..7a15c4f31a 100644 --- a/frontend/src/lib/components/schema/AddPropertyFormV2.svelte +++ b/frontend/src/lib/components/schema/AddPropertyFormV2.svelte @@ -6,7 +6,6 @@ let name: string = '' export let customName: string | undefined = undefined - export let disabled: boolean = false const dispatch = createEventDispatcher() @@ -16,7 +15,7 @@ } - + @@ -31,7 +30,6 @@ close() } }} - {disabled} /> - + {captureInfo.active ? 'Stop' : 'Start capturing'} +
+ + + -
- {#if captureInfo.active} - - {/if} -
-
-
- -
- {#if displayAlert} - - Capturing will suscribe to the trigger endpoint. Treat carefully. - - {/if} - -
- {#key (captureInfo.active, disabled)} -
- {#if disabled === true} - Enter a valid configuration to start capturing. - {:else} - {@render description?.()} - {/if} -
- {/key} -
-
-
- - {#if children} -
- {@render children?.()} -
- {/if} -
-
- - - -
-
- {#if lastCapture} - - {#snippet trigger()} - - {/snippet} - {#snippet content()} - - {/snippet} - - {/if} - {#if selectedCapture} - {@const SvelteComponent = triggerIconMap[captureType]} -
- - - Capture {formatDateShort(selectedCapture?.created_at)} - -
- {/if} - - {#if selectedCapture} - {@const label = isFlow && testKind === 'main' ? 'Test flow with args' : 'Apply args'} - {@const title = - isFlow && testKind === 'main' - ? 'Test flow using captured data' - : testKind === 'preprocessor' - ? 'Apply args to preprocessor' - : 'Apply args to inputs'} - + {#if captureInfo.active} + + {:else} + + Start capturing to test your runnables with real data. Once active, all incoming + payloads will be captured and displayed below, allowing you to test your runnables + effectively. + {/if}
- {#if displayResult && toolbarLocation === 'external'} - { - if (displayResult && typeof displayResult.openDrawer === 'function') { - displayResult.openDrawer() - } - }} - /> - {/if} -
-
- {#if isLoadingBigPayload} - - {:else if selectedCapture?.main_args} -
- { - toolbarLocation = detail - }} - /> + {#if disabled} +
+ Enter a valid configuration to start capturing.
- {:else} -
No captures to show yet.
{/if}
- - - + +
+
diff --git a/frontend/src/lib/components/triggers/CaptureTable.svelte b/frontend/src/lib/components/triggers/CaptureTable.svelte index b93c6177e7..cc784e33b6 100644 --- a/frontend/src/lib/components/triggers/CaptureTable.svelte +++ b/frontend/src/lib/components/triggers/CaptureTable.svelte @@ -30,7 +30,6 @@ export let canEdit = false export let fullHeight = true export let limitPayloadSize = false - export let noBorder = false export let captureActiveIndicator: boolean | undefined = undefined let selected: number | undefined = undefined @@ -60,7 +59,6 @@ } select: any testWithArgs: any - selectCapture: Capture | undefined }>() interface CaptureWithPayload extends Capture { @@ -143,7 +141,6 @@ const payloadData = await getPayload(capture) selected = capture.id dispatch('select', structuredClone(payloadData)) - dispatch('selectCapture', capture) } } @@ -274,7 +271,6 @@ on:error={(e) => handleError(e.detail)} on:select={(e) => handleSelect(e.detail)} bind:length={capturesLength} - {noBorder} neverShowLoader={captureActiveIndicator !== undefined} > @@ -392,7 +388,7 @@ -
No captures yet
+
No captures yet
diff --git a/frontend/src/lib/components/triggers/CaptureWrapper.svelte b/frontend/src/lib/components/triggers/CaptureWrapper.svelte index 05a59f755b..e767edce1c 100644 --- a/frontend/src/lib/components/triggers/CaptureWrapper.svelte +++ b/frontend/src/lib/components/triggers/CaptureWrapper.svelte @@ -2,55 +2,36 @@ import { workspaceStore } from '$lib/stores' import { CaptureService, type CaptureConfig, type CaptureTriggerKind } from '$lib/gen' import { onDestroy } from 'svelte' - import { isObject, sendUserToast, sleep } from '$lib/utils' - import RouteCapture from './http/RouteCapture.svelte' + import { capitalize, isObject, sendUserToast, sleep } from '$lib/utils' + import { isCloudHosted } from '$lib/cloud' + import Alert from '../common/alert/Alert.svelte' + import RouteEditorConfigSection from './http/RouteEditorConfigSection.svelte' + import WebsocketEditorConfigSection from './websocket/WebsocketEditorConfigSection.svelte' + import WebhooksConfigSection from './webhook/WebhooksConfigSection.svelte' + import EmailTriggerConfigSection from '../details/EmailTriggerConfigSection.svelte' + import KafkaTriggersConfigSection from './kafka/KafkaTriggersConfigSection.svelte' import type { ConnectionInfo } from '../common/alert/ConnectionIndicator.svelte' import type { CaptureInfo } from './CaptureSection.svelte' + import CaptureTable from './CaptureTable.svelte' + import NatsTriggersConfigSection from './nats/NatsTriggersConfigSection.svelte' + import MqttEditorConfigSection from './mqtt/MqttEditorConfigSection.svelte' + import SqsTriggerEditorConfigSection from './sqs/SqsTriggerEditorConfigSection.svelte' + import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte' import { invalidRelations } from './postgres/utils' - import WebhooksCapture from './webhook/WebhooksCapture.svelte' - import EmailTriggerCaptures from '../details/EmailTriggerCaptures.svelte' - import WebsocketCapture from './websocket/WebsocketCapture.svelte' - import PostgresCapture from './postgres/PostgresCapture.svelte' - import KafkaCapture from './kafka/KafkaCapture.svelte' - import NatsCapture from './nats/NatsCapture.svelte' - import MqttCapture from './mqtt/MqttCapture.svelte' - import SqsCapture from './sqs/SqsCapture.svelte' - import GcpCapture from './gcp/GcpCapture.svelte' + import { DEFAULT_V3_CONFIG, DEFAULT_V5_CONFIG } from './mqtt/constant' + import GcpTriggerEditorConfigSection from './gcp/GcpTriggerEditorConfigSection.svelte' - interface Props { - isFlow: boolean - path: string - hasPreprocessor: boolean - canHavePreprocessor: boolean - captureType?: CaptureTriggerKind - data?: any - connectionInfo?: ConnectionInfo | undefined - args?: Record - isValid?: boolean - triggerDeployed?: boolean - } - - let { - isFlow, - path, - hasPreprocessor, - canHavePreprocessor, - captureType = 'webhook', - data = {}, - connectionInfo = $bindable(undefined), - args = $bindable({}), - isValid = false, - triggerDeployed = false - }: Props = $props() - - let captureLoading = $state(false) - let captureActive = $state(false) - let captureConfigs: { - [key: string]: CaptureConfig - } = $state({}) - let ready = $state(false) - - const config: CaptureConfig | undefined = $derived(captureConfigs[captureType]) + export let isFlow: boolean + export let path: string + export let hasPreprocessor: boolean + export let canHavePreprocessor: boolean + export let captureType: CaptureTriggerKind = 'webhook' + export let showCapture = false + export let data: any = {} + export let connectionInfo: ConnectionInfo | undefined = undefined + export let loading = false + export let args: Record = {} + export let captureTable: CaptureTable | undefined = undefined export async function setConfig(): Promise { if (captureType === 'postgres') { @@ -58,18 +39,18 @@ sendUserToast('Table to track must be set', true) return false } - if ( invalidRelations(args.publication.table_to_track, { showError: true, trackSchemaTableError: true - }) !== '' + }) === true ) { return false } } try { - await CaptureService.setCaptureConfig({ + loading = true + args = await CaptureService.setCaptureConfig({ requestBody: { trigger_kind: captureType, path, @@ -78,19 +59,22 @@ }, workspace: $workspaceStore! }) + loading = false return true } catch (error) { + loading = false sendUserToast(error.body, true) return false } } - function isStreamingCapture() { - if (captureType === 'gcp' && args.delivery_type === 'push') { - return false - } - return ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp'].includes(captureType) - } + let captureActive = false + + let captureConfigs: { + [key: string]: CaptureConfig + } = {} + + const STREAMING_CAPTURES = ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp'] async function getCaptureConfigs() { const captureConfigsList = await CaptureService.getCaptureConfigs({ @@ -98,12 +82,10 @@ runnableKind: isFlow ? 'flow' : 'script', path }) - captureConfigs = captureConfigsList.reduce((acc, c) => { acc[c.trigger_kind] = c return acc }, {}) - if (isStreamingCapture() && captureActive) { const config = captureConfigs[captureType] if (config && config.error) { @@ -133,15 +115,30 @@ } i++ await sleep(1000) + captureTable?.loadCaptures(true) } } + let ready = false function setDefaultArgs(captureConfigs: { [key: string]: CaptureConfig }) { if (captureType in captureConfigs) { const triggerConfig = captureConfigs[captureType].trigger_config args = isObject(triggerConfig) ? triggerConfig : {} } else { - args = {} + switch (captureType) { + case 'mqtt': + //define these field so any reactive statement that may use them will not crash trying to access their property + args = { + v3_config: DEFAULT_V3_CONFIG, + v5_config: DEFAULT_V5_CONFIG, + client_version: 'v5', + subscribe_topics: [] + } + break + + default: + args = {} + } } ready = true } @@ -161,19 +158,25 @@ if (captureActive || e.detail.disableOnly) { captureActive = false } else { - try { - captureLoading = true - const configSet = await setConfig() - - if (configSet) { - capture() - } - } finally { - captureLoading = false + const configSet = await setConfig() + if (configSet) { + capture() } } } + let config: CaptureConfig | undefined + $: config = captureConfigs[captureType] + + let cloudDisabled = STREAMING_CAPTURES.includes(captureType) && isCloudHosted() + + function isStreamingCapture() { + if (captureType === 'gcp' && args.delivery_type === 'push') { + return false + } + return ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp'].includes(captureType) + } + function updateConnectionInfo(config: CaptureConfig | undefined, captureActive: boolean) { if (isStreamingCapture() && config && captureActive) { const serverEnabled = getServerEnabled(config) @@ -189,31 +192,37 @@ connectionInfo = undefined } } - $effect(() => { - updateConnectionInfo(config, captureActive) - }) + $: updateConnectionInfo(config, captureActive) - let captureInfo: CaptureInfo = $derived({ + let captureInfo: CaptureInfo + $: captureInfo = { active: captureActive, hasPreprocessor, canHavePreprocessor, isFlow, path, - connectionInfo - }) + connectionInfo, + loading: loading + } - $effect(() => { - args && (captureActive = false) - }) + $: args && (captureActive = false) {#key ready} -
- {#if captureType === 'websocket'} - + {#if cloudDisabled} + + {capitalize(captureType)} triggers are disabled in the multi-tenant cloud. + + {:else if captureType === 'websocket'} + {:else if captureType === 'postgres'} - {:else if captureType === 'webhook'} - {:else if captureType === 'http'} - {:else if captureType === 'email'} - {:else if captureType === 'kafka'} - {:else if captureType === 'nats'} - {:else if captureType === 'mqtt'} - {:else if captureType === 'sqs'} - {:else if captureType === 'gcp'} - - import Button from '$lib/components/common/button/Button.svelte' - import { Trash, Save, RotateCcw } from 'lucide-svelte' - import { type Snippet } from 'svelte' - import Toggle from '$lib/components/Toggle.svelte' - import { Tooltip } from '../meltComponents' - - interface Props { - isDraftOnly: any - hasDraft: any - saveDisabled: any - enabled: boolean | undefined - allowDraft: any - edit: any - isLoading: any - permissions: 'write' | 'create' | 'none' - isDeployed: boolean - extra?: Snippet - onDelete?: () => void - onReset?: () => void - onToggleEnabled?: (enabled: boolean) => void - onUpdate?: () => void - cloudDisabled?: boolean - triggerType?: string - } - - let { - isDraftOnly, - hasDraft, - saveDisabled, - enabled, - allowDraft, - edit, - isLoading, - permissions, - isDeployed, - extra, - onDelete, - onReset, - onToggleEnabled, - onUpdate, - cloudDisabled = false - }: Props = $props() - - const canSave = $derived((permissions === 'write' && edit) || permissions === 'create') - - -{#if !allowDraft} - {@render extra?.()} - {#if edit && enabled !== undefined} - { - onToggleEnabled?.(detail) - }} - /> - {/if} - {#if canSave} - - {/if} -{:else} -
- {#if !isDraftOnly && !hasDraft && enabled !== undefined} -
- { - onToggleEnabled?.(detail) - }} - /> -
- {/if} - {#if isDraftOnly} - - {/if} - {#if canSave && (isDraftOnly || hasDraft)} - - - - {#if !isDeployed} - Deploy the runnable to enable trigger creation - {:else if cloudDisabled} - This trigger is disabled in the multi-tenant cloud - {:else} - Enter a valid config to {isDraftOnly ? 'deploy' : 'update'} the trigger - {/if} - - - {/if} -
-{/if} diff --git a/frontend/src/lib/components/triggers/TriggerLabel.svelte b/frontend/src/lib/components/triggers/TriggerLabel.svelte deleted file mode 100644 index 5010b3fa38..0000000000 --- a/frontend/src/lib/components/triggers/TriggerLabel.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - - {label} - - -{#if trigger.isPrimary} - - Primary - -{/if} - -{#if trigger.draftConfig && !trigger.isDraft} - - Modified - -{/if} - -{#if trigger.isDraft} - - New - -{/if} diff --git a/frontend/src/lib/components/triggers/TriggersEditor.svelte b/frontend/src/lib/components/triggers/TriggersEditor.svelte index 5eeed48bec..7b4968d77c 100644 --- a/frontend/src/lib/components/triggers/TriggersEditor.svelte +++ b/frontend/src/lib/components/triggers/TriggersEditor.svelte @@ -1,348 +1,269 @@ -
- - - -
- - {#if !useVerticalTriggerBar} -
- -
- {:else} -
- - - - { - triggersState.selectedTriggerIndex = triggerIndex - }} - /> -
- {/if} - -
+ {#if !$simplifiedPoll} +
+ + Webhooks + Schedules + HTTP + WebSockets + Postgres + + Event streams + + Email + {#if isFlow} + Scheduled Poll - {#if loading} -
- {:else if triggersState.selectedTrigger} - {#key [renderCount, triggersState.selectedTriggerIndex].join('-')} -
- +
+ {#if $selectedTrigger === 'webhooks'} +
+ +
+ {:else if $selectedTrigger === 'emails'} +
+ +
+ {:else if $selectedTrigger === 'routes'} +
+ +
+ {:else if $selectedTrigger === 'websockets'} +
+ +
+ {:else if $selectedTrigger === 'postgres'} +
+ +
+ {:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats' || $selectedTrigger === 'sqs' || $selectedTrigger === 'mqtt' || $selectedTrigger === 'gcp'} +
+ + + + + + + + {#if eventStreamType === 'kafka'} + { - deleteDraftTrigger(triggersState.selectedTriggerIndex) - }} - onUpdate={(path) => { - handleUpdate(triggersState.selectedTriggerIndex, path) - }} - onConfigChange={(cfg, canSave, updated) => { - if (updated) { - handleUpdateDraftConfig(triggersState.selectedTriggerIndex, cfg, canSave) - } - }} - onCaptureConfigChange={(cfg, isValidConfig) => { - config = cfg - isValid = isValidConfig - }} - onReset={() => { - handleResetDraft(triggersState.selectedTriggerIndex) - }} - on:email-domain={({ detail }) => { - emailDomain = detail - }} + path={initialPath || fakeInitialPath} + {isFlow} + isEditor={true} + {canHavePreprocessor} + {hasPreprocessor} /> -
- {/key} - {:else} - {`Select a trigger from the ${useVerticalTriggerBar ? 'left toolbar' : 'table'} or a create a new one`} + {:else if eventStreamType === 'nats'} + + {:else if eventStreamType === 'sqs'} + + {:else if eventStreamType === 'mqtt'} + + {:else if eventStreamType === 'gcp'} + + {/if} +
+ {:else if $selectedTrigger === 'schedules'} +
+ +
+ {:else if $selectedTrigger === 'scheduledPoll'} +
+ +
{/if}
-
- - {#if !cloudDisabled && triggersState.selectedTrigger && triggersState.selectedTrigger.type !== 'schedule' && triggersState.selectedTrigger.type != 'poll' && !noCapture} - {@const captureKind = triggersState.selectedTrigger - ? triggerTypeToCaptureKind(triggersState.selectedTrigger.type) - : undefined} - {#if captureKind} - {#key captureKind} - - - - {/key} - {/if} - {/if} - - -
+ + +
+ {:else} +
+ +
+ {/if} +
diff --git a/frontend/src/lib/components/triggers/TriggersEditorSection.svelte b/frontend/src/lib/components/triggers/TriggersEditorSection.svelte new file mode 100644 index 0000000000..c6e1f0ba2d --- /dev/null +++ b/frontend/src/lib/components/triggers/TriggersEditorSection.svelte @@ -0,0 +1,135 @@ + + +
+ + {#if !collapsed || alwaysOpened} +
+ {#if isEditor} + + {/if} + + {#if !noSave} + {@const disabled = newItem || cloudDisabled} + + + + {#if disabled} + {#if newItem} + Deploy the runnable to enable trigger creation + {:else if cloudDisabled} + {capitalize(triggerType)} triggers are disabled in the multi-tenant cloud + {/if} + {:else} + Create new {captureTypeLabels[triggerType].toLowerCase()} + {/if} + + + {/if} +
+ {/if} +
+ + {#if isEditor} + + {:else} + + {/if} +
diff --git a/frontend/src/lib/components/triggers/TriggersTable.svelte b/frontend/src/lib/components/triggers/TriggersTable.svelte deleted file mode 100644 index 3d185ace89..0000000000 --- a/frontend/src/lib/components/triggers/TriggersTable.svelte +++ /dev/null @@ -1,139 +0,0 @@ - - -
-
- - - -
- - - {#each triggers as trigger, index} - {@const SvelteComponent = triggerIconMap[trigger.type]} - onSelect?.(index)} - > - -
- - - {#if trigger.isPrimary} - - {/if} -
- - -
-
- - {#if trigger.type === 'webhook' && webhookToken} - - {`${webhookToken} token${webhookToken > 1 ? 's' : ''}`} - - {:else if trigger.type === 'email' && emailToken} - - {`${emailToken} token${emailToken > 1 ? 's' : ''}`} - - {/if} -
- - {#if !['email', 'webhook', 'cli'].includes(trigger.type)} - {#if trigger.isDraft} -
- - - {/each} - - {#if !loading && triggers.length === 0} - - No triggers found - - {/if} - {#if loading && triggers.length === 0} - - -
- - Loading triggers... -
- - - {/if} - -
-
diff --git a/frontend/src/lib/components/triggers/TriggersWrapper.svelte b/frontend/src/lib/components/triggers/TriggersWrapper.svelte index 7ff6c88f8a..7c971f46c1 100644 --- a/frontend/src/lib/components/triggers/TriggersWrapper.svelte +++ b/frontend/src/lib/components/triggers/TriggersWrapper.svelte @@ -1,173 +1,117 @@ -{#if selectedTrigger.type === 'http'} - -{:else if selectedTrigger.type === 'webhook'} - -{:else if selectedTrigger.type === 'email'} - -{:else if selectedTrigger.type === 'schedule'} - -{:else if selectedTrigger.type === 'websocket'} - -{:else if selectedTrigger.type === 'kafka'} - -{:else if selectedTrigger.type === 'postgres'} - -{:else if selectedTrigger.type === 'nats'} - -{:else if selectedTrigger.type === 'mqtt'} - -{:else if selectedTrigger.type === 'sqs'} - -{:else if selectedTrigger.type === 'gcp'} - -{:else if selectedTrigger.type === 'poll'} - -{:else if selectedTrigger.type === 'cli'} -
- - -
-{/if} - -{#snippet customLabel()} - {@const IconComponent = triggerIconMap[selectedTrigger.type]} -
- + {#if cloudDisabled} + + {capitalize(triggerType)} triggers are disabled in the multi-tenant cloud. + + {:else if triggerType === 'websocket'} + - -
-{/snippet} + {:else if triggerType === 'postgres'} + + {:else if triggerType === 'webhook'} + + {:else if triggerType === 'http'} + + {:else if triggerType === 'email'} + + {:else if triggerType === 'kafka'} + + {:else if triggerType === 'nats'} + + {:else if triggerType === 'mqtt'} + + {:else if triggerType === 'sqs'} + + {:else if triggerType === 'gcp'} + + {/if} +
diff --git a/frontend/src/lib/components/triggers/gcp/GcpCapture.svelte b/frontend/src/lib/components/triggers/gcp/GcpCapture.svelte deleted file mode 100644 index 7538026782..0000000000 --- a/frontend/src/lib/components/triggers/gcp/GcpCapture.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} -

- Listening to GCP Pub/Sub events... -

- {:else} -

- Start capturing to listen to GCP Pub/Sub events. -

- {/if} - {/snippet} - - {#if deliveryType === 'push'} - {@const captureUrl = getCaptureUrl(captureInfo)} - - {/if} -
-{/if} diff --git a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditor.svelte b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditor.svelte index 2a7c31512a..1b2f8cf389 100644 --- a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditor.svelte +++ b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditor.svelte @@ -2,9 +2,7 @@ import { tick } from 'svelte' import GcpTriggerEditorInner from './GcpTriggerEditorInner.svelte' - let { onUpdate }: { onUpdate?: (path?: string) => void } = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() @@ -14,17 +12,16 @@ export async function openNew( is_flow: boolean, initial_script_path?: string, - defaultValues?: Record, - newDraft?: boolean + defaultValues?: Record ) { open = true await tick() drawer?.openNew(is_flow, initial_script_path, defaultValues) } - let drawer: GcpTriggerEditorInner | undefined = $state() + let drawer: GcpTriggerEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte index 0fff25fab4..816dffd90f 100644 --- a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte @@ -1,8 +1,10 @@
-
- - {#if showTestingBadge} - + {#if showCapture && captureInfo} + {@const captureURL = `${base_endpoint}/${path}`} + + {#if delivery_type === 'push'} + {/if} - + + {/if} +
@@ -191,7 +208,7 @@ {#if subscription_mode === 'create_update'}
{#if delivery_type === 'push' && delivery_config}
-
- -
+ +
+ + +
+

Enable Google Cloud authentication for push delivery using a verified token. If the subscription uses push delivery, its endpoint URL must - match the following format: {`${base_endpoint}/${path}`}/*, - meaning it must start with - {`${base_endpoint}/${path}`} followed by any path segment. + match the following format: {base_endpoint}/*, meaning it must + start with + {base_endpoint} followed by any path segment.

{/if} diff --git a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte index 3c1f7fd5d7..993fc6611a 100644 --- a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte @@ -5,8 +5,10 @@ import Path from '$lib/components/Path.svelte' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, sendUserToast } from '$lib/utils' - import { Loader2 } from 'lucide-svelte' + import { createEventDispatcher } from 'svelte' + import { Loader2, Save } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' + import Toggle from '$lib/components/Toggle.svelte' import { GcpTriggerService, type DeliveryType, @@ -18,83 +20,34 @@ import Required from '$lib/components/Required.svelte' import GcpTriggerEditorConfigSection from './GcpTriggerEditorConfigSection.svelte' import { base } from '$app/paths' - import type { Snippet } from 'svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import { saveGcpTriggerFromCfg } from './utils' - import { handleConfigChange } from '../utils' - let drawer: Drawer | undefined = $state(undefined) - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let delivery_type: DeliveryType = $state('pull') - let itemKind: 'flow' | 'script' = $state('script') - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path: string = $state('') - let pathError = $state('') - let enabled = $state(false) - let dirtyPath = $state(false) - let can_write = $state(true) - let drawerLoading = $state(true) - let topic_id: string = $state('') - let gcp_resource_path: string = $state('') - let subscription_id: string = $state('') - let isValid = $state(false) - let delivery_config: PushConfig | undefined = $state(undefined) - let subscription_mode: SubscriptionMode = $state('create_update') - let initialConfig: Record | undefined = undefined - let deploymentLoading = $state(false) - let base_endpoint = $derived(`${window.location.origin}${base}`) + let is_loading = false + let drawer: Drawer + let is_flow: boolean = false + let initialPath = '' + let edit = true + let delivery_type: DeliveryType = 'pull' + let itemKind: 'flow' | 'script' = 'script' + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let enabled = false + let dirtyPath = false + let can_write = true + let drawerLoading = true + let topic_id: string = '' + let gcp_resource_path: string = '' + let subscription_id: string = '' + let isValid = false + let delivery_config: PushConfig | undefined = undefined + let subscription_mode: SubscriptionMode = 'create_update' + const dispatch = createEventDispatcher() - let { - useDrawer = true, - description = undefined, - hideTarget = false, - hideTooltips = false, - isEditor = false, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - isDeployed = false, - customLabel = undefined, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined, - cloudDisabled = false - }: { - useDrawer?: boolean - description?: Snippet | undefined - hideTarget?: boolean - hideTooltips?: boolean - isEditor?: boolean - allowDraft?: boolean - hasDraft?: boolean - isDraftOnly?: boolean - isDeployed?: boolean - customLabel?: Snippet - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void - onCaptureConfigChange?: (cfg: Record, isValid: boolean) => void - onUpdate?: (path?: string) => void - onDelete?: () => void - onReset?: () => void - cloudDisabled?: boolean - } = $props() + $: is_flow = itemKind === 'flow' - const gcpConfig = $derived.by(getGcpConfig) - const saveDisabled = $derived( - pathError != '' || emptyString(script_path) || !isValid || !can_write - ) - const captureConfig = $derived.by(isEditor ? getGcpCaptureConfig : () => ({})) - - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultValues?: Record - ) { + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() @@ -102,14 +55,11 @@ itemKind = isFlow ? 'flow' : 'script' edit = true dirtyPath = false - await loadTrigger(defaultValues) + await loadTrigger() } catch (err) { sendUserToast(`Could not load GCP Pub/Sub trigger: ${err.body}`, true) } finally { drawerLoading = false - if (!defaultValues) { - initialConfig = structuredClone($state.snapshot(getGcpConfig())) - } } } @@ -132,183 +82,144 @@ subscription_id = '' topic_id = defaultValues?.topic_id subscription_mode = defaultValues?.subscription_mode ?? 'create_update' - path = defaultValues?.path ?? '' + path = '' initialPath = '' edit = false dirtyPath = false - enabled = defaultValues?.enabled ?? false } finally { drawerLoading = false } } - async function loadTrigger(defaultConfig?: Record): Promise { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - return - } else { - try { - const s = await GcpTriggerService.getGcpTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - loadTriggerConfig(s) - } catch (error) { - sendUserToast(`Could not load GCP Pub/Sub trigger: ${error.body}`, true) - } + async function loadTrigger(): Promise { + try { + const s = await GcpTriggerService.getGcpTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + script_path = s.script_path + initialScriptPath = s.script_path + gcp_resource_path = s.gcp_resource_path + delivery_type = s.delivery_type + subscription_id = s.subscription_id + delivery_config = s.delivery_config + subscription_mode = s.subscription_mode + is_flow = s.is_flow + path = s.path + enabled = s.enabled + topic_id = s.topic_id + can_write = canWrite(s.path, s.extra_perms, $userStore) + } catch (error) { + sendUserToast(`Could not load GCP Pub/Sub trigger: ${error.body}`, true) } } - async function loadTriggerConfig(cfg?: Record): Promise { - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - gcp_resource_path = cfg?.gcp_resource_path - delivery_type = cfg?.delivery_type - subscription_id = cfg?.subscription_id - delivery_config = cfg?.delivery_config - subscription_mode = cfg?.subscription_mode - is_flow = cfg?.is_flow - path = cfg?.path - enabled = cfg?.enabled - topic_id = cfg?.topic_id - can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore) - } - async function updateTrigger(): Promise { - deploymentLoading = true - const cfg = gcpConfig - if (!cfg) { - return - } - const isSaved = await saveGcpTriggerFromCfg( - initialPath, - cfg, - edit, - $workspaceStore!, - usedTriggerKinds - ) - if (isSaved) { - onUpdate?.(cfg.path) - drawer?.closeDrawer() - } - deploymentLoading = false - } + try { + is_loading = true + const base_endpoint = `${window.location.origin}${base}` + if (delivery_type === 'push') { + if (!delivery_config) { + sendUserToast('Must set route path when delivery type is push', true) + return + } + } else { + delivery_config = undefined + } + if (edit) { + await GcpTriggerService.updateGcpTrigger({ + workspace: $workspaceStore!, + path: initialPath, + requestBody: { + gcp_resource_path, + subscription_mode, + subscription_id, + delivery_type, + delivery_config, + base_endpoint, + topic_id, + path, + script_path, + enabled, + is_flow + } + }) + sendUserToast(`GCP Pub/Sub trigger ${path} updated`) + } else { + await GcpTriggerService.createGcpTrigger({ + workspace: $workspaceStore!, + requestBody: { + gcp_resource_path, + subscription_mode, + subscription_id, + delivery_type, + delivery_config, + base_endpoint, + topic_id, + path, + script_path, + enabled: true, + is_flow + } + }) + sendUserToast(`GCP Pub/Sub trigger ${path} created`) + } - function getGcpConfig() { - return { - gcp_resource_path, - subscription_mode, - subscription_id, - delivery_type, - delivery_config, - base_endpoint, - topic_id, - path, - script_path, - enabled, - is_flow + if (!$usedTriggerKinds.includes('gcp')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'gcp'] + } + dispatch('update') + drawer.closeDrawer() + is_loading = false + } catch (error) { + is_loading = false + sendUserToast(error.body, true) } } - - function getGcpCaptureConfig() { - return { - gcp_resource_path, - subscription_mode, - subscription_id, - delivery_type, - delivery_config, - base_endpoint, - topic_id, - path - } - } - - async function handleToggleEnabled(toggleEnabled: boolean) { - enabled = toggleEnabled - if (!isDraftOnly && !hasDraft) { - await GcpTriggerService.setGcpTriggerEnabled({ - path: initialPath, - workspace: $workspaceStore ?? '', - requestBody: { enabled: toggleEnabled } - }) - sendUserToast(`${toggleEnabled ? 'enabled' : 'disabled'} GCP Pub/Sub trigger ${initialPath}`) - } - } - - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid) - }) - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(gcpConfig, initialConfig, saveDisabled, edit, onConfigChange) - } - }) -{#if useDrawer} - - - - {@render actionsButtons()} - - {@render config()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} + + + + {#if !drawerLoading && can_write} + {#if edit} +
+ { + sendUserToast( + `${e.detail ? 'enabled' : 'disabled'} GCP Pub/Sub trigger ${initialPath}` + ) + }} + /> +
+ {/if} + {/if}
- - {@render actionsButtons()} - - {@render config()} -
-{/if} - -{#snippet actionsButtons()} - {#if !drawerLoading && can_write} - - {/if} -{/snippet} - -{#snippet config()} - {#if drawerLoading} -
- -

Loading...

-
- {:else} -
- {#if description} - {@render description()} - {/if} - {#if !hideTooltips} + {#if drawerLoading} +
+ +

Loading...

+
+ {:else} +
{#if edit} Changes can take up to 30 seconds to take effect. @@ -316,26 +227,23 @@ New GCP Pub/Sub trigger can take up to 30 seconds to start listening. {/if} - {/if} -
-
-
-
+
+
+ +
- {#if !hideTarget}

Pick a script or flow to be triggered @@ -348,38 +256,35 @@ allowFlow={true} bind:itemKind bind:scriptPath={script_path} - allowRefresh={can_write} - allowEdit={!$userStore?.operator} + allowRefresh /> {#if emptyString(script_path)} {/if}

- {/if} - -
- {/if} -{/snippet} + +
+ {/if} + + diff --git a/frontend/src/lib/components/triggers/gcp/GcpTriggerPanel.svelte b/frontend/src/lib/components/triggers/gcp/GcpTriggerPanel.svelte index 348a3a17b8..fe9c587c1b 100644 --- a/frontend/src/lib/components/triggers/gcp/GcpTriggerPanel.svelte +++ b/frontend/src/lib/components/triggers/gcp/GcpTriggerPanel.svelte @@ -1,68 +1,131 @@ + { + loadTriggers() + }} + bind:this={gcpTriggerEditor} +/> + {#if !$enterpriseLicense} GCP Pub/Sub triggers are an enterprise only feature. +{:else if isCloudHosted()} + + GCP Pub/Sub triggers are disabled in the multi-tenant cloud. + {:else}
- - {#snippet description()} - {#if cloudDisabled} - - GCP Pub/Sub triggers are disabled in the multi-tenant cloud. - - {:else} - - GCP Pub/Sub triggers execute scripts and flows in response to messages published to - Google Cloud Pub/Sub topics. - - {/if} - {/snippet} - + + GCP Pub/Sub triggers allow your scripts or flows to process messages from Google Cloud + Pub/Sub in real time. Each trigger listens to a Pub/Sub subscription and executes a script or + a flow when new messages are published to the corresponding topic. + + + {#if !newItem && gcpTriggers && gcpTriggers.length > 0} +
+
+
+ {#each gcpTriggers as gcpTrigger (gcpTrigger.path)} +
+
{gcpTrigger.path}
+ +
+ +
+
+ {/each} +
+
+
+ {/if} + + { + gcpTriggerEditor?.openNew(isFlow, path, e.detail.config) + }} + on:addPreprocessor + on:updateSchema + on:testWithArgs + cloudDisabled={false} + triggerType="gcp" + {isFlow} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + bind:showCapture={dontCloseOnLoad} + />
{/if} diff --git a/frontend/src/lib/components/triggers/gcp/utils.ts b/frontend/src/lib/components/triggers/gcp/utils.ts deleted file mode 100644 index e2bebce6cd..0000000000 --- a/frontend/src/lib/components/triggers/gcp/utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GcpTriggerService } from '$lib/gen' -import { sendUserToast } from '$lib/toast' -import { get, type Writable } from 'svelte/store' - -export async function saveGcpTriggerFromCfg( - initialPath: string, - cfg: Record, - edit: boolean, - workspace: string, - usedTriggerKinds: Writable -): Promise { - try { - const requestBody = { - gcp_resource_path: cfg.gcp_resource_path, - subscription_mode: cfg.subscription_mode, - subscription_id: cfg.subscription_id, - delivery_type: cfg.delivery_type, - delivery_config: cfg.delivery_config, - base_endpoint: cfg.base_endpoint, - topic_id: cfg.topic_id, - path: cfg.path, - script_path: cfg.script_path, - enabled: cfg.enabled, - is_flow: cfg.is_flow - } - if (edit) { - await GcpTriggerService.updateGcpTrigger({ - workspace, - path: initialPath, - requestBody - }) - sendUserToast(`GCP Pub/Sub trigger ${cfg.path} updated`) - } else { - await GcpTriggerService.createGcpTrigger({ - workspace: workspace, - requestBody: { - ...requestBody, - enabled: true - } - }) - sendUserToast(`GCP Pub/Sub trigger ${cfg.path} created`) - } - - if (!get(usedTriggerKinds).includes('gcp')) { - usedTriggerKinds.update((t) => [...t, 'gcp']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/components/triggers/http/RouteBodyTransformerOption.svelte b/frontend/src/lib/components/triggers/http/RouteBodyTransformerOption.svelte index 1fa781d089..d9f129851f 100644 --- a/frontend/src/lib/components/triggers/http/RouteBodyTransformerOption.svelte +++ b/frontend/src/lib/components/triggers/http/RouteBodyTransformerOption.svelte @@ -2,25 +2,13 @@ import Label from '$lib/components/Label.svelte' import Toggle from '$lib/components/Toggle.svelte' import Tooltip from '$lib/components/Tooltip.svelte' - import type { Snippet } from 'svelte' - interface Props { - raw_string: boolean - wrap_body: boolean - disabled?: boolean - testingBadge?: Snippet | undefined - } - - let { - raw_string = $bindable(), - wrap_body = $bindable(), - disabled = false, - testingBadge = undefined - }: Props = $props() + export let raw_string: boolean + export let wrap_body: boolean diff --git a/frontend/src/lib/components/triggers/http/RouteCapture.svelte b/frontend/src/lib/components/triggers/http/RouteCapture.svelte deleted file mode 100644 index 983c6f3cd3..0000000000 --- a/frontend/src/lib/components/triggers/http/RouteCapture.svelte +++ /dev/null @@ -1,87 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} -

- Send a POST request to the URL below to simulate a http event. -

- {:else} -

- Start capturing to listen to HTTP requests on this test URL. -

- {/if} - {/snippet} - - - - -
-{/if} diff --git a/frontend/src/lib/components/triggers/http/RouteEditor.svelte b/frontend/src/lib/components/triggers/http/RouteEditor.svelte index 00914e8172..ce093aa976 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditor.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditor.svelte @@ -2,13 +2,7 @@ import { tick } from 'svelte' import RouteEditorInner from './RouteEditorInner.svelte' - interface Props { - onUpdate?: (cfg?: Record) => void - } - - let { onUpdate = undefined }: Props = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() @@ -25,9 +19,13 @@ drawer?.openNew(is_flow, initial_script_path, defaultValues) } - let drawer: RouteEditorInner | undefined = $state() + export async function getTriggers() { + return drawer?.getTriggers() + } + + let drawer: RouteEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte index ae51547ef7..0c523decd5 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte @@ -1,16 +1,25 @@
+ {#if showCapture && captureInfo} + {@const captureURL = `${location.origin}${base}/api/w/${$workspaceStore}/capture_u/http/${ + captureInfo.isFlow ? 'flow' : 'script' + }/${captureInfo.path.replaceAll('/', '.')}/${route_path}`} + {@const cleanedRunnableArgs = + isObject(runnableArgs) && 'wm_trigger' in runnableArgs + ? Object.fromEntries(Object.entries(runnableArgs).filter(([key]) => key !== 'wm_trigger')) + : runnableArgs} + + + + + + {/if}
- - {#if showTestingBadge} - - {/if} - - {#if !userCanEditConfig && isDraftOnly} - + {#if !($userStore?.is_admin || $userStore?.is_super_admin)} + Route endpoints can only be edited by workspace admins
@@ -104,7 +144,7 @@ type="text" autocomplete="off" bind:value={route_path} - disabled={!userCanEditConfig || !can_write} + disabled={!($userStore?.is_admin || $userStore?.is_super_admin) || !can_write} class={routeError === '' ? '' : 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'} @@ -118,28 +158,44 @@ - - - - - + + + + + -
- - +
+
+ + Full endpoint + + { + currentTarget.select() + }} + /> +
{dirtyRoutePath ? routeError : ''}
- {#if !isCloudHosted()} + {#if !capture_mode && !isCloudHosted()}
{ workspaced_route = !workspaced_route dirtyRoutePath = true @@ -155,6 +211,9 @@
{/if}
+ {#if capture_mode} + + {/if}
diff --git a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte index 59661b89d9..427540b288 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte @@ -8,8 +8,9 @@ import { HttpTriggerService, VariableService, type AuthenticationMethod } from '$lib/gen' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, sendUserToast } from '$lib/utils' + import { createEventDispatcher } from 'svelte' import Section from '$lib/components/Section.svelte' - import { Loader2, Pipette, Plus } from 'lucide-svelte' + import { Loader2, Save, Pipette, Plus } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' import VariableEditor from '../../VariableEditor.svelte' import { json } from 'svelte-highlight/languages' @@ -26,94 +27,42 @@ import ResourcePicker from '$lib/components/ResourcePicker.svelte' import ItemPicker from '../../ItemPicker.svelte' import { Popover } from '$lib/components/meltComponents' - import { HUB_SCRIPT_ID, saveHttpRouteFromCfg, SECRET_KEY_PATH } from './utils' + import { HUB_SCRIPT_ID, SECRET_KEY_PATH } from './utils' import { HubFlow } from '$lib/hub' import RouteBodyTransformerOption from './RouteBodyTransformerOption.svelte' - import TestingBadge from '../testingBadge.svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import { handleConfigChange } from '../utils' + let is_flow: boolean = false + let initialPath = '' + let edit = true - let { - useDrawer = true, - hideTarget = false, - description = undefined, - isEditor = false, - customLabel = undefined, - isDraftOnly = false, - allowDraft = false, - hasDraft = false, - isDeployed = false, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined - } = $props() + let itemKind: 'flow' | 'script' = 'script' - // Form data state - let initialPath = $state('') - let edit = $state(true) - let itemKind = $state<'flow' | 'script'>('script') - let is_flow = $state(false) - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path = $state('') - let pathError = $state('') - let isValid = $state(false) - let dirtyRoutePath = $state(false) - let dirtyPath = $state(false) - let is_async = $state(false) - let authentication_method = $state('none') - let route_path = $state('') - let http_method = $state<'get' | 'post' | 'put' | 'patch' | 'delete'>('post') - let static_asset_config = $state<{ s3: string; storage?: string; filename?: string } | undefined>( + $: is_flow = itemKind === 'flow' + + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let isValid = false + let dirtyRoutePath = false + let is_async = false + let authentication_method: AuthenticationMethod = 'none' + let route_path = '' + let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post' + let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined = undefined - ) - let is_static_website = $state(false) - let s3FileUploadRawMode = $state(false) - let workspaced_route = $state(false) - let raw_string = $state(false) - let wrap_body = $state(false) - let drawerLoading = $state(true) - let showLoader = $state(false) - let authentication_resource_path = $state('') - let variable_path = $state('') - let signature_options_type = $state<'custom_script' | 'custom_signature'>('custom_signature') - let can_write = $state(true) - let extraPerms = $state | undefined>(undefined) - - // Component references - let s3FilePicker = $state(null) - let s3Editor = $state(null) - let variablePicker = $state(null) - let variableEditor = $state(null) - let drawer = $state(null) - let initialConfig: Record | undefined = undefined - let deploymentLoading = $state(false) - - const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin) - const routeConfig = $derived.by(getRouteConfig) - const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({})) - const saveDisabled = $derived( - drawerLoading || - !can_write || - pathError != '' || - !isValid || - (!static_asset_config && emptyString(script_path)) - ) - - $effect(() => { - is_flow = itemKind === 'flow' - }) - - // Update is_static_website based on static_asset_config - $effect(() => { - if (!static_asset_config) { - is_static_website = false - } - }) - + let is_static_website: boolean = false + let s3FilePicker: S3FilePicker + let s3FileUploadRawMode = false + let s3Editor: SimpleEditor | undefined = undefined + let workspaced_route: boolean = false + let raw_string = false + let wrap_body = false + let drawerLoading = true + let authentication_resource_path: string = '' + let variablePicker: ItemPicker + let variableEditor: VariableEditor + let variable_path: string = '' type AuthenticationOption = { label: string value: AuthenticationMethod @@ -125,6 +74,8 @@ return await VariableService.listVariable({ workspace: $workspaceStore ?? '' }) } + let signature_options_type: 'custom_script' | 'custom_signature' = 'custom_signature' + const authentication_options: AuthenticationOption[] = [ { label: 'No Auth', @@ -157,34 +108,20 @@ } ] - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultConfig?: Record - ) { + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true - let loader = setTimeout(() => { - showLoader = true - }, 100) // if loading takes less than 100ms, we don't show the loader try { drawer?.openDrawer() initialPath = ePath - path = ePath itemKind = isFlow ? 'flow' : 'script' edit = true dirtyPath = false dirtyRoutePath = false - await loadTrigger(defaultConfig) + await loadTrigger() } catch (err) { sendUserToast(`Could not load route: ${err}`, true) } finally { - if (!defaultConfig) { - // If the route is loaded from the backend, we to set the initial config - initialConfig = structuredClone($state.snapshot(getRouteConfig())) - } - clearTimeout(loader) drawerLoading = false - showLoader = false } } @@ -194,9 +131,6 @@ defaultValues?: Record ) { drawerLoading = true - let loader = setTimeout(() => { - showLoader = true - }, 100) // if loading takes less than 100ms, we don't show the loader try { drawer?.openDrawer() is_flow = nis_flow @@ -212,7 +146,7 @@ script_path = fixedScriptPath static_asset_config = undefined s3FileUploadRawMode = false - path = defaultValues?.path ?? '' + path = '' initialPath = '' dirtyPath = false is_static_website = false @@ -223,73 +157,47 @@ raw_string = defaultValues?.raw_string ?? false wrap_body = defaultValues?.wrap_body ?? false } finally { - clearTimeout(loader) drawerLoading = false - showLoader = false } } - function loadTriggerConfig(cfg?: Record): void { - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - is_flow = cfg?.is_flow - path = cfg?.path - route_path = cfg?.route_path - http_method = cfg?.http_method ?? 'post' - is_async = cfg?.is_async - workspaced_route = cfg?.workspaced_route - wrap_body = cfg?.wrap_body - raw_string = cfg?.raw_string - authentication_resource_path = cfg?.authentication_resource_path ?? '' - if (cfg?.authentication_method === 'custom_script') { + const dispatch = createEventDispatcher() + + let can_write = true + async function loadTrigger(): Promise { + const s = await HttpTriggerService.getHttpTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + + script_path = s.script_path + initialScriptPath = s.script_path + is_flow = s.is_flow + path = s.path + route_path = s.route_path + http_method = s.http_method ?? 'post' + is_async = s.is_async + workspaced_route = s.workspaced_route + wrap_body = s.wrap_body + raw_string = s.raw_string + authentication_resource_path = s.authentication_resource_path ?? '' + if (s.authentication_method === 'custom_script') { authentication_method = 'signature' signature_options_type = 'custom_script' } else { - authentication_method = cfg?.authentication_method + authentication_method = s.authentication_method signature_options_type = 'custom_signature' } if (!isCloudHosted()) { - static_asset_config = cfg?.static_asset_config - s3FileUploadRawMode = !!cfg?.static_asset_config - is_static_website = cfg?.is_static_website + static_asset_config = s.static_asset_config + s3FileUploadRawMode = !!static_asset_config + is_static_website = s.is_static_website } - extraPerms = cfg?.extra_perms - can_write = canWrite(path, cfg?.extra_perms, $userStore) - } - async function loadTrigger(defaultConfig?: Record): Promise { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - return - } else { - const s = await HttpTriggerService.getHttpTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - - loadTriggerConfig(s) - } + can_write = canWrite(s.path, s.extra_perms, $userStore) } async function triggerScript(): Promise { - deploymentLoading = true - const saveCfg = routeConfig - const isSaved = await saveHttpRouteFromCfg( - initialPath, - saveCfg, - edit, - $workspaceStore!, - !!$userStore?.is_admin || !!$userStore?.is_super_admin, - usedTriggerKinds - ) - if (isSaved) { - onUpdate(saveCfg.path) - drawer?.closeDrawer() - } - deploymentLoading = false - } - - function getRouteConfig(): Record { // If the user selects "signature" with the "custom_script" option, // we explicitly set the authentication method to "custom_script" // (which is a valid enum on its own in the backend) @@ -297,47 +205,60 @@ authentication_method === 'signature' && signature_options_type === 'custom_script' ? 'custom_script' : authentication_method - const nCfg = { - script_path, - is_flow, - path, - route_path, - http_method, - is_async, - workspaced_route, - wrap_body, - raw_string, - authentication_resource_path, - authentication_method: auth_method, - static_asset_config, - is_static_website, - extra_perms: extraPerms + + if (edit) { + await HttpTriggerService.updateHttpTrigger({ + workspace: $workspaceStore!, + path: initialPath, + requestBody: { + path, + script_path, + is_flow, + is_async, + authentication_method: auth_method, + route_path: $userStore?.is_admin || $userStore?.is_super_admin ? route_path : undefined, + http_method, + static_asset_config, + is_static_website, + workspaced_route, + authentication_resource_path, + wrap_body, + raw_string + } + }) + sendUserToast(`Route ${path} updated`) + } else { + await HttpTriggerService.createHttpTrigger({ + workspace: $workspaceStore!, + requestBody: { + path, + script_path, + is_flow, + is_async, + authentication_method: auth_method, + route_path, + http_method, + static_asset_config, + is_static_website, + workspaced_route, + authentication_resource_path, + wrap_body, + raw_string + } + }) + sendUserToast(`Route ${path} created`) } - return nCfg + if (!$usedTriggerKinds.includes('http')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'http'] + } + dispatch('update') + drawer.closeDrawer() } + let drawer: Drawer - // Update config for captures - function getCaptureConfig() { - const newCaptureConfig = { - route_path: routeConfig.route_path, - http_method: routeConfig.http_method, - raw_string: routeConfig.raw_string, - wrap_body: routeConfig.wrap_body, - path: routeConfig.path - } - // - return newCaptureConfig - } + let dirtyPath = false - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid) - }) - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(routeConfig, initialConfig, saveDisabled, edit, onConfigChange) - } - }) + $: !static_asset_config && (is_static_website = false) {#if static_asset_config} @@ -353,30 +274,46 @@ /> {/if} -{#snippet config()} - {#if drawerLoading} - {#if showLoader} + + + + {#if !drawerLoading && can_write} + + {/if} + + {#if drawerLoading} - {/if} - {:else} -
-
- -
+ {:else} +
+
+ +
- {#if !hideTarget}
{#if !isCloudHosted()} - - - + + + {/if} @@ -435,7 +371,13 @@ {#if s3FileUploadRawMode} {#if can_write} { + dispatch('focus') + }} + on:blur={(e) => { + dispatch('blur') + }} code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)} bind:value={static_asset_config} /> @@ -514,257 +456,183 @@
{/if} - {/if} - + - {#if !is_static_website} -
-
- {#if !static_asset_config} -
- -
- {/if} - - - {#each authentication_options as option} - {#if option.resource_type && authentication_method === option.value} - - {/if} - {/each} - - {#if authentication_method === 'signature'} - {#if signature_options_type === 'custom_signature'} - - {:else if signature_options_type === 'custom_script'} -

- Pick a secret variable or create one which will be used as a secret key for your - custom script/flow
-

-
-
- - -
- + {#if !is_static_website} +
+
+ {#if !static_asset_config} +
+
{/if} - {/if} + - -
-
- {/if} -
- {/if} -{/snippet} + {#each authentication_options as option} + {#if option.resource_type && authentication_method === option.value} + + {/if} + {/each} -{#snippet testingBadge()} - {#if isEditor} - - {/if} -{/snippet} + {#if authentication_method === 'signature'} + {#if signature_options_type === 'custom_signature'} + + {:else if signature_options_type === 'custom_script'} +

+ Pick a secret variable or create one which will be used as a secret key for your + custom script/flow
+

+
+
+ + +
+ +
+ {/if} + {/if} -{#snippet saveButton()} - {#if !drawerLoading} - - {/if} -{/snippet} - -{#if useDrawer} - - drawer?.closeDrawer()} - > - - {@render saveButton()} - - {@render config()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} - {/if} - - - {@render saveButton()} - - {#if description} - {@render description()} + {#if !static_asset_config} + + {/if} +
+
+ {/if} +
{/if} - {@render config()} - -{/if} +
+
variableEditor?.editVariable(x) }} + buttons={{ 'Edit/View': (x) => variableEditor.editVariable(x) }} >
- variablePicker?.openDrawer()} /> + diff --git a/frontend/src/lib/components/triggers/http/RoutesPanel.svelte b/frontend/src/lib/components/triggers/http/RoutesPanel.svelte index 3e34e3a56d..4c215022d4 100644 --- a/frontend/src/lib/components/triggers/http/RoutesPanel.svelte +++ b/frontend/src/lib/components/triggers/http/RoutesPanel.svelte @@ -1,57 +1,123 @@ - { + loadTriggers() + }} bind:this={routeEditor} - hideTarget - {isEditor} - {customLabel} - isDraftOnly={selectedTrigger.isDraft} - allowDraft - hasDraft={!!selectedTrigger.draftConfig} - {...restProps} -> - {#snippet description()} -
- Routes expose your scripts and flows as HTTP endpoints. Each route can be configured with a - specific HTTP method and path. +/> - {#if !$userStore?.is_admin && !$userStore?.is_super_admin && selectedTrigger.isDraft} - +
+ + Routes expose your scripts and flows as HTTP endpoints. Each route can be configured with a + specific HTTP method and path. + + {#if !newItem && httpTriggers && httpTriggers.length > 0} +
+ {#if !$userStore?.is_admin && !$userStore?.is_super_admin} + {/if} -
- {/snippet} - + +
+ {#each httpTriggers as httpTriggers (httpTriggers.path)} +
+
{httpTriggers.path}
+
+ {httpTriggers.http_method.toUpperCase()} /{httpTriggers.route_path} +
+
+ +
+
+ {/each} +
+ + {/if} + + { + routeEditor?.openNew(isFlow, path, e.detail.config) + }} + on:applyArgs + on:addPreprocessor + on:updateSchema + on:testWithArgs + bind:showCapture={dontCloseOnLoad} + cloudDisabled={false} + triggerType="http" + {isFlow} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + data={{ args }} + bind:openForm + /> +
diff --git a/frontend/src/lib/components/triggers/http/utils.ts b/frontend/src/lib/components/triggers/http/utils.ts index 04734b2c81..4deb21fc4c 100644 --- a/frontend/src/lib/components/triggers/http/utils.ts +++ b/frontend/src/lib/components/triggers/http/utils.ts @@ -1,9 +1,5 @@ import { base } from '$lib/base' import { isCloudHosted } from '$lib/cloud' -import { HttpTriggerService } from '$lib/gen/services.gen' -import { sendUserToast } from '$lib/toast' -import type { Writable } from 'svelte/store' -import { get } from 'svelte/store' export const SECRET_KEY_PATH = 'secret_key_path' export const HUB_SCRIPT_ID = 19670 @@ -29,50 +25,3 @@ export function replacePlaceholderForSignatureScriptTemplate(content: string) { `$1${secret_key_path}$2` ) } - -export async function saveHttpRouteFromCfg( - initialPath: string, - routeCfg: Record, - edit: boolean, - workspace: string, - isAdmin: boolean, - usedTriggerKinds: Writable -): Promise { - const requestBody = { - path: routeCfg.path, - script_path: routeCfg.script_path, - is_flow: routeCfg.is_flow, - is_async: routeCfg.is_async, - authentication_method: routeCfg.authentication_method, - route_path: isAdmin || !edit ? routeCfg.route_path : undefined, - http_method: routeCfg.http_method, - is_static_website: routeCfg.is_static_website, - workspaced_route: routeCfg.workspaced_route, - authentication_resource_path: routeCfg.authentication_resource_path, - wrap_body: routeCfg.wrap_body, - raw_string: routeCfg.raw_string - } - try { - if (edit) { - await HttpTriggerService.updateHttpTrigger({ - workspace: workspace, - path: initialPath, - requestBody: requestBody - }) - sendUserToast(`Route ${routeCfg.path} updated`) - } else { - await HttpTriggerService.createHttpTrigger({ - workspace: workspace, - requestBody: requestBody - }) - sendUserToast(`Route ${routeCfg.path} created`) - } - if (!get(usedTriggerKinds).includes('http')) { - usedTriggerKinds.update((t) => [...t, 'http']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/components/triggers/kafka/KafkaCapture.svelte b/frontend/src/lib/components/triggers/kafka/KafkaCapture.svelte deleted file mode 100644 index ce02db0b94..0000000000 --- a/frontend/src/lib/components/triggers/kafka/KafkaCapture.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} - {#if captureInfo.connectionInfo?.connected} -

- Listening to Kafka events... -

- {:else} -

- Connecting to kafka... -

- {/if} - {:else} -

- Start capturing to listen to Kafka events. -

- {/if} - {/snippet} -
-{/if} diff --git a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditor.svelte b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditor.svelte index dac8ec1dc7..4044225223 100644 --- a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditor.svelte +++ b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditor.svelte @@ -2,9 +2,7 @@ import { tick } from 'svelte' import KafkaTriggerEditorInner from './KafkaTriggerEditorInner.svelte' - let { onUpdate }: { onUpdate?: (path?: string) => void } = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() @@ -21,9 +19,9 @@ drawer?.openNew(is_flow, initial_script_path, defaultValues) } - let drawer: KafkaTriggerEditorInner | undefined = $state() + let drawer: KafkaTriggerEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte index 7349e07d6c..792fd7f744 100644 --- a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte @@ -8,107 +8,35 @@ import { KafkaTriggerService } from '$lib/gen' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, sendUserToast } from '$lib/utils' + import { createEventDispatcher } from 'svelte' import Section from '$lib/components/Section.svelte' - import { Loader2 } from 'lucide-svelte' + import { Loader2, Save } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' + import Toggle from '$lib/components/Toggle.svelte' import KafkaTriggersConfigSection from './KafkaTriggersConfigSection.svelte' - import type { Snippet } from 'svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import { saveKafkaTriggerFromCfg } from './utils' - import { handleConfigChange } from '../utils' - interface Props { - useDrawer?: boolean - description?: Snippet | undefined - hideTarget?: boolean - hideTooltips?: boolean - isEditor?: boolean - allowDraft?: boolean - hasDraft?: boolean - isDraftOnly?: boolean - customLabel?: Snippet - isDeployed?: boolean - cloudDisabled?: boolean - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void - onCaptureConfigChange?: (cfg: Record, isValid: boolean) => void - onUpdate?: (path?: string) => void - onDelete?: () => void - onReset?: () => void - } + let drawer: Drawer + let is_flow: boolean = false + let initialPath = '' + let edit = true + let itemKind: 'flow' | 'script' = 'script' + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let enabled = false + let dirtyPath = false + let can_write = true + let drawerLoading = true + let defaultValues: Record | undefined = undefined + let args: Record = {} - let { - useDrawer = true, - description = undefined, - hideTarget = false, - hideTooltips = false, - isEditor = false, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - customLabel, - isDeployed = false, - cloudDisabled = false, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined - }: Props = $props() + const dispatch = createEventDispatcher() - let drawer: Drawer | undefined = $state() - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let itemKind: 'flow' | 'script' = $state('script') - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path: string = $state('') - let pathError = $state('') - let enabled = $state(false) - let dirtyPath = $state(false) - let can_write = $state(true) - let drawerLoading = $state(true) - let showLoading = $state(false) - let initialConfig: Record | undefined = undefined - let extra_perms = $state | undefined>(undefined) - let kafkaCfgValid = $state(false) - let kafkaResourcePath = $state('') - let kafkaCfg: Record = $state({}) - let deploymentLoading = $state(false) + $: is_flow = itemKind === 'flow' - const isValid = $derived( - !!kafkaResourcePath && - kafkaCfgValid && - kafkaCfg.topics && - kafkaCfg.topics.length > 0 && - kafkaCfg.topics.every((b) => /^[a-zA-Z0-9-_.]+$/.test(b)) - ) - const saveDisabled = $derived( - pathError !== '' || - !isValid || - drawerLoading || - !can_write || - emptyString(script_path) || - emptyString(kafkaResourcePath) || - kafkaCfg.topics.length === 0 || - kafkaCfg.topics.some((t) => emptyString(t)) - ) - const kafkaConfig = $derived.by(getSaveCfg) - const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({})) - - $effect(() => { - is_flow = itemKind === 'flow' - }) - - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultConfig?: Record - ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() @@ -116,16 +44,11 @@ itemKind = isFlow ? 'flow' : 'script' edit = true dirtyPath = false - await loadTrigger(defaultConfig) + await loadTrigger() } catch (err) { sendUserToast(`Could not load Kafka trigger: ${err}`, true) } finally { - if (!defaultConfig) { - initialConfig = structuredClone($state.snapshot(getSaveCfg())) - } - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } @@ -134,209 +57,164 @@ fixedScriptPath_?: string, nDefaultValues?: Record ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms drawerLoading = true try { drawer?.openDrawer() is_flow = nis_flow edit = false itemKind = nis_flow ? 'flow' : 'script' - kafkaResourcePath = nDefaultValues?.kafka_resource_path ?? '' - kafkaCfg = { - group_id: nDefaultValues?.group_id ?? '', - topics: nDefaultValues?.topics ?? [''] - } + args.kafka_resource_path = nDefaultValues?.kafka_resource_path ?? '' + args.group_id = nDefaultValues?.group_id ?? '' + args.topics = nDefaultValues?.topics ?? [''] initialScriptPath = '' fixedScriptPath = fixedScriptPath_ ?? '' script_path = fixedScriptPath - path = nDefaultValues?.path ?? '' + path = '' initialPath = '' dirtyPath = false + defaultValues = nDefaultValues } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } - function loadTriggerConfig(cfg?: Record): void { - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - is_flow = cfg?.is_flow - path = cfg?.path - kafkaResourcePath = cfg?.kafka_resource_path - kafkaCfg = { - group_id: cfg?.group_id, - topics: cfg?.topics - } - enabled = cfg?.enabled - extra_perms = cfg?.extra_perms - can_write = canWrite(path, cfg?.extra_perms, $userStore) - } + async function loadTrigger(): Promise { + const s = await KafkaTriggerService.getKafkaTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + script_path = s.script_path + initialScriptPath = s.script_path - async function loadTrigger(defaultConfig?: Record): Promise { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - return - } else { - const s = await KafkaTriggerService.getKafkaTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - loadTriggerConfig(s) - } - } + is_flow = s.is_flow + path = s.path + args.kafka_resource_path = s.kafka_resource_path + args.group_id = s.group_id + args.topics = s.topics + enabled = s.enabled - function getSaveCfg(): Record { - return { - path, - script_path, - is_flow, - kafka_resource_path: kafkaResourcePath, - group_id: kafkaCfg.group_id, - topics: kafkaCfg.topics, - enabled, - extra_perms: extra_perms - } + can_write = canWrite(s.path, s.extra_perms, $userStore) } async function updateTrigger(): Promise { - deploymentLoading = true - const cfg = getSaveCfg() - const isSaved = await saveKafkaTriggerFromCfg( - initialPath, - cfg, - edit, - $workspaceStore!, - usedTriggerKinds + if (edit) { + await KafkaTriggerService.updateKafkaTrigger({ + workspace: $workspaceStore!, + path: initialPath, + requestBody: { + path, + script_path, + is_flow, + kafka_resource_path: args.kafka_resource_path, + group_id: args.group_id, + topics: args.topics + } + }) + sendUserToast(`Kafka trigger ${path} updated`) + } else { + await KafkaTriggerService.createKafkaTrigger({ + workspace: $workspaceStore!, + requestBody: { + path, + script_path, + is_flow, + enabled: true, + kafka_resource_path: args.kafka_resource_path, + group_id: args.group_id, + topics: args.topics + } + }) + sendUserToast(`Kafka trigger ${path} created`) + } + if (!$usedTriggerKinds.includes('kafka')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'kafka'] + } + dispatch('update') + drawer.closeDrawer() + } + + function useDefaultValues() { + if (args.kafka_resource_path && args.kafka_resource_path != '') { + return false + } + if (!defaultValues) { + return false + } + return ( + defaultValues.brokers && + defaultValues.brokers.length > 0 && + defaultValues.brokers.some((broker: string) => broker.trim() !== '') ) - if (isSaved) { - onUpdate?.(cfg.path) - drawer?.closeDrawer() - } - deploymentLoading = false } - function getCaptureConfig(): Record { - return { - kafka_resource_path: kafkaResourcePath, - group_id: kafkaCfg.group_id, - topics: structuredClone($state.snapshot(kafkaCfg.topics)), - path - } - } - - async function handleToggleEnabled(nEnabled: boolean) { - await KafkaTriggerService.setKafkaTriggerEnabled({ - path: initialPath, - workspace: $workspaceStore ?? '', - requestBody: { enabled: nEnabled } - }) - sendUserToast(`${nEnabled ? 'enabled' : 'disabled'} Kafka trigger ${initialPath}`) - } - - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid) - }) - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(kafkaConfig, initialConfig, saveDisabled, edit, onConfigChange) - } - }) + let isValid = false -{#if useDrawer} - - - - {@render actionsButtons('sm')} - - {@render config()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} + + + + {#if !drawerLoading} + {#if edit} +
+ { + await KafkaTriggerService.setKafkaTriggerEnabled({ + path: initialPath, + workspace: $workspaceStore ?? '', + requestBody: { enabled: e.detail } + }) + sendUserToast(`${e.detail ? 'enabled' : 'disabled'} Kafka trigger ${initialPath}`) + }} + /> +
+ {/if} + {#if can_write} + + {/if} {/if}
- - {@render actionsButtons('xs')} - - {@render config()} -
-{/if} - -{#snippet actionsButtons(size: 'xs' | 'sm' = 'sm')} - {#if !drawerLoading} - - {/if} -{/snippet} - -{#snippet config()} - {#if drawerLoading} - {#if showLoading} + {#if drawerLoading} - {/if} - {:else} -
- {#if description} - {@render description()} - {/if} - {#if !hideTooltips} - - {#if edit} - Changes can take up to 30 seconds to take effect. - {:else} - Kafka consumers can take up to 30 seconds to start. - {/if} - - {/if} -
-
-
- -
+ {:else} + + {#if edit} + Changes can take up to 30 seconds to take effect. + {:else} + Kafka consumers can take up to 30 seconds to start. + {/if} + +
+
+ +
- {#if !hideTarget}

Pick a script or flow to be triggered @@ -357,23 +235,20 @@ btnClasses="ml-4 mt-2" color="dark" size="xs" - disabled={!can_write} href={itemKind === 'flow' ? '/flows/add?hub=65' : '/scripts/add?hub=hub%2F19659'} target="_blank">Create from template {/if}

- {/if} - -
- {/if} -{/snippet} + + + {/if} + +
diff --git a/frontend/src/lib/components/triggers/kafka/KafkaTriggersConfigSection.svelte b/frontend/src/lib/components/triggers/kafka/KafkaTriggersConfigSection.svelte index 83f4720736..0b287d040e 100644 --- a/frontend/src/lib/components/triggers/kafka/KafkaTriggersConfigSection.svelte +++ b/frontend/src/lib/components/triggers/kafka/KafkaTriggersConfigSection.svelte @@ -1,31 +1,162 @@
-
- {#snippet header()} - {#if showTestingBadge} - - {/if} - {/snippet} + {#if showCapture && captureInfo} + + {/if} +
- - {#if !!kafkaResourcePath} + + {#if !staticInputDisabled} + { + if (ev.detail === 'static') { + delete args.kafka_resource_path + args.brokers = [''] + args.security = { + label: 'PLAINTEXT' + } + } else { + delete args.brokers + delete args.security + } + }} + let:item + > + + + + {/if} + + + {#if selected === 'resource'} + + {:else} + args, + (v) => { + args = { + ...args, + ...v + } + } + } + bind:isValid={isStaticConnectionValid} + lightHeader={true} + /> + {/if} + {#if isConnectionValid} {/if} @@ -90,11 +287,18 @@
args, + (v) => { + args = { + ...args, + ...v + } + } + } + bind:isValid={otherArgsValid} lightHeader={true} - disabled={!can_write} />
diff --git a/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte b/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte index d11fe49768..364a976921 100644 --- a/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte +++ b/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte @@ -1,69 +1,137 @@ + { + loadTriggers() + }} + bind:this={kafkaTriggerEditor} +/> + {#if !$enterpriseLicense} Kafka triggers are an enterprise only feature. +{:else if isCloudHosted()} + + Kafka triggers are disabled in the multi-tenant cloud. + {:else}
- - {#snippet description()} - {#if cloudDisabled} - - Kafka triggers are disabled in the multi-tenant cloud. - - {:else} - - Kafka triggers execute scripts and flows in response to messages published to Kafka - topics. - - {/if} - {/snippet} - + + Kafka triggers execute scripts and flows in response to messages published to Kafka topics. + + {#if !newItem && kafkaTriggers && kafkaTriggers.length > 0} +
+
+ {#each kafkaTriggers as kafkaTrigger (kafkaTrigger.path)} +
+
{kafkaTrigger.path}
+
+ {kafkaTrigger.kafka_resource_path} +
+
+ +
+
+ {/each} +
+
+ {/if} + { + saveTrigger(path, e.detail.config) + }} + on:applyArgs + on:addPreprocessor + on:updateSchema + on:testWithArgs + cloudDisabled={false} + triggerType="kafka" + {isFlow} + {data} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + bind:showCapture={dontCloseOnLoad} + />
{/if} diff --git a/frontend/src/lib/components/triggers/kafka/utils.ts b/frontend/src/lib/components/triggers/kafka/utils.ts deleted file mode 100644 index d87a730d1b..0000000000 --- a/frontend/src/lib/components/triggers/kafka/utils.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { KafkaTriggerService } from '$lib/gen' -import { sendUserToast } from '$lib/toast' -import { get, type Writable } from 'svelte/store' - -export async function saveKafkaTriggerFromCfg( - initialPath: string, - cfg: Record, - edit: boolean, - workspace: string, - usedTriggerKinds: Writable -): Promise { - const requestBody = { - path: cfg.path, - script_path: cfg.script_path, - is_flow: cfg.is_flow, - kafka_resource_path: cfg.kafka_resource_path, - group_id: cfg.group_id, - topics: cfg.topics - } - try { - if (edit) { - await KafkaTriggerService.updateKafkaTrigger({ - workspace, - path: initialPath, - requestBody - }) - sendUserToast(`Kafka trigger ${cfg.path} updated`) - } else { - await KafkaTriggerService.createKafkaTrigger({ - workspace, - requestBody: { ...requestBody, enabled: true } - }) - sendUserToast(`Kafka trigger ${cfg.path} created`) - } - if (!get(usedTriggerKinds).includes('kafka')) { - usedTriggerKinds.update((t) => [...t, 'kafka']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/components/triggers/mqtt/MqttCapture.svelte b/frontend/src/lib/components/triggers/mqtt/MqttCapture.svelte deleted file mode 100644 index e31e8edd6f..0000000000 --- a/frontend/src/lib/components/triggers/mqtt/MqttCapture.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} - {#if captureInfo.connectionInfo?.connected} -

- Listening to MQTT messages... -

- {:else} -

- Connecting to mqtt... -

- {/if} - {:else} -

- Start capturing to listen to MQTT messages. -

- {/if} - {/snippet} -
-{/if} diff --git a/frontend/src/lib/components/triggers/mqtt/MqttEditorConfigSection.svelte b/frontend/src/lib/components/triggers/mqtt/MqttEditorConfigSection.svelte index ae78ff3143..b2c11571ae 100644 --- a/frontend/src/lib/components/triggers/mqtt/MqttEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/mqtt/MqttEditorConfigSection.svelte @@ -1,5 +1,7 @@
+ {#if showCapture && captureInfo} + + {/if}
- - {#if showTestingBadge} - - {/if} -
diff --git a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditor.svelte b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditor.svelte index ac74859255..2bf129ae4d 100644 --- a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditor.svelte +++ b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditor.svelte @@ -2,9 +2,7 @@ import { tick } from 'svelte' import MqttTriggerEditorInner from './MqttTriggerEditorInner.svelte' - let { onUpdate }: { onUpdate?: (path?: string) => void } = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() @@ -21,9 +19,9 @@ drawer?.openNew(is_flow, initial_script_path, defaultValues) } - let drawer: MqttTriggerEditorInner | undefined = $state() + let drawer: MqttTriggerEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte index 0f4b31465e..3a94f63a9d 100644 --- a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte @@ -7,9 +7,11 @@ import ScriptPicker from '$lib/components/ScriptPicker.svelte' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, sendUserToast } from '$lib/utils' + import { createEventDispatcher } from 'svelte' import Section from '$lib/components/Section.svelte' - import { Loader2 } from 'lucide-svelte' + import { Loader2, Save } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' + import Toggle from '$lib/components/Toggle.svelte' import { MqttTriggerService, type MqttClientVersion, @@ -18,92 +20,33 @@ type MqttSubscribeTopic } from '$lib/gen' import MqttEditorConfigSection from './MqttEditorConfigSection.svelte' - import type { Snippet } from 'svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import { saveMqttTriggerFromCfg } from './utils' - import { handleConfigChange } from '../utils' - interface Props { - useDrawer?: boolean - description?: Snippet | undefined - hideTarget?: boolean - hideTooltips?: boolean - isEditor?: boolean - allowDraft?: boolean - hasDraft?: boolean - isDraftOnly?: boolean - customLabel?: Snippet - isDeployed?: boolean - cloudDisabled?: boolean - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void - onCaptureConfigChange?: (cfg: Record, isValid: boolean) => void - onUpdate?: (path?: string) => void - onDelete?: () => void - onReset?: () => void - } + let mqtt_resource_path: string = '' + let drawer: Drawer + let is_flow: boolean = false + let initialPath = '' + let edit = true + let itemKind: 'flow' | 'script' = 'script' + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let enabled = false + let dirtyPath = false + let can_write = true + let drawerLoading = true + let subscribe_topics: MqttSubscribeTopic[] = [] + let v3_config: MqttV3Config | undefined + let v5_config: MqttV5Config | undefined + let client_version: MqttClientVersion | undefined + let client_id: string + let isValid: boolean + const dispatch = createEventDispatcher() - let { - useDrawer = true, - description = undefined, - hideTarget = false, - hideTooltips = false, - isEditor = false, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - customLabel = undefined, - isDeployed = false, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined, - cloudDisabled = false - }: Props = $props() + $: is_flow = itemKind === 'flow' - let mqtt_resource_path: string = $state('') - let drawer: Drawer | undefined = $state(undefined) - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let itemKind: 'flow' | 'script' = $state('script') - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path: string = $state('') - let pathError = $state('') - let enabled = $state(false) - let dirtyPath = $state(false) - let can_write = $state(true) - let drawerLoading = $state(true) - let showLoading = $state(false) - let subscribe_topics: MqttSubscribeTopic[] = $state([]) - let v3_config: MqttV3Config | undefined = $state() - let v5_config: MqttV5Config | undefined = $state() - let client_version: MqttClientVersion | undefined = $state() - let client_id: string | undefined = $state(undefined) - let isValid: boolean | undefined = $state(undefined) - let initialConfig: Record | undefined = undefined - let deploymentLoading = $state(false) - - const mqttConfig = $derived.by(getSaveCfg) - const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({})) - const saveDisabled = $derived( - pathError != '' || emptyString(script_path) || !can_write || !isValid - ) - - $effect(() => { - is_flow = itemKind === 'flow' - }) - - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultConfig?: Record - ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() @@ -111,16 +54,11 @@ itemKind = isFlow ? 'flow' : 'script' edit = true dirtyPath = false - await loadTrigger(defaultConfig) + await loadTrigger() } catch (err) { sendUserToast(`Could not load mqtt trigger: ${err.body}`, true) } finally { - if (!defaultConfig) { - initialConfig = structuredClone($state.snapshot(getSaveCfg())) - } - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } @@ -129,12 +67,9 @@ fixedScriptPath_?: string, defaultValues?: Record ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) drawerLoading = true try { - mqtt_resource_path = defaultValues?.mqtt_resource_path ?? '' + mqtt_resource_path = '' drawer?.openDrawer() is_flow = nis_flow itemKind = nis_flow ? 'flow' : 'script' @@ -142,212 +77,149 @@ fixedScriptPath = fixedScriptPath_ ?? '' script_path = fixedScriptPath subscribe_topics = defaultValues?.topics ?? [] - path = defaultValues?.path ?? '' + path = '' initialPath = '' edit = false dirtyPath = false client_version = defaultValues?.client_version ?? 'v5' client_id = defaultValues?.client_id ?? '' - enabled = defaultValues?.enabled ?? false } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } - async function loadTriggerConfig(cfg?: Record): Promise { + async function loadTrigger(): Promise { try { - mqtt_resource_path = cfg?.mqtt_resource_path - subscribe_topics = cfg?.subscribe_topics - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - is_flow = cfg?.is_flow - path = cfg?.path - enabled = cfg?.enabled - client_version = cfg?.client_version - v3_config = cfg?.v3_config - v5_config = cfg?.v5_config - client_id = cfg?.client_id ?? '' - can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore) - } catch (error) { - sendUserToast(`Could not load mqtt trigger config: ${error.body}`, true) - } - } - - async function loadTrigger(defaultConfig?: Record): Promise { - try { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - return - } else { - const s = await MqttTriggerService.getMqttTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - loadTriggerConfig(s) - } + const s = await MqttTriggerService.getMqttTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + mqtt_resource_path = s.mqtt_resource_path + subscribe_topics = s.subscribe_topics + script_path = s.script_path + initialScriptPath = s.script_path + is_flow = s.is_flow + path = s.path + enabled = s.enabled + client_version = s.client_version + v3_config = s.v3_config + v5_config = s.v5_config + client_id = s.client_id ?? '' + can_write = canWrite(s.path, s.extra_perms, $userStore) } catch (error) { sendUserToast(`Could not load mqtt trigger: ${error.body}`, true) } } - function getSaveCfg(): Record { - return { - client_id, - client_version, - v3_config, - v5_config, - mqtt_resource_path, - subscribe_topics, - path, - script_path, - enabled, - is_flow - } - } - - function getCaptureConfig(): Record { - return { - mqtt_resource_path, - subscribe_topics, - client_version, - v3_config, - v5_config, - client_id, - path - } - } - async function updateTrigger(): Promise { - deploymentLoading = true - const cfg = getSaveCfg() - const isSaved = await saveMqttTriggerFromCfg( - initialPath, - cfg, - edit, - $workspaceStore!, - usedTriggerKinds - ) - if (isSaved) { - onUpdate?.(cfg.path) - drawer?.closeDrawer() - } - deploymentLoading = false - } - - async function handleToggleEnabled(newEnabled: boolean) { - enabled = newEnabled - if (!isDraftOnly && !hasDraft) { - await MqttTriggerService.setMqttTriggerEnabled({ + if (edit) { + await MqttTriggerService.updateMqttTrigger({ + workspace: $workspaceStore!, path: initialPath, - workspace: $workspaceStore ?? '', - requestBody: { enabled: newEnabled } + requestBody: { + client_id, + client_version, + v3_config, + v5_config, + mqtt_resource_path, + subscribe_topics, + path, + script_path, + enabled, + is_flow + } }) - sendUserToast(`${newEnabled ? 'enabled' : 'disabled'} MQTT trigger ${initialPath}`) + sendUserToast(`MQTT trigger ${path} updated`) + } else { + await MqttTriggerService.createMqttTrigger({ + workspace: $workspaceStore!, + requestBody: { + client_id, + client_version, + v3_config, + v5_config, + mqtt_resource_path, + subscribe_topics, + enabled: true, + path, + script_path, + is_flow + } + }) + sendUserToast(`MQTT trigger ${path} created`) } + + if (!$usedTriggerKinds.includes('mqtt')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'mqtt'] + } + dispatch('update') + drawer.closeDrawer() } - - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid ?? false) - }) - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(mqttConfig, initialConfig, saveDisabled, edit, onConfigChange) - } - }) -{#if useDrawer} - - - - {@render actions()} - - {@render config()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} + + + + {#if !drawerLoading && can_write} + {#if edit} +
+ { + sendUserToast(`${e.detail ? 'enabled' : 'disabled'} mqtt trigger ${initialPath}`) + }} + /> +
+ {/if} + {/if}
- - {@render actions()} - - {@render config()} -
-{/if} - -{#snippet actions()} - {#if !drawerLoading} - - {/if} -{/snippet} - -{#snippet config()} - {#if drawerLoading} - {#if showLoading} - - {/if} - {:else} -
- {#if description} - {@render description()} - {/if} - {#if !hideTooltips} - + {#if drawerLoading} +
+ +

Loading...

+
+ {:else} +
+ {#if edit} Changes can take up to 30 seconds to take effect. {:else} New MQTT triggers can take up to 30 seconds to start listening. {/if} - {/if} -
-
-
-
+
+
+ +
- {#if !hideTarget}

Pick a script or flow to be triggered @@ -360,37 +232,32 @@ allowFlow={true} bind:itemKind bind:scriptPath={script_path} - allowRefresh={can_write} - allowEdit={!$userStore?.operator} + allowRefresh /> {#if emptyString(script_path)} - Create from template - {/if}

- {/if} - -
- {/if} -{/snippet} + +
+ {/if} + + diff --git a/frontend/src/lib/components/triggers/mqtt/MqttTriggersPanel.svelte b/frontend/src/lib/components/triggers/mqtt/MqttTriggersPanel.svelte index 7b918726f2..834b6c16bb 100644 --- a/frontend/src/lib/components/triggers/mqtt/MqttTriggersPanel.svelte +++ b/frontend/src/lib/components/triggers/mqtt/MqttTriggersPanel.svelte @@ -1,62 +1,126 @@ -
- - {#snippet description()} - {#if cloudDisabled} - - MQTT triggers are disabled in the multi-tenant cloud. - - {:else} - - MQTT triggers allow you to execute scripts and flows in response to MQTT messages. They - can be configured to subscribe to specific topics with different QoS levels. - - {/if} - {/snippet} - -
+ { + loadTriggers() + }} + bind:this={mqttTriggerEditor} +/> + +{#if isCloudHosted()} + + MQTT triggers are disabled in the multi-tenant cloud. + +{:else} +
+ + Windmill can connect to an MQTT broker and subscribes to specific topics thus allowing the + execution of script/flows based on the event triggered by those subscribed topics + + + {#if !newItem && mqttTriggers && mqttTriggers.length > 0} +
+
+
+ {#each mqttTriggers as mqttTriggers (mqttTriggers.path)} +
+
{mqttTriggers.path}
+ +
+ +
+
+ {/each} +
+
+
+ {/if} + + { + mqttTriggerEditor?.openNew(isFlow, path, e.detail.config) + }} + on:addPreprocessor + on:updateSchema + on:testWithArgs + cloudDisabled={false} + triggerType="mqtt" + {isFlow} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + bind:showCapture={dontCloseOnLoad} + /> +
+{/if} diff --git a/frontend/src/lib/components/triggers/mqtt/utils.ts b/frontend/src/lib/components/triggers/mqtt/utils.ts deleted file mode 100644 index 14e264a088..0000000000 --- a/frontend/src/lib/components/triggers/mqtt/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MqttTriggerService } from '$lib/gen' -import { sendUserToast } from '$lib/toast' -import { get, type Writable } from 'svelte/store' - -export async function saveMqttTriggerFromCfg( - initialPath: string, - cfg: Record, - edit: boolean, - workspace: string, - usedTriggerKinds: Writable -): Promise { - const requestBody = { - client_id: cfg.client_id, - client_version: cfg.client_version, - v3_config: cfg.v3_config, - v5_config: cfg.v5_config, - mqtt_resource_path: cfg.mqtt_resource_path, - subscribe_topics: cfg.subscribe_topics, - path: cfg.path, - script_path: cfg.script_path, - enabled: cfg.enabled, - is_flow: cfg.is_flow - } - try { - if (edit) { - await MqttTriggerService.updateMqttTrigger({ - workspace, - path: initialPath, - requestBody - }) - sendUserToast(`MQTT trigger ${cfg.path} updated`) - } else { - await MqttTriggerService.createMqttTrigger({ - workspace, - requestBody: { ...requestBody, enabled: true } - }) - sendUserToast(`MQTT trigger ${cfg.path} created`) - } - - if (!get(usedTriggerKinds).includes('mqtt')) { - usedTriggerKinds.update((t) => [...t, 'mqtt']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/components/triggers/nats/NatsCapture.svelte b/frontend/src/lib/components/triggers/nats/NatsCapture.svelte deleted file mode 100644 index c44fb8b5d7..0000000000 --- a/frontend/src/lib/components/triggers/nats/NatsCapture.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} - {#if captureInfo.connectionInfo?.connected} -

- Listening to NATS messages... -

- {:else} -

- Connecting to nats... -

- {/if} - {:else} -

- Start capturing to listen to NATS messages. -

- {/if} - {/snippet} -
-{/if} diff --git a/frontend/src/lib/components/triggers/nats/NatsTriggerEditor.svelte b/frontend/src/lib/components/triggers/nats/NatsTriggerEditor.svelte index ef01c1e51d..e4b74437d9 100644 --- a/frontend/src/lib/components/triggers/nats/NatsTriggerEditor.svelte +++ b/frontend/src/lib/components/triggers/nats/NatsTriggerEditor.svelte @@ -2,9 +2,7 @@ import { tick } from 'svelte' import NatsTriggerEditorInner from './NatsTriggerEditorInner.svelte' - let { onUpdate } = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() @@ -21,9 +19,9 @@ drawer?.openNew(is_flow, initial_script_path, defaultValues) } - let drawer: NatsTriggerEditorInner | undefined = $state() + let drawer: NatsTriggerEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte index d1a0b87bee..6d3cab175f 100644 --- a/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte @@ -8,97 +8,35 @@ import { NatsTriggerService } from '$lib/gen' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, sendUserToast } from '$lib/utils' + import { createEventDispatcher } from 'svelte' import Section from '$lib/components/Section.svelte' - import { Loader2 } from 'lucide-svelte' + import { Loader2, Save } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' import NatsTriggersConfigSection from './NatsTriggersConfigSection.svelte' - import type { Snippet } from 'svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import { saveNatsTriggerFromCfg } from './utils' - import { handleConfigChange } from '../utils' + import Toggle from '$lib/components/Toggle.svelte' - interface Props { - useDrawer?: boolean - description?: Snippet | undefined - hideTarget?: boolean - hideTooltips?: boolean - useEditButton?: boolean - isEditor?: boolean - allowDraft?: boolean - hasDraft?: boolean - isDraftOnly?: boolean - isDeployed?: boolean - cloudDisabled?: boolean - customLabel?: Snippet - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void - onCaptureConfigChange?: (cfg: Record, isValid: boolean) => void - onUpdate?: (path?: string) => void - onDelete?: () => void - onReset?: () => void - } + let drawer: Drawer + let is_flow: boolean = false + let initialPath = '' + let edit = true + let itemKind: 'flow' | 'script' = 'script' + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let enabled = false + let dirtyPath = false + let can_write = true + let drawerLoading = true + let defaultValues: Record | undefined = undefined + let args: Record = {} - let { - useDrawer = true, - description = undefined, - hideTarget = false, - hideTooltips = false, - isEditor = false, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - isDeployed = false, - cloudDisabled = false, - customLabel = undefined, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined - }: Props = $props() + const dispatch = createEventDispatcher() - let drawer: Drawer | undefined = $state(undefined) - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let itemKind: 'flow' | 'script' = $state('script') - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path: string = $state('') - let pathError = $state('') - let enabled = $state(false) - let dirtyPath = $state(false) - let can_write = $state(true) - let drawerLoading = $state(true) - let showLoading = $state(false) - let defaultValues: Record | undefined = $state(undefined) - let natsResourcePath = $state('') - let subjects = $state(['']) - let useJetstream = $state(false) - let streamName = $state('') - let consumerName = $state('') - let initialConfig: Record | undefined = undefined - let deploymentLoading = $state(false) - let isValid = $state(false) + $: is_flow = itemKind === 'flow' - const saveDisabled = $derived( - pathError != '' || emptyString(script_path) || !can_write || !isValid - ) - const natsConfig = $derived.by(getSaveCfg) - const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({})) - - $effect(() => { - is_flow = itemKind === 'flow' - }) - - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultConfig?: Record - ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() @@ -106,16 +44,11 @@ itemKind = isFlow ? 'flow' : 'script' edit = true dirtyPath = false - await loadTrigger(defaultConfig) + await loadTrigger() } catch (err) { sendUserToast(`Could not load nats trigger: ${err}`, true) } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false - if (!defaultConfig) { - initialConfig = structuredClone($state.snapshot(getSaveCfg())) - } } } @@ -124,95 +57,92 @@ fixedScriptPath_?: string, nDefaultValues?: Record ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) drawerLoading = true try { drawer?.openDrawer() is_flow = nis_flow edit = false itemKind = nis_flow ? 'flow' : 'script' - natsResourcePath = nDefaultValues?.nats_resource_path ?? '' - subjects = nDefaultValues?.subjects ?? [''] - useJetstream = nDefaultValues?.use_jetstream ?? false - streamName = useJetstream ? (nDefaultValues?.stream_name ?? '') : undefined - consumerName = useJetstream ? (nDefaultValues?.consumer_name ?? '') : undefined + args.nats_resource_path = nDefaultValues?.nats_resource_path ?? '' + args.subjects = nDefaultValues?.subjects ?? [''] + args.use_jetstream = nDefaultValues?.use_jetstream ?? false + args.stream_name = args.use_jetstream ? (nDefaultValues?.stream_name ?? '') : undefined + args.consumer_name = args.use_jetstream ? (nDefaultValues?.consumer_name ?? '') : undefined initialScriptPath = '' fixedScriptPath = fixedScriptPath_ ?? '' script_path = fixedScriptPath - path = nDefaultValues?.path ?? '' + path = '' initialPath = '' dirtyPath = false defaultValues = nDefaultValues - enabled = nDefaultValues?.enabled ?? false } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } - async function loadTriggerConfig(cfg?: Record): Promise { - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - is_flow = cfg?.is_flow - path = cfg?.path - natsResourcePath = cfg?.nats_resource_path - streamName = cfg?.stream_name - consumerName = cfg?.consumer_name - subjects = cfg?.subjects || [''] - useJetstream = cfg?.use_jetstream || false - enabled = cfg?.enabled - can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore) - } + async function loadTrigger(): Promise { + const s = await NatsTriggerService.getNatsTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + script_path = s.script_path + initialScriptPath = s.script_path - async function loadTrigger(defaultConfig?: Record): Promise { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - return - } else { - const s = await NatsTriggerService.getNatsTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - loadTriggerConfig(s) - } - } + is_flow = s.is_flow + path = s.path + args.nats_resource_path = s.nats_resource_path + args.stream_name = s.stream_name + args.consumer_name = s.consumer_name + args.subjects = s.subjects + args.use_jetstream = s.use_jetstream + enabled = s.enabled - function getSaveCfg() { - return { - path, - script_path, - is_flow, - enabled, - nats_resource_path: natsResourcePath, - stream_name: streamName, - consumer_name: consumerName, - subjects, - use_jetstream: useJetstream - } + can_write = canWrite(s.path, s.extra_perms, $userStore) } async function updateTrigger(): Promise { - deploymentLoading = true - const cfg = natsConfig - const isSaved = await saveNatsTriggerFromCfg( - initialPath, - cfg, - edit, - $workspaceStore!, - usedTriggerKinds - ) - if (isSaved) { - onUpdate?.(cfg.path) - drawer?.closeDrawer() + if (edit) { + await NatsTriggerService.updateNatsTrigger({ + workspace: $workspaceStore!, + path: initialPath, + requestBody: { + path, + script_path, + is_flow, + nats_resource_path: args.nats_resource_path, + stream_name: args.stream_name, + consumer_name: args.consumer_name, + subjects: args.subjects, + use_jetstream: args.use_jetstream + } + }) + sendUserToast(`Nats trigger ${path} updated`) + } else { + await NatsTriggerService.createNatsTrigger({ + workspace: $workspaceStore!, + requestBody: { + path, + script_path, + is_flow, + enabled: true, + nats_resource_path: args.nats_resource_path, + stream_name: args.stream_name, + consumer_name: args.consumer_name, + subjects: args.subjects, + use_jetstream: args.use_jetstream + } + }) + sendUserToast(`Nats trigger ${path} created`) } - deploymentLoading = false + if (!$usedTriggerKinds.includes('nats')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'nats'] + } + dispatch('update') + drawer.closeDrawer() } function useDefaultValues() { - if (natsResourcePath && natsResourcePath != '') { + if (args.nats_resource_path && args.nats_resource_path != '') { return false } if (!defaultValues) { @@ -225,120 +155,73 @@ ) } - async function handleToggleEnabled(toggleEnabled: boolean) { - enabled = toggleEnabled - if (!isDraftOnly && !hasDraft) { - await NatsTriggerService.setNatsTriggerEnabled({ - path: initialPath, - workspace: $workspaceStore ?? '', - requestBody: { enabled: toggleEnabled } - }) - sendUserToast(`${toggleEnabled ? 'enabled' : 'disabled'} NATS trigger ${initialPath}`) - } - } - - function getCaptureConfig() { - const { nats_resource_path, subjects, stream_name, consumer_name, use_jetstream } = natsConfig - return { nats_resource_path, subjects, stream_name, consumer_name, use_jetstream } - } - - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid) - }) - - $effect(() => { - !drawerLoading && - handleConfigChange(natsConfig, initialConfig, saveDisabled, edit, onConfigChange) - }) + let isValid = false -{#if useDrawer} - - - - {@render actions()} - - {@render config()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} + + + + {#if !drawerLoading} + {#if edit} +
+ { + await NatsTriggerService.setNatsTriggerEnabled({ + path: initialPath, + workspace: $workspaceStore ?? '', + requestBody: { enabled: e.detail } + }) + sendUserToast(`${e.detail ? 'enabled' : 'disabled'} NATS trigger ${initialPath}`) + }} + /> +
+ {/if} + {#if can_write} + + {/if} {/if}
- - {@render actions()} - - {@render config()} -
-{/if} - -{#snippet actions()} - {#if !drawerLoading} - - {/if} -{/snippet} - -{#snippet config()} - {#if drawerLoading} - {#if showLoading} + {#if drawerLoading} - {/if} - {:else} -
- {#if description} - {@render description()} - {/if} - {#if !hideTooltips} - - {#if edit} - Changes can take up to 30 seconds to take effect. - {:else} - NATS consumers can take up to 30 seconds to start. - {/if} - - {/if} -
-
-
- -
- {#if !hideTarget} + {:else} + + {#if edit} + Changes can take up to 30 seconds to take effect. + {:else} + NATS consumers can take up to 30 seconds to start. + {/if} + +
+
+ +

Pick a script or flow to be triggered @@ -360,29 +243,19 @@ color="dark" size="xs" href={itemKind === 'flow' ? '/flows/add?hub=66' : '/scripts/add?hub=hub%2F19663'} - target="_blank" + target="_blank">Create from template - Create from template - {/if}

- {/if} - { - isValid = detail - }} - defaultValues={useDefaultValues() ? defaultValues : undefined} - {can_write} - showTestingBadge={isEditor} - /> -
- {/if} -{/snippet} + + + {/if} + + diff --git a/frontend/src/lib/components/triggers/nats/NatsTriggersConfigSection.svelte b/frontend/src/lib/components/triggers/nats/NatsTriggersConfigSection.svelte index d925552ff0..0a00b449a1 100644 --- a/frontend/src/lib/components/triggers/nats/NatsTriggersConfigSection.svelte +++ b/frontend/src/lib/components/triggers/nats/NatsTriggersConfigSection.svelte @@ -1,49 +1,135 @@
+ {#if showCapture && captureInfo} + + {/if}
- {#snippet badge()} - {#if showTestingBadge} - - {/if} - {/snippet}
- - {#if isConnectionValid} - + {#if !staticInputDisabled} + { + if (ev.detail === 'static') { + delete args.nats_resource_path + args.require_tls = false + args.servers = [''] + args.auth = { + label: 'NO_AUTH' + } + } else { + delete args.servers + delete args.auth + delete args.require_tls + } + }} + let:item + > + + + + {/if} + + + {#if selected === 'resource'} + + {:else} + args, + (v) => { + args = { + ...args, + ...v + } + } + } + bind:isValid={isStaticConnectionValid} + lightHeader={true} + /> + {/if} + {#if isConnectionValid} + {/if}
@@ -160,10 +291,17 @@ setNewArgs(args)} + bind:args={ + () => args, + (v) => { + args = { + ...args, + ...v + } + } + } bind:isValid={otherArgsValid} lightHeader={true} - disabled={!can_write} /> {globalError} diff --git a/frontend/src/lib/components/triggers/nats/NatsTriggersPanel.svelte b/frontend/src/lib/components/triggers/nats/NatsTriggersPanel.svelte index 20bfe8d5dc..12cd385e3a 100644 --- a/frontend/src/lib/components/triggers/nats/NatsTriggersPanel.svelte +++ b/frontend/src/lib/components/triggers/nats/NatsTriggersPanel.svelte @@ -1,69 +1,137 @@ + { + loadTriggers() + }} + bind:this={natsTriggerEditor} +/> + {#if !$enterpriseLicense} - NATS triggers are an enterprise only feature. + Nats triggers are an enterprise only feature. + +{:else if isCloudHosted()} + + Nats triggers are disabled in the multi-tenant cloud. {:else}
- - {#snippet description()} - {#if cloudDisabled} - - NATS triggers are disabled in the multi-tenant cloud. - - {:else} - - NATS triggers allow you to execute scripts and flows in response to NATS messages. They - can be configured to listen to specific subjects and to use JetStream or not. - - {/if} - {/snippet} - + + NATS triggers execute scripts and flows in response to messages published to NATS subjects. + + + {#if !newItem && natsTriggers && natsTriggers.length > 0} +
+
+ {#each natsTriggers as natsTrigger (natsTrigger.path)} +
+
{natsTrigger.path}
+
+ {natsTrigger.nats_resource_path} +
+
+ +
+
+ {/each} +
+
+ {/if} + + { + saveTrigger(path, e.detail.config) + }} + on:applyArgs + on:addPreprocessor + cloudDisabled={false} + triggerType="nats" + {isFlow} + {data} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + bind:showCapture={dontCloseOnLoad} + />
{/if} diff --git a/frontend/src/lib/components/triggers/nats/utils.ts b/frontend/src/lib/components/triggers/nats/utils.ts deleted file mode 100644 index 926956314c..0000000000 --- a/frontend/src/lib/components/triggers/nats/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NatsTriggerService } from '$lib/gen' -import { sendUserToast } from '$lib/toast' -import { get, type Writable } from 'svelte/store' - -export async function saveNatsTriggerFromCfg( - initialPath: string, - cfg: Record, - edit: boolean, - workspace: string, - usedTriggerKinds: Writable -): Promise { - const requestBody = { - path: cfg.path, - script_path: cfg.script_path, - is_flow: cfg.is_flow, - nats_resource_path: cfg.nats_resource_path, - stream_name: cfg.stream_name, - consumer_name: cfg.consumer_name, - subjects: cfg.subjects, - use_jetstream: cfg.use_jetstream - } - try { - if (edit) { - await NatsTriggerService.updateNatsTrigger({ - workspace, - path: initialPath, - requestBody - }) - sendUserToast(`Nats trigger ${cfg.path} updated`) - } else { - await NatsTriggerService.createNatsTrigger({ - workspace, - requestBody: { - ...requestBody, - enabled: true - } - }) - sendUserToast(`Nats trigger ${cfg.path} created`) - } - if (!get(usedTriggerKinds).includes('nats')) { - usedTriggerKinds.update((t) => [...t, 'nats']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/components/triggers/postgres/PostgresCapture.svelte b/frontend/src/lib/components/triggers/postgres/PostgresCapture.svelte deleted file mode 100644 index 6e1238d7df..0000000000 --- a/frontend/src/lib/components/triggers/postgres/PostgresCapture.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} - {#if captureInfo.connectionInfo?.connected} -

- Listening to Postgres database transactions... -

- {:else} -

- Connecting to Postgres database... -

- {/if} - {:else} -

- Start capturing to listen to Postgres database transactions. -

- {/if} - {/snippet} -
-{/if} diff --git a/frontend/src/lib/components/triggers/postgres/PostgresEditorConfigSection.svelte b/frontend/src/lib/components/triggers/postgres/PostgresEditorConfigSection.svelte new file mode 100644 index 0000000000..00018eca3c --- /dev/null +++ b/frontend/src/lib/components/triggers/postgres/PostgresEditorConfigSection.svelte @@ -0,0 +1,141 @@ + + +
+ {#if showCapture && captureInfo} + + {/if} +
+
+
+

+ Pick a database to connect to +

+ { + if (emptyString(postgres_resource_path)) { + publication = { ...DEFAULT_PUBLICATION } + } + }} + /> + {#if postgres_resource_path} + + + {/if} +
+ {#if postgres_resource_path} + + + {/if} +
+
+
diff --git a/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditor.svelte b/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditor.svelte index ac36f4ffc2..9b46ac2b0b 100644 --- a/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditor.svelte +++ b/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditor.svelte @@ -2,28 +2,22 @@ import { tick } from 'svelte' import PostgresTriggerEditorInner from './PostgresTriggerEditorInner.svelte' - let { onUpdate } = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() drawer?.openEdit(ePath, isFlow) } - export async function openNew( - is_flow: boolean, - initial_script_path?: string, - defaultValues?: Record - ) { + export async function openNew(is_flow: boolean, initial_script_path?: string, defaultValues?: Record) { open = true await tick() drawer?.openNew(is_flow, initial_script_path, defaultValues) } - let drawer: PostgresTriggerEditorInner | undefined = $state() + let drawer: PostgresTriggerEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte index 3c5253aa72..988152f00c 100644 --- a/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte @@ -8,9 +8,11 @@ import { PostgresTriggerService, type Language, type Relations } from '$lib/gen' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, emptyStringTrimmed, sendUserToast } from '$lib/utils' + import { createEventDispatcher } from 'svelte' import Section from '$lib/components/Section.svelte' - import { Loader2, X } from 'lucide-svelte' + import { Loader2, Save, X } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' + import Toggle from '$lib/components/Toggle.svelte' import ResourcePicker from '$lib/components/ResourcePicker.svelte' import Tooltip from '$lib/components/Tooltip.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' @@ -22,124 +24,38 @@ import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Tab from '$lib/components/common/tabs/Tab.svelte' import RelationPicker from './RelationPicker.svelte' - import { invalidRelations, savePostgresTriggerFromCfg } from './utils' + import { invalidRelations } from './utils' import CheckPostgresRequirement from './CheckPostgresRequirement.svelte' import { base } from '$lib/base' - import type { Snippet } from 'svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import TestingBadge from '../testingBadge.svelte' - import { handleConfigChange } from '../utils' - import { fade } from 'svelte/transition' - interface Props { - useDrawer?: boolean - description?: Snippet | undefined - hideTarget?: boolean - isEditor?: boolean - hideTooltips?: boolean - allowDraft?: boolean - hasDraft?: boolean - isDraftOnly?: boolean - isDeployed?: boolean - cloudDisabled?: boolean - customLabel?: Snippet - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void - onCaptureConfigChange?: (cfg: Record, isValid: boolean) => void - onUpdate?: (path?: string) => void - onDelete?: () => void - onReset?: () => void - } - - let { - useDrawer = true, - description = undefined, - hideTarget = false, - isEditor = false, - hideTooltips = false, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - isDeployed = false, - cloudDisabled = false, - customLabel = undefined, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined - }: Props = $props() - - let drawer: Drawer | undefined = $state(undefined) - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let itemKind: 'flow' | 'script' = $state('script') - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path: string = $state('') - let pathError = $state('') - let enabled = $state(false) - let dirtyPath = $state(false) - let can_write = $state(true) - let drawerLoading = $state(true) - let showLoading = $state(false) - let postgres_resource_path = $state('') - let publication_name: string = $state('') - let replication_slot_name: string = $state('') - let relations: Relations[] | undefined = $state([]) - let transaction_to_track: string[] = $state([]) + let drawer: Drawer + let is_flow: boolean = false + let initialPath = '' + let edit = true + let itemKind: 'flow' | 'script' = 'script' + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let enabled = false + let dirtyPath = false + let can_write = true + let drawerLoading = true + let postgres_resource_path = '' + let publication_name: string = '' + let replication_slot_name: string = '' + let relations: Relations[] | undefined = [] + let transaction_to_track: string[] = [] let language: Language = 'Typescript' - let loading = $state(false) + let loading = false type actions = 'create' | 'get' - let selectedPublicationAction: actions | undefined = $state(undefined) - let selectedSlotAction: actions | undefined = $state(undefined) - let publicationItems: string[] = $state([]) + let selectedPublicationAction: actions + let selectedSlotAction: actions + let publicationItems: string[] = [] let transactionType: string[] = ['Insert', 'Update', 'Delete'] - let tab: 'advanced' | 'basic' = $state('basic') - let initialConfig: Record | undefined = undefined - let deploymentLoading = $state(false) - - const errorMessage = $derived.by(() => { - if (relations && relations.length > 0) { - return invalidRelations(relations, { - showError: true, - trackSchemaTableError: true - }) - } - return '' - }) - - const isValid = $derived( - !emptyString(postgres_resource_path) && - !emptyString(script_path) && - !emptyString(publication_name) && - !emptyString(replication_slot_name) && - !errorMessage - ) - const postgresConfig = $derived.by(getSaveCfg) - const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({})) - - function isAdvancedTab(t: 'advanced' | 'basic'): boolean { - return t === 'advanced' - } - - function isBasicTab(t: 'advanced' | 'basic'): boolean { - return t === 'basic' - } - - let saveDisabled = $derived( - pathError !== '' || - emptyString(postgres_resource_path) || - emptyString(script_path) || - (isAdvancedTab(tab) && emptyString(replication_slot_name)) || - emptyString(publication_name) || - (relations && isBasicTab(tab) && relations.length === 0) || - transaction_to_track.length === 0 || - drawerLoading || - !can_write - ) - + let tab: 'advanced' | 'basic' = 'basic' + let isLoading = false async function createPublication() { try { const message = await PostgresTriggerService.createPostgresPublication({ @@ -173,18 +89,11 @@ } } - $effect(() => { - is_flow = itemKind === 'flow' - }) + const dispatch = createEventDispatcher() - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultConfig?: Record - ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms + $: is_flow = itemKind === 'flow' + + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() @@ -199,28 +108,19 @@ relations = [] transaction_to_track = [] tab = 'basic' - await loadTrigger(defaultConfig) + await loadTrigger() } catch (err) { sendUserToast(`Could not load postgres trigger: ${err.body}`, true) } finally { - if (!defaultConfig) { - initialConfig = structuredClone($state.snapshot(getSaveCfg())) - } - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } export async function openNew( nis_flow: boolean, fixedScriptPath_?: string, - defaultValues?: Record, - newDraft?: boolean + defaultValues?: Record ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms drawerLoading = true try { selectedPublicationAction = 'create' @@ -230,127 +130,120 @@ drawer?.openDrawer() is_flow = nis_flow itemKind = nis_flow ? 'flow' : 'script' - initialScriptPath = defaultValues?.script_path ?? '' + initialScriptPath = '' fixedScriptPath = fixedScriptPath_ ?? '' script_path = fixedScriptPath - path = defaultValues?.path ?? '' + path = '' initialPath = '' postgres_resource_path = defaultValues?.postgres_resource_path ?? '' edit = false dirtyPath = false - publication_name = defaultValues?.publication_name ?? `windmill_publication_${random_adj()}` - replication_slot_name = - defaultValues?.replication_slot_name ?? `windmill_replication_${random_adj()}` - transaction_to_track = defaultValues?.publication?.transaction_to_track || [ + publication_name = `windmill_publication_${random_adj()}` + replication_slot_name = `windmill_replication_${random_adj()}` + transaction_to_track = defaultValues?.publication.transaction_to_track || [ 'Insert', 'Update', 'Delete' ] - relations = defaultValues?.publication?.table_to_track || [ + relations = defaultValues?.publication.table_to_track || [ { schema_name: 'public', table_to_track: [] } ] } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } - function getSaveCfg(): Record { - const cfg = { - script_path: script_path, - initialScriptPath: initialScriptPath, - is_flow: is_flow, - path: path, - postgres_resource_path: postgres_resource_path, - publication_name: edit || tab === 'advanced' ? publication_name : undefined, - replication_slot_name: edit || tab === 'advanced' ? replication_slot_name : undefined, - publication: - !edit || tab === 'basic' - ? { - transaction_to_track: transaction_to_track, - table_to_track: relations - } - : undefined - } - return cfg - } + async function loadTrigger(): Promise { + const s = await PostgresTriggerService.getPostgresTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + script_path = s.script_path + initialScriptPath = s.script_path - async function loadTriggerConfig(cfg?: Record): Promise { - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - is_flow = cfg?.is_flow - path = cfg?.path - enabled = cfg?.enabled - postgres_resource_path = cfg?.postgres_resource_path - publication_name = cfg?.publication_name - replication_slot_name = cfg?.replication_slot_name - can_write = canWrite(path, cfg?.extra_perms, $userStore) - transaction_to_track = [...cfg?.publication?.transaction_to_track] - relations = cfg?.publication?.table_to_track ?? [] - } + is_flow = s.is_flow + path = s.path + enabled = s.enabled + postgres_resource_path = s.postgres_resource_path + publication_name = s.publication_name + replication_slot_name = s.replication_slot_name - async function loadTrigger(defaultConfig?: Record): Promise { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - if (defaultConfig?.publication) { - transaction_to_track = [...defaultConfig.publication.transaction_to_track] - relations = defaultConfig.publication.table_to_track ?? [] - } - return - } else { - const s = await PostgresTriggerService.getPostgresTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - - const publication_data = await PostgresTriggerService.getPostgresPublication({ - path: s.postgres_resource_path, - workspace: $workspaceStore!, - publication: s.publication_name - }) - - loadTriggerConfig({ ...s, publication: publication_data }) - } - } - - function getCaptureConfig() { - return { - postgres_resource_path, - publication: - !edit || tab === 'basic' - ? { - transaction_to_track: transaction_to_track, - table_to_track: relations - } - : undefined, - publication_name: edit || tab !== 'basic' ? publication_name : undefined, - replication_slot_name: edit || tab !== 'basic' ? replication_slot_name : undefined, - path - } + const publication_data = await PostgresTriggerService.getPostgresPublication({ + path: postgres_resource_path, + workspace: $workspaceStore!, + publication: publication_name + }) + transaction_to_track = [...publication_data.transaction_to_track] + relations = publication_data.table_to_track ?? [] + can_write = canWrite(s.path, s.extra_perms, $userStore) } async function updateTrigger(): Promise { - const cfg = postgresConfig - if (!cfg) { + if ( + relations && + invalidRelations(relations, { + showError: true, + trackSchemaTableError: true + }) === true + ) { return } - deploymentLoading = true - const isSaved = await savePostgresTriggerFromCfg( - initialPath, - cfg, - edit, - $workspaceStore!, - usedTriggerKinds - ) - if (isSaved) { - onUpdate?.(path) - drawer?.closeDrawer() + isLoading = true + try { + if (edit) { + await PostgresTriggerService.updatePostgresTrigger({ + workspace: $workspaceStore!, + path: initialPath, + requestBody: { + path, + script_path, + is_flow, + postgres_resource_path, + enabled, + replication_slot_name, + publication_name, + publication: + tab === 'basic' + ? { + transaction_to_track, + table_to_track: relations + } + : undefined + } + }) + sendUserToast(`PostgresTrigger ${path} updated`) + } else { + await PostgresTriggerService.createPostgresTrigger({ + workspace: $workspaceStore!, + requestBody: { + path, + script_path, + is_flow, + enabled: true, + postgres_resource_path, + replication_slot_name: tab === 'basic' ? undefined : replication_slot_name, + publication_name: tab === 'basic' ? undefined : publication_name, + publication: { + transaction_to_track, + table_to_track: relations + } + } + }) + sendUserToast(`PostgresTrigger ${path} created`) + } + isLoading = false + if (!$usedTriggerKinds.includes('postgres')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'postgres'] + } + dispatch('update') + drawer.closeDrawer() + } catch (error) { + isLoading = false + sendUserToast(error.body || error.message, true) } - deploymentLoading = false } const getTemplateScript = async () => { @@ -375,115 +268,81 @@ sendUserToast(error.body, true) } } - - async function handleToggleEnabled(toggleEnabled: boolean) { - enabled = toggleEnabled - if (!isDraftOnly && !hasDraft) { - await PostgresTriggerService.setPostgresTriggerEnabled({ - path: initialPath, - workspace: $workspaceStore ?? '', - requestBody: { enabled: toggleEnabled } - }) - sendUserToast(`${toggleEnabled ? 'enabled' : 'disabled'} postgres trigger ${initialPath}`) - } - } - - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid) - }) - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(postgresConfig, initialConfig, saveDisabled, edit, onConfigChange) - } - }) -{#if useDrawer} - - - {@render actions()} - {@render content()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} + + + + {#if !drawerLoading && can_write} + {#if edit} +
+ { + await PostgresTriggerService.setPostgresTriggerEnabled({ + path: initialPath, + workspace: $workspaceStore ?? '', + requestBody: { enabled: e.detail } + }) + sendUserToast( + `${e.detail ? 'enabled' : 'disabled'} postgres trigger ${initialPath}` + ) + }} + /> +
+ {/if} + {/if}
- - {@render actions()} - - {@render content()} -
-{/if} - -{#snippet actions()} - {#if !drawerLoading} - - {/if} -{/snippet} - -{#snippet content()} - {#if drawerLoading} - {#if showLoading} - -

Loading...

- {/if} - {:else} -
- {#if description} - {@render description()} - {/if} - {#if !hideTooltips} - - {#if edit} - Changes can take up to 30 seconds to take effect. - {:else} - New postgres triggers can take up to 30 seconds to start listening. - {/if} - - {/if} -
-
- - {#if !hideTarget} + {#if drawerLoading} +
+ +

Loading...

+
+ {:else} + + {#if edit} + Changes can take up to 30 seconds to take effect. + {:else} + New postgres triggers can take up to 30 seconds to start listening. + {/if} + +
+

Pick a script or flow to be triggered @@ -496,8 +355,7 @@ allowFlow={true} bind:itemKind bind:scriptPath={script_path} - allowRefresh={can_write} - allowEdit={!$userStore?.operator} + allowRefresh /> {#if emptyStringTrimmed(script_path) && is_flow === false} @@ -522,239 +380,218 @@ {/if}

- {/if} -
- - {#if isEditor} - - {/if} - -

- Pick a database to connect to -

-
-
- - -
- - {#if postgres_resource_path} -
+ + + + + + {/if} + + + + {/if} + + diff --git a/frontend/src/lib/components/triggers/postgres/PostgresTriggersPanel.svelte b/frontend/src/lib/components/triggers/postgres/PostgresTriggersPanel.svelte index 0a007d6db0..cd707bb5d3 100644 --- a/frontend/src/lib/components/triggers/postgres/PostgresTriggersPanel.svelte +++ b/frontend/src/lib/components/triggers/postgres/PostgresTriggersPanel.svelte @@ -1,67 +1,125 @@ -
- - {#snippet description()} - {#if cloudDisabled} - - Postgres triggers are disabled in the multi-tenant cloud. - - {:else} - - Windmill can connect to a Postgres database and trigger runnables (scripts, flows) in - response to database transactions (INSERT, UPDATE, DELETE) on specified tables, schemas, - or the entire database. Listening is done using Postgres's logical replication streaming - protocol, ensuring efficient and low-latency triggering. - - {/if} - {/snippet} - -
+ { + loadTriggers() + }} + bind:this={postgresTriggerEditor} +/> +{#if isCloudHosted()} + + Postgres triggers are disabled in the multi-tenant cloud. + +{:else} +
+ + Windmill can connect to a Postgres database and trigger runnables (scripts, flows) in response + to database transactions (INSERT, UPDATE, DELETE) on specified tables, schemas, or the entire + database. Listening is done using Postgres's logical replication streaming protocol, ensuring + efficient and low-latency triggering. + + + {#if !newItem && postgresTriggers && postgresTriggers.length > 0} +
+
+
+ {#each postgresTriggers as postgresTriggers (postgresTriggers.path)} +
+
{postgresTriggers.path}
+
+ +
+
+ {/each} +
+
+
+ {/if} + { + postgresTriggerEditor?.openNew(isFlow, path, e.detail.config) + }} + on:addPreprocessor + on:updateSchema + on:testWithArgs + cloudDisabled={false} + triggerType="postgres" + {isFlow} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + /> +
+{/if} diff --git a/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte b/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte index 840d9a6856..77b168a0f8 100644 --- a/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte +++ b/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte @@ -16,7 +16,6 @@ export let postgres_resource_path: string = '' export let relations: Relations[] | undefined = undefined export let transaction_to_track: string[] = [] - export let disabled: boolean = false async function listDatabasePublication() { try { @@ -85,17 +84,18 @@ listDatabasePublication() let darkMode = false +
+
{#each v.table_to_track as table_to_track, j} @@ -92,7 +90,6 @@ type="text" bind:value={table_to_track.table_name} class="!bg-surface mt-1" - {disabled} /> @@ -131,7 +128,6 @@ selected={table_to_track.columns_name ?? []} placeholder="Select columns" --sms-options-margin="4px" - {disabled} onchange={(e) => { const option = e.option?.toString() if (e.type === 'add') { @@ -185,7 +181,6 @@ type="text" bind:value={table_to_track.where_clause} class="!bg-surface mt-1" - {disabled} />
{/each} @@ -208,14 +202,12 @@ on:add={({ detail }) => { addTable(detail.name, i) }} - {disabled} >
+ @@ -266,13 +258,12 @@ invalidRelations(appendedRelations, { showError: true, trackSchemaTableError: false - }) === '' + }) === false ) { relations = appendedRelations } } }} - {disabled} > diff --git a/frontend/src/lib/components/triggers/postgres/utils.ts b/frontend/src/lib/components/triggers/postgres/utils.ts index 8122da726d..6b4e3e91b5 100644 --- a/frontend/src/lib/components/triggers/postgres/utils.ts +++ b/frontend/src/lib/components/triggers/postgres/utils.ts @@ -1,8 +1,6 @@ -import { PostgresTriggerService, type Relations } from '$lib/gen' +import type { Relations } from '$lib/gen' import { sendUserToast } from '$lib/toast' import { emptyString } from '$lib/utils' -import type { Writable } from 'svelte/store' -import { get } from 'svelte/store' type RelationError = { schemaIndex: number @@ -20,9 +18,7 @@ export function invalidRelations( trackSchemaTableError?: boolean showError?: boolean } -): string { - let errorMessage: string = '' - +): boolean { let error: RelationError = { schemaIndex: -1, tableIndex: -1, @@ -86,6 +82,8 @@ export function invalidRelations( error.trackAllTablesInSchema && error.trackSpecificColumnsInTable) if ((options?.showError ?? false) && errorFound) { + let errorMessage: string = '' + if (error.schemaError) { errorMessage = `Schema Error: Please enter a name for schema number ${error.schemaIndex}` } else if (error.tableError) { @@ -97,53 +95,8 @@ export function invalidRelations( errorMessage = 'Configuration Error: Schema-level tracking and specific table tracking with column selection cannot be used together. Refer to the documentation for valid configurations.' } + sendUserToast(errorMessage, true) } - return errorMessage -} - -export async function savePostgresTriggerFromCfg( - initialPath: string, - config: Record, - edit: boolean, - workspace: string, - usedTriggerKinds: Writable -): Promise { - try { - const requestBody = { - path: config.path, - script_path: config.script_path, - is_flow: config.is_flow, - postgres_resource_path: config.postgres_resource_path, - replication_slot_name: config.replication_slot_name, - publication_name: config.publication_name, - publication: config.publication, - enabled: config.enabled - } - if (edit) { - await PostgresTriggerService.updatePostgresTrigger({ - workspace: workspace, - path: initialPath, - requestBody - }) - sendUserToast(`PostgresTrigger ${config.path} updated`) - } else { - await PostgresTriggerService.createPostgresTrigger({ - workspace: workspace, - requestBody: { - ...requestBody, - enabled: true - } - }) - sendUserToast(`PostgresTrigger ${config.path} created`) - } - - if (!get(usedTriggerKinds).includes('postgres')) { - usedTriggerKinds.update((t) => [...t, 'postgres']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } + return errorFound } diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte index c75e215e79..f70e3a48cb 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte @@ -2,9 +2,7 @@ import { tick } from 'svelte' import ScheduleEditorInner from './ScheduleEditorInner.svelte' - let { onUpdate }: { onUpdate?: (path?: string) => void } = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() @@ -18,12 +16,12 @@ ) { open = true await tick() - drawer?.openNew(is_flow, initial_script_path, undefined, schedule_path) + drawer?.openNew(is_flow, initial_script_path, schedule_path) } - let drawer: ScheduleEditorInner | undefined = $state() + let drawer: ScheduleEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index 91dc02c2b0..69fe6c77d4 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -23,122 +23,77 @@ import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, formatCron, sendUserToast, cronV1toV2 } from '$lib/utils' import { base } from '$lib/base' + import { createEventDispatcher } from 'svelte' import Section from '$lib/components/Section.svelte' import { List, Loader2, Save, AlertTriangle } from 'lucide-svelte' import autosize from '$lib/autosize' - import TriggerEditorToolbar from '$lib/components/triggers/TriggerEditorToolbar.svelte' - import { saveScheduleFromCfg } from '$lib/components/flows/scheduleUtils' import DateTimeInput from '$lib/components/DateTimeInput.svelte' import FlowRetries from '$lib/components/flows/content/FlowRetries.svelte' import Label from '$lib/components/Label.svelte' import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte' import { runScheduleNow } from '../scheduled/utils' - import { handleConfigChange } from '../utils' - - let { - useDrawer = true, - hideTarget = false, - docDescription = undefined, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - primary = false, - draftSchema = undefined, - customLabel = undefined, - isDeployed = false, - onUpdate = undefined, - onConfigChange = undefined, - onDelete = undefined, - onReset = undefined - } = $props() let optionTabSelected: 'error_handler' | 'recovery_handler' | 'success_handler' | 'retries' = - $state('error_handler') - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let schedule: string = $state('0 0 12 * *') - let cronVersion: string = $state('v2') - let isLatestCron = $state(true) - let initialCronVersion: string = $state('v2') + 'error_handler' + + let is_flow: boolean = false + let initialPath = '' + let edit = true + let schedule: string = '0 0 12 * *' + let cronVersion: string = 'v2' + let isLatestCron = true + let initialCronVersion: string = 'v2' let initialSchedule: string - let timezone: string = $state(Intl.DateTimeFormat().resolvedOptions().timeZone) - let paused_until: string | undefined = $state(undefined) - let itemKind: 'flow' | 'script' = $state('script') - let errorHandleritemKind: 'flow' | 'script' = $state('script') - let wsErrorHandlerMuted: boolean = $state(false) - let errorHandlerPath: string | undefined = $state(undefined) - let errorHandlerCustomInitialPath: string | undefined = $state(undefined) - let errorHandlerSelected: 'custom' | 'slack' | 'teams' = $state('slack') - let errorHandlerExtraArgs: Record = $state({}) - let recoveryHandlerPath: string | undefined = $state(undefined) - let recoveryHandlerCustomInitialPath: string | undefined = $state(undefined) - let recoveryHandlerSelected: 'custom' | 'slack' | 'teams' = $state('slack') - let recoveryHandlerItemKind: 'flow' | 'script' = $state('script') - let recoveryHandlerExtraArgs: Record = $state({}) - let successHandlerPath: string | undefined = $state(undefined) - let successHandlerCustomInitialPath: string | undefined = $state(undefined) - let successHandlerSelected: 'custom' | 'slack' | 'teams' = $state('slack') - let successHandlerItemKind: 'flow' | 'script' = $state('script') - let successHandlerExtraArgs: Record = $state({}) - let failedTimes = $state(1) - let failedExact = $state(false) - let recoveredTimes = $state(1) - let retry: Retry | undefined = $state(undefined) - let script_path = $state('') - let initialScriptPath = $state('') - let runnable: Script | Flow | undefined = $state() - let args: Record = $state({}) - let loading = $state(false) - let drawerLoading = $state(true) - let showLoading = $state(false) - let initialConfig: Record | undefined = undefined - let extraPerms: Record = $state({}) - let can_write = $state(true) - let initNewPath = $state(false) - let path: string = $state('') - let enabled: boolean = $state(false) - let pathError = $state('') - let summary = $state('') - let description = $state('') - let no_flow_overlap = $state(false) - let tag: string | undefined = $state(undefined) - let validCRON = $state(true) - let isValid = $state(true) - let allowSchedule = $derived(isValid && validCRON && script_path != '') - let deploymentLoading = $state(false) + let timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone + let paused_until: string | undefined = undefined - const saveDisabled = $derived( - !allowSchedule || - pathError != '' || - emptyString(script_path) || - (errorHandlerSelected == 'slack' && - !emptyString(errorHandlerPath) && - emptyString(errorHandlerExtraArgs['channel'])) || - !can_write - ) - const scheduleCfg = $derived.by(getScheduleCfg) + let itemKind: 'flow' | 'script' = 'script' + let errorHandleritemKind: 'flow' | 'script' = 'script' + let wsErrorHandlerMuted: boolean = false + let errorHandlerPath: string | undefined = undefined + let errorHandlerCustomInitialPath: string | undefined = undefined + let errorHandlerSelected: 'custom' | 'slack' | 'teams' = 'slack' + let errorHandlerExtraArgs: Record = {} + let recoveryHandlerPath: string | undefined = undefined + let recoveryHandlerCustomInitialPath: string | undefined = undefined + let recoveryHandlerSelected: 'custom' | 'slack' | 'teams' = 'slack' + let recoveryHandlerItemKind: 'flow' | 'script' = 'script' + let recoveryHandlerExtraArgs: Record = {} + let successHandlerPath: string | undefined = undefined + let successHandlerCustomInitialPath: string | undefined = undefined + let successHandlerSelected: 'custom' | 'slack' | 'teams' = 'slack' + let successHandlerItemKind: 'flow' | 'script' = 'script' + let successHandlerExtraArgs: Record = {} + let failedTimes = 1 + let failedExact = false + let recoveredTimes = 1 + let duplicate = false + let retry: Retry | undefined = undefined - export async function openEdit(ePath: string, isFlow: boolean, defaultCfg?: Record) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms + let script_path = '' + let initialScriptPath = '' + + let runnable: Script | Flow | undefined + let args: Record = {} + + let loading = false + + let drawerLoading = true + export function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() is_flow = isFlow initialPath = ePath itemKind = is_flow ? 'flow' : 'script' - path = defaultCfg?.path ?? ePath - await loadSchedule(defaultCfg) + if (path == ePath) { + loadSchedule() + } else { + path = ePath + } edit = true } finally { - if (!defaultCfg) { - initialConfig = structuredClone($state.snapshot(getScheduleCfg())) - } - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } @@ -192,7 +147,8 @@ successHandlerSelected = 'slack' successHandlerExtraArgs = {} } - } else { + } + else { let defaultErrorHandlerMaybe = undefined let defaultRecoveryHandlerMaybe = undefined let defaultSuccessHandlerMaybe = undefined @@ -265,12 +221,8 @@ export async function openNew( nis_flow: boolean, initial_script_path?: string, - defaultValues?: Schedule, schedule_path?: string ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms drawerLoading = true try { let s: Schedule | undefined @@ -279,9 +231,7 @@ workspace: $workspaceStore!, path: schedule_path }) - initNewPath = true - } else if (defaultValues) { - s = defaultValues + duplicate = true } drawer?.openDrawer() runnable = undefined @@ -289,7 +239,7 @@ edit = false itemKind = is_flow ? 'flow' : 'script' initialScriptPath = initial_script_path ?? '' - path = initNewPath ? '' : (defaultValues?.path ?? initialScriptPath) + path = duplicate === true ? '' : initialScriptPath initialPath = path cronVersion = s?.cron_version ?? 'v2' @@ -314,9 +264,7 @@ await setScheduleHandler(s) } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } @@ -326,18 +274,28 @@ } } - $effect(() => { - ;(is_flow = itemKind == 'flow') && resetRetries() - }) + $: (is_flow = itemKind == 'flow') && resetRetries() + + let isValid = true + + let path: string = '' + let enabled: boolean = false + let pathError = '' + let summary = '' + let description = '' + let no_flow_overlap = false + let tag: string | undefined = undefined + + let validCRON = true + $: allowSchedule = isValid && validCRON && script_path != '' // set isValid to true when a script/flow without any properties is selected - $effect(() => { - setDefaultValid(draftSchema ?? runnable?.schema) - }) + $: runnable?.schema && + runnable.schema.properties && + Object.keys(runnable.schema.properties).length === 0 && + (isValid = true) - function setDefaultValid(schema: Record | undefined) { - isValid = schema?.properties && Object.keys(schema.properties).length === 0 - } + const dispatch = createEventDispatcher() async function loadScript(p: string | undefined): Promise { if (p) { @@ -437,108 +395,129 @@ } } - async function loadSchedule(defaultCfg?: Record): Promise { - if (!defaultCfg) { - try { - const s = await ScheduleService.getSchedule({ - workspace: $workspaceStore!, - path: initialPath - }) - await loadScheduleCfg(s) - } catch (err) { - sendUserToast(`Could not load schedule: ${err}`, true) - } - } else { - await loadScheduleCfg(defaultCfg) - } - } - - async function loadScheduleCfg(cfg: Record): Promise { + let can_write = true + async function loadSchedule(): Promise { loading = true + try { + const s = await ScheduleService.getSchedule({ + workspace: $workspaceStore!, + path: initialPath + }) + is_flow = s.is_flow + cronVersion = s.cron_version ?? 'v2' + initialCronVersion = cronVersion + isLatestCron = cronVersion == 'v2' + enabled = s.enabled + schedule = s.schedule + initialSchedule = schedule + timezone = s.timezone + paused_until = s.paused_until + showPauseUntil = paused_until !== undefined + summary = s.summary ?? '' + description = s.description ?? '' + script_path = s.script_path ?? '' + args = s.args ?? {} + can_write = canWrite(s.path, s.extra_perms, $userStore) + tag = s.tag - cronVersion = cfg.cron_version ?? 'v2' - initialCronVersion = cronVersion - isLatestCron = cronVersion == 'v2' - enabled = cfg.enabled - schedule = cfg.schedule - initialSchedule = schedule - timezone = cfg.timezone - paused_until = cfg.paused_until - showPauseUntil = paused_until !== undefined - summary = cfg.summary ?? '' - description = cfg.description ?? '' - script_path = cfg.script_path ?? '' - await loadScript(script_path) + await loadScript(script_path) - is_flow = cfg.is_flow - no_flow_overlap = cfg.no_flow_overlap ?? false - wsErrorHandlerMuted = cfg.ws_error_handler_muted ?? false - retry = cfg.retry - if (cfg.on_failure) { - let splitted = cfg.on_failure.split('/') - errorHandleritemKind = splitted[0] as 'flow' | 'script' - errorHandlerPath = splitted.slice(1)?.join('/') - errorHandlerCustomInitialPath = errorHandlerPath - failedTimes = cfg.on_failure_times ?? 1 - failedExact = cfg.on_failure_exact ?? false - errorHandlerExtraArgs = cfg.on_failure_extra_args ?? {} - errorHandlerSelected = getHandlerType('error', errorHandlerPath ?? '') - } else { - errorHandlerPath = undefined - errorHandleritemKind = 'script' - errorHandlerCustomInitialPath = undefined - errorHandlerExtraArgs = {} - failedExact = false - failedTimes = 1 - errorHandlerSelected = 'slack' + no_flow_overlap = s.no_flow_overlap ?? false + wsErrorHandlerMuted = s.ws_error_handler_muted ?? false + retry = s.retry + await setScheduleHandler(s) + } catch (err) { + sendUserToast(`Could not load schedule: ${err}`, true) } - if (cfg.on_recovery) { - let splitted = cfg.on_recovery.split('/') - recoveryHandlerItemKind = splitted[0] as 'flow' | 'script' - recoveryHandlerPath = splitted.slice(1)?.join('/') - recoveryHandlerCustomInitialPath = recoveryHandlerPath - recoveredTimes = cfg.on_recovery_times ?? 1 - recoveryHandlerExtraArgs = cfg.on_recovery_extra_args ?? {} - recoveryHandlerSelected = getHandlerType('recovery', recoveryHandlerPath ?? '') - } else { - recoveryHandlerPath = undefined - recoveryHandlerItemKind = 'script' - recoveryHandlerCustomInitialPath = undefined - recoveredTimes = 1 - recoveryHandlerSelected = 'slack' - recoveryHandlerExtraArgs = {} - } - if (cfg.on_success) { - let splitted = cfg.on_success.split('/') - successHandlerItemKind = splitted[0] as 'flow' | 'script' - successHandlerPath = splitted.slice(1)?.join('/') - successHandlerCustomInitialPath = successHandlerPath - successHandlerExtraArgs = cfg.on_success_extra_args ?? {} - successHandlerSelected = getHandlerType('success', successHandlerPath ?? '') - } else { - successHandlerPath = undefined - successHandlerItemKind = 'script' - successHandlerCustomInitialPath = undefined - successHandlerSelected = 'slack' - successHandlerExtraArgs = {} - } - args = cfg.args ?? {} - extraPerms = cfg.extra_perms ?? {} - can_write = canWrite(cfg.path, cfg.extra_perms, $userStore) - tag = cfg.tag - loading = false } async function scheduleScript(): Promise { - const scheduleCfg = getScheduleCfg() - deploymentLoading = true - const isSaved = await saveScheduleFromCfg(scheduleCfg, edit, $workspaceStore!) - if (isSaved) { - onUpdate?.(scheduleCfg.path) - drawer?.closeDrawer() + if (errorHandlerPath !== undefined && isSlackHandler('error', errorHandlerPath)) { + errorHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token' + } else { + errorHandlerExtraArgs['slack'] = undefined } - deploymentLoading = false + if (recoveryHandlerPath !== undefined && isSlackHandler('recovery', recoveryHandlerPath)) { + recoveryHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token' + } else { + recoveryHandlerExtraArgs['slack'] = undefined + } + if (successHandlerPath !== undefined && isSlackHandler('success', successHandlerPath)) { + successHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token' + } else { + successHandlerExtraArgs['slack'] = undefined + } + if (edit) { + await ScheduleService.updateSchedule({ + workspace: $workspaceStore!, + path: initialPath, + requestBody: { + schedule: formatCron(schedule), + timezone, + args, + on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined, + on_failure_times: failedTimes, + on_failure_exact: failedExact, + on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined, + on_recovery: recoveryHandlerPath + ? `${recoveryHandlerItemKind}/${recoveryHandlerPath}` + : undefined, + on_recovery_times: recoveredTimes, + on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {}, + on_success: successHandlerPath + ? `${successHandlerItemKind}/${successHandlerPath}` + : undefined, + on_success_extra_args: successHandlerPath ? successHandlerExtraArgs : {}, + ws_error_handler_muted: wsErrorHandlerMuted, + retry: retry, + summary: summary != '' ? summary : undefined, + description: description, + no_flow_overlap: no_flow_overlap, + tag: tag, + paused_until: paused_until, + cron_version: cronVersion + } + }) + sendUserToast(`Schedule ${path} updated`) + } else { + await ScheduleService.createSchedule({ + workspace: $workspaceStore!, + requestBody: { + path, + schedule: formatCron(schedule), + timezone, + script_path, + is_flow, + args, + enabled: true, + on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined, + on_failure_times: failedTimes, + on_failure_exact: failedExact, + on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined, + on_recovery: recoveryHandlerPath + ? `${recoveryHandlerItemKind}/${recoveryHandlerPath}` + : undefined, + on_recovery_times: recoveredTimes, + on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {}, + on_success: successHandlerPath + ? `${successHandlerItemKind}/${successHandlerPath}` + : undefined, + on_success_extra_args: successHandlerPath ? successHandlerExtraArgs : {}, + ws_error_handler_muted: wsErrorHandlerMuted, + retry: retry, + summary: summary != '' ? summary : undefined, + description: description, + no_flow_overlap: no_flow_overlap, + tag: tag, + paused_until: paused_until, + cron_version: cronVersion + } + }) + sendUserToast(`Schedule ${path} created`) + } + dispatch('update') + drawer.closeDrawer() } function getHandlerType( @@ -583,15 +562,21 @@ } } - let drawer: Drawer | undefined = $state() + $: { + if ($workspaceStore) { + if (edit && path != '') { + loadSchedule() + } + } + } - let pathC: Path | undefined = $state() - let dirtyPath = $state(false) + let drawer: Drawer - let showPauseUntil = $state(false) - $effect(() => { - !showPauseUntil && (paused_until = undefined) - }) + let pathC: Path + let dirtyPath = false + + let showPauseUntil = false + $: !showPauseUntil && (paused_until = undefined) function onVersionChange() { cronVersion = isLatestCron ? 'v2' : 'v1' @@ -607,99 +592,20 @@ schedule = initialSchedule } } - - function getScheduleCfg(): Record { - let errorHadlerExtraArgsDerived = structuredClone($state.snapshot(errorHandlerExtraArgs)) - if (errorHandlerPath !== undefined && isSlackHandler('error', errorHandlerPath)) { - errorHadlerExtraArgsDerived['slack'] = '$res:f/slack_bot/bot_token' - } else { - errorHadlerExtraArgsDerived['slack'] = undefined - } - - let recoveryHandlerExtraArgsDerived = structuredClone($state.snapshot(recoveryHandlerExtraArgs)) - if (recoveryHandlerPath !== undefined && isSlackHandler('recovery', recoveryHandlerPath)) { - recoveryHandlerExtraArgsDerived['slack'] = '$res:f/slack_bot/bot_token' - } else { - recoveryHandlerExtraArgsDerived['slack'] = undefined - } - - let successHandlerExtraArgsDerived = structuredClone($state.snapshot(successHandlerExtraArgs)) - if (successHandlerPath !== undefined && isSlackHandler('success', successHandlerPath)) { - successHandlerExtraArgsDerived['slack'] = '$res:f/slack_bot/bot_token' - } else { - successHandlerExtraArgsDerived['slack'] = undefined - } - return { - path: path, - schedule: formatCron(schedule), - timezone: timezone, - script_path: script_path, - is_flow: is_flow, - args: args, - enabled: enabled, - on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined, - on_failure_times: failedTimes, - on_failure_exact: failedExact, - on_failure_extra_args: errorHandlerPath ? errorHadlerExtraArgsDerived : undefined, - on_recovery: recoveryHandlerPath - ? `${recoveryHandlerItemKind}/${recoveryHandlerPath}` - : undefined, - on_recovery_times: recoveredTimes, - on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgsDerived : {}, - on_success: successHandlerPath - ? `${successHandlerItemKind}/${successHandlerPath}` - : undefined, - on_success_extra_args: successHandlerPath ? successHandlerExtraArgsDerived : {}, - ws_error_handler_muted: wsErrorHandlerMuted, - retry: retry, - summary: summary != '' ? summary : undefined, - description: description, - no_flow_overlap: no_flow_overlap, - tag: tag, - paused_until: paused_until, - cron_version: cronVersion, - extra_perms: extraPerms - } - } - - async function handleToggleEnabled(nEnabled: boolean) { - enabled = nEnabled - if (!isDraftOnly && !hasDraft) { - await ScheduleService.setScheduleEnabled({ - path: initialPath, - workspace: $workspaceStore ?? '', - requestBody: { enabled: nEnabled } - }) - sendUserToast(`${nEnabled ? 'enabled' : 'disabled'} schedule ${initialPath}`) - } - } - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(scheduleCfg, initialConfig, saveDisabled, edit, onConfigChange) - } - }) -{#snippet saveButton()} - {#if !drawerLoading} - - {#snippet extra()} - {#if !drawerLoading && edit} + + + + {#if !drawerLoading} + {#if edit}
- {/if} - {/snippet} -
- {/if} -{/snippet} - -{#snippet content()} - {#if drawerLoading} - {#if showLoading} - - {/if} - {:else} -
-
-
-

Metadata

- -
-
- {#snippet header()} - {#if cronVersion === 'v1'} - Schedules use CRON syntax. Seconds are mandatory. - {:else} - Schedules use extended CRON syntax. - {/if} - {/snippet} - {#if initialCronVersion !== 'v2'} -
- - + {/if} + + {/if} + + {#if drawerLoading} + + {:else} +
+
+
+

Metadata

+
- {/if} - - - {#if showPauseUntil} - - {/if} -
+ -
- {#if !hideTarget} + +
+ +
+ + {#if cronVersion === 'v1'} + Schedules use CRON syntax. Seconds are mandatory. + {:else} + Schedules use extended CRON syntax. + {/if} + + {#if initialCronVersion !== 'v2'} +
+ + +
+ {/if} + + + {#if showPauseUntil} + + {/if} +
+
{#if !edit}

Pick a script or flow to be triggered by the schedule

{/if} - {/if} -
- {#if !loading} - {#if runnable || draftSchema} - {@const schema = draftSchema ?? runnable?.schema} - {#if schema && schema.properties && Object.keys(schema.properties).length > 0} - {#await import('$lib/components/SchemaForm.svelte')} - - {:then Module} - - {/await} +
+ {#if !loading} + {#if runnable} + {#if runnable?.schema && runnable.schema.properties && Object.keys(runnable.schema.properties).length > 0} + {#await import('$lib/components/SchemaForm.svelte')} + + {:then Module} + + {/await} + {:else} +
+ This {is_flow ? 'flow' : 'script'} takes no argument +
+ {/if} + {:else if script_path != ''} +
+ You cannot see the the {is_flow ? 'flow' : 'script'} input form as you do not have + access to it. +
{:else} -
- This {is_flow ? 'flow' : 'script'} takes no argument +
+ Pick a {is_flow ? 'flow' : 'script'} and fill its argument here
{/if} - {:else if script_path != ''} -
- You cannot see the the {is_flow ? 'flow' : 'script'} input form as you do not have access - to it. -
{:else} -
- Pick a {is_flow ? 'flow' : 'script'} and fill its argument here -
+ + {/if} +
+ + +
+ {#if !loading} + + Error Handler + Recovery Handler + Success Handler + {#if itemKind === 'script'} + Retries + Custom tag + {/if} + +
+ {#if optionTabSelected === 'error_handler'} +
+ +
+ {#if !$enterpriseLicense}(ee only){/if} +
+
+ +
+ defaults + saveAsDefaultErrorHandler(false) + }, + { + displayName: 'Override all existing', + type: 'delete', + action: () => saveAsDefaultErrorHandler(true) + } + ]} + > + + + Set as default + + +
+
+
+ +
+ + + + +
+
The following args will be passed to the error handler: +
    +
  • path: The path of the script or flow that failed.
  • +
  • is_flow: Whether the runnable is a flow.
  • +
  • schedule_path: The path of the schedule.
  • +
  • error: The error details.
  • +
  • failed_times: Minimum number of times the schedule failed + before calling the error handler.
  • +
  • started_at: The start datetime of the latest job that failed.
  • +
+
+
+
+
+
+
+
+

+ Triggered when schedule failed

+ + +

time{failedTimes > 1 ? 's in a row' : ''}

+
+
+
+ {:else if optionTabSelected === 'recovery_handler'} + {@const disabled = !can_write || emptyString($enterpriseLicense)} +
+ +
+ {#if !$enterpriseLicense}(ee only){/if} +
+
+ +
+ defaults + saveAsDefaultRecoveryHandler(false) + }, + { + displayName: 'Override all existing', + type: 'delete', + action: () => saveAsDefaultRecoveryHandler(true) + } + ]} + > + + + Set as default + + +
+
+ + + +
+
The following args will be passed to the recovery handler: +
    +
  • path: The path of the script or flow that recovered.
  • +
  • is_flow: Whether the runnable is a flow.
  • +
  • schedule_path: The path of the schedule.
  • +
  • error: The error of the last job that errored
  • +
  • error_started_at: The start datetime of the last job that + errored
  • +
  • success_times: The number of times the schedule succeeded + before calling the recovery handler.
  • +
  • success_result: The result of the latest successful job
  • +
  • success_started_at: The start datetime of the latest + successful job
  • +
+
+
+
+
+
+
+
+

Triggered when schedule recovered

+ +

time{recoveredTimes > 1 ? 's in a row' : ''}

+
+
+
+ {:else if optionTabSelected === 'success_handler'} + {@const disabled = !can_write || emptyString($enterpriseLicense)} +
+ +
+ {#if !$enterpriseLicense}(ee only){/if} +
+
+ +
+ defaults + saveAsDefaultSuccessHandler(false) + }, + { + displayName: 'Override all existing', + type: 'delete', + action: () => saveAsDefaultSuccessHandler(true) + } + ]} + > + + + Set as default + + +
+
+ + + +
+
The following args will be passed to the success handler: +
    +
  • path: The path of the script or flow that succeeded.
  • +
  • is_flow: Whether the runnable is a flow.
  • +
  • schedule_path: The path of the schedule.
  • +
  • success_result: The result of the successful job
  • +
  • success_started_at: The start datetime of the successful job
  • +
+
+
+
+
+
+
+ {:else if optionTabSelected === 'retries'} + {@const disabled = !can_write || emptyString($enterpriseLicense)} +
+ +
+ {#if !$enterpriseLicense}(ee only){/if} +
+ + If defined, upon error this schedule will be retried with a delay and a maximum + number of attempts as defined below. +
+ This is only available for individual script. For flows, retries can be set on each + flow step in the flow editor. +
+
+ +
+ {:else if optionTabSelected === 'tag'} +
+ +
{/if} {:else} {/if}
- - -
- {@render errorHandler()} -
-
- {/if} -{/snippet} - -{#snippet errorHandler()} -
- {#if !loading} - - Error Handler - Recovery Handler - Success Handler - {#if itemKind === 'script'} - Retries - Custom tag - {/if} - -
- {#if optionTabSelected === 'error_handler'} -
- {#snippet header()} -
- {#if !$enterpriseLicense}(ee only){/if} -
- {/snippet} - {#snippet action()} -
- defaults - saveAsDefaultErrorHandler(false) - }, - { - displayName: 'Override all existing', - type: 'delete', - action: () => saveAsDefaultErrorHandler(true) - } - ]} - > - {#snippet children()} - - Set as default - {/snippet} - -
- {/snippet} -
- -
- - - - - -
-
The following args will be passed to the error handler: -
    -
  • path: The path of the script or flow that failed.
  • -
  • is_flow: Whether the runnable is a flow.
  • -
  • schedule_path: The path of the schedule.
  • -
  • error: The error details.
  • -
  • failed_times: Minimum number of times the schedule failed before - calling the error handler.
  • -
  • started_at: The start datetime of the latest job that failed.
  • -
-
-
-
-
-
-
-
-

- Triggered when schedule failed

- - -

time{failedTimes > 1 ? 's in a row' : ''}

-
-
-
- {:else if optionTabSelected === 'recovery_handler'} - {@const disabled = !can_write || emptyString($enterpriseLicense)} -
- {#snippet header()} -
- {#if !$enterpriseLicense}(ee only){/if} -
- {/snippet} - {#snippet action()} -
- defaults - saveAsDefaultRecoveryHandler(false) - }, - { - displayName: 'Override all existing', - type: 'delete', - action: () => saveAsDefaultRecoveryHandler(true) - } - ]} - > - {#snippet children()} - - Set as default - {/snippet} - -
- {/snippet} - - - - -
-
The following args will be passed to the recovery handler: -
    -
  • path: The path of the script or flow that recovered.
  • -
  • is_flow: Whether the runnable is a flow.
  • -
  • schedule_path: The path of the schedule.
  • -
  • error: The error of the last job that errored
  • -
  • error_started_at: The start datetime of the last job that errored
  • -
  • success_times: The number of times the schedule succeeded before - calling the recovery handler.
  • -
  • success_result: The result of the latest successful job
  • -
  • success_started_at: The start datetime of the latest successful job
  • -
-
-
-
-
-
-
-
-

Triggered when schedule recovered

- -

time{recoveredTimes > 1 ? 's in a row' : ''}

-
-
-
- {:else if optionTabSelected === 'success_handler'} - {@const disabled = !can_write || emptyString($enterpriseLicense)} -
- {#snippet header()} -
- {#if !$enterpriseLicense}(ee only){/if} -
- {/snippet} - {#snippet action()} -
- defaults - saveAsDefaultSuccessHandler(false) - }, - { - displayName: 'Override all existing', - type: 'delete', - action: () => saveAsDefaultSuccessHandler(true) - } - ]} - > - {#snippet children()} - - Set as default - {/snippet} - -
- {/snippet} - - - - -
-
The following args will be passed to the success handler: -
    -
  • path: The path of the script or flow that succeeded.
  • -
  • is_flow: Whether the runnable is a flow.
  • -
  • schedule_path: The path of the schedule.
  • -
  • success_result: The result of the successful job
  • -
  • success_started_at: The start datetime of the successful job
  • -
-
-
-
-
-
-
- {:else if optionTabSelected === 'retries'} - {@const disabled = !can_write || emptyString($enterpriseLicense)} -
- {#snippet header()} -
- {#if !$enterpriseLicense}(ee only){/if} -
- - If defined, upon error this schedule will be retried with a delay and a maximum number - of attempts as defined below. -
- This is only available for individual script. For flows, retries can be set on each flow - step in the flow editor. -
- {/snippet} - -
- {:else if optionTabSelected === 'tag'} -
- -
- {/if} - {:else} - - {/if} -
-{/snippet} - -{#if useDrawer} - - - -
- {@render saveButton()} -
-
- {@render content()} -
-
-{:else} -
- - {#if customLabel} - {@render customLabel()} - {/if} - - -
- {@render saveButton()}
-
- {#if docDescription} - {@render docDescription()} {/if} - {@render content()} -
-{/if} + + diff --git a/frontend/src/lib/components/triggers/sqs/SqsCapture.svelte b/frontend/src/lib/components/triggers/sqs/SqsCapture.svelte deleted file mode 100644 index d61e663d0c..0000000000 --- a/frontend/src/lib/components/triggers/sqs/SqsCapture.svelte +++ /dev/null @@ -1,52 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} -

- Listening to SQS messages... -

- {:else} -

- Start capturing to listen to SQS messages. -

- {/if} - {/snippet} -
-{/if} diff --git a/frontend/src/lib/components/triggers/sqs/SqsTriggerEditor.svelte b/frontend/src/lib/components/triggers/sqs/SqsTriggerEditor.svelte index 2e1cc47727..2de3e17567 100644 --- a/frontend/src/lib/components/triggers/sqs/SqsTriggerEditor.svelte +++ b/frontend/src/lib/components/triggers/sqs/SqsTriggerEditor.svelte @@ -2,9 +2,7 @@ import { tick } from 'svelte' import SqsTriggerEditorInner from './SqsTriggerEditorInner.svelte' - let { onUpdate } = $props() - - let open = $state(false) + let open = false export async function openEdit(ePath: string, isFlow: boolean) { open = true await tick() @@ -21,9 +19,9 @@ drawer?.openNew(is_flow, initial_script_path, defaultValues) } - let drawer: SqsTriggerEditorInner | undefined = $state() + let drawer: SqsTriggerEditorInner {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorConfigSection.svelte b/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorConfigSection.svelte index 55282e8762..3de41919fb 100644 --- a/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorConfigSection.svelte @@ -1,5 +1,7 @@
+ {#if showCapture && captureInfo} + + {/if}
- - {#if showTestingBadge} - - {/if} -
@@ -53,13 +64,9 @@ Select an AWS resource to authenticate your account.

- { - aws_resource_path = '' - }} - let:item - > + { + aws_resource_path = '' + }} let:item> diff --git a/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorInner.svelte index c23a683b2c..eee25de840 100644 --- a/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/sqs/SqsTriggerEditorInner.svelte @@ -5,96 +5,40 @@ import Path from '$lib/components/Path.svelte' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptyString, sendUserToast } from '$lib/utils' - import { Loader2 } from 'lucide-svelte' + import { createEventDispatcher } from 'svelte' + import { Loader2, Save } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' + import Toggle from '$lib/components/Toggle.svelte' import { SqsTriggerService, type AwsAuthResourceType } from '$lib/gen' import SqsTriggerEditorConfigSection from './SqsTriggerEditorConfigSection.svelte' import Section from '$lib/components/Section.svelte' import ScriptPicker from '$lib/components/ScriptPicker.svelte' import Required from '$lib/components/Required.svelte' - import type { Snippet } from 'svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import { saveSqsTriggerFromCfg } from './utils' - import { handleConfigChange } from '../utils' - interface Props { - useDrawer?: boolean - description?: Snippet | undefined - hideTarget?: boolean - hideTooltips?: boolean - allowDraft?: boolean - hasDraft?: boolean - isDraftOnly?: boolean - isEditor?: boolean - customLabel?: Snippet - isDeployed?: boolean - cloudDisabled?: boolean - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void - onCaptureConfigChange?: (cfg: Record, isValid: boolean) => void - onUpdate?: (path?: string) => void - onDelete?: () => void - onReset?: () => void - } + let drawer: Drawer + let is_flow: boolean = false + let initialPath = '' + let edit = true + let itemKind: 'flow' | 'script' = 'script' + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let enabled = false + let dirtyPath = false + let can_write = true + let drawerLoading = true + let aws_resource_path: string = '' + let queue_url = '' + let message_attributes: string[] = [] + let aws_auth_resource_type: AwsAuthResourceType = 'credentials' + let isValid = false + const dispatch = createEventDispatcher() - let { - useDrawer = true, - description = undefined, - hideTarget = false, - hideTooltips = false, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - isEditor = false, - customLabel = undefined, - isDeployed = false, - cloudDisabled = false, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined - }: Props = $props() + $: is_flow = itemKind === 'flow' - let drawer: Drawer | undefined = $state(undefined) - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let itemKind: 'flow' | 'script' = $state('script') - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path: string = $state('') - let pathError = $state('') - let enabled = $state(false) - let dirtyPath = $state(false) - let can_write = $state(true) - let drawerLoading = $state(true) - let showLoading = $state(false) - let aws_resource_path: string = $state('') - let queue_url = $state('') - let message_attributes: string[] = $state([]) - let aws_auth_resource_type: AwsAuthResourceType = $state('credentials') - let isValid = $state(false) - let initialConfig: Record | undefined = undefined - let deploymentLoading = $state(false) - - const sqsConfig = $derived.by(getSaveCfg) - const captureConfig = $derived.by(getCaptureConfig) - const saveDisabled = $derived( - pathError != '' || emptyString(script_path) || !isValid || !can_write - ) - $effect(() => { - is_flow = itemKind === 'flow' - }) - - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultConfig?: Record - ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() @@ -102,13 +46,11 @@ itemKind = isFlow ? 'flow' : 'script' edit = true dirtyPath = false - await loadTrigger(defaultConfig) + await loadTrigger() } catch (err) { sendUserToast(`Could not load sqs trigger: ${err.body}`, true) } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } @@ -117,9 +59,6 @@ fixedScriptPath_?: string, defaultValues?: Record ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) drawerLoading = true try { drawer?.openDrawer() @@ -130,207 +69,143 @@ script_path = fixedScriptPath aws_resource_path = defaultValues?.aws_resource_path ?? '' queue_url = defaultValues?.queue_url ?? '' - path = defaultValues?.path ?? '' + path = '' message_attributes = defaultValues?.message_attributes ?? [] aws_auth_resource_type = defaultValues?.aws_auth_resource_type ?? 'credentials' initialPath = '' edit = false dirtyPath = false - enabled = defaultValues?.enabled ?? false } finally { - initialConfig = structuredClone($state.snapshot(getSaveCfg())) - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } - async function loadTriggerConfig(cfg?: Record): Promise { + async function loadTrigger(): Promise { try { - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - aws_resource_path = cfg?.aws_resource_path - queue_url = cfg?.queue_url - is_flow = cfg?.is_flow - message_attributes = cfg?.message_attributes ?? [] - path = cfg?.path - enabled = cfg?.enabled - aws_auth_resource_type = cfg?.aws_auth_resource_type - can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore) - } catch (error) { - sendUserToast(`Could not load SQS trigger config: ${error.body}`, true) - } - } - - async function loadTrigger(defaultConfig?: Record): Promise { - try { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - return - } else { - const s = await SqsTriggerService.getSqsTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - loadTriggerConfig(s) - } + const s = await SqsTriggerService.getSqsTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + script_path = s.script_path + initialScriptPath = s.script_path + aws_resource_path = s.aws_resource_path + queue_url = s.queue_url + is_flow = s.is_flow + message_attributes = s.message_attributes ?? [] + path = s.path + enabled = s.enabled + aws_auth_resource_type = s.aws_auth_resource_type + can_write = canWrite(s.path, s.extra_perms, $userStore) } catch (error) { sendUserToast(`Could not load SQS trigger: ${error.body}`, true) } } - function getSaveCfg(): Record { - return { - script_path, - is_flow, - path, - aws_resource_path, - queue_url, - message_attributes, - aws_auth_resource_type, - enabled - } - } - - async function handleToggleEnabled(nEnabled: boolean) { - enabled = nEnabled - if (!isDraftOnly && !hasDraft) { - await SqsTriggerService.setSqsTriggerEnabled({ - path: initialPath, - workspace: $workspaceStore ?? '', - requestBody: { enabled: nEnabled } - }) - sendUserToast(`${nEnabled ? 'enabled' : 'disabled'} SQS trigger ${initialPath}`) - } - } - async function updateTrigger(): Promise { - deploymentLoading = true - const cfg = getSaveCfg() - const isSaved = await saveSqsTriggerFromCfg( - initialPath, - cfg, - edit, - $workspaceStore!, - usedTriggerKinds - ) - if (isSaved) { - onUpdate?.(cfg.path) - drawer?.closeDrawer() + if (edit) { + await SqsTriggerService.updateSqsTrigger({ + workspace: $workspaceStore!, + path: initialPath, + requestBody: { + path, + script_path, + aws_auth_resource_type, + enabled, + is_flow, + queue_url, + aws_resource_path, + message_attributes + } + }) + sendUserToast(`SQS trigger ${path} updated`) + } else { + await SqsTriggerService.createSqsTrigger({ + workspace: $workspaceStore!, + requestBody: { + enabled: true, + aws_resource_path, + queue_url, + aws_auth_resource_type, + path, + script_path, + is_flow, + message_attributes + } + }) + sendUserToast(`SQS trigger ${path} created`) } - deploymentLoading = false + + if (!$usedTriggerKinds.includes('sqs')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'sqs'] + } + dispatch('update') + drawer.closeDrawer() } - - function getCaptureConfig(): Record { - return { - aws_resource_path, - queue_url, - message_attributes, - aws_auth_resource_type, - path - } - } - - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid) - }) - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(sqsConfig, initialConfig, saveDisabled, edit, onConfigChange) - } - }) -{#if useDrawer} - - - - {@render actions()} - - {@render config()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} + + + + {#if !drawerLoading && can_write} + {#if edit} +
+ { + sendUserToast(`${e.detail ? 'enabled' : 'disabled'} sqs trigger ${initialPath}`) + }} + /> +
+ {/if} + {/if}
- - {@render actions()} - - {@render config()} -
-{/if} - -{#snippet actions()} - {#if !drawerLoading} - - {/if} -{/snippet} - -{#snippet config()} - {#if drawerLoading} - {#if showLoading} - - {/if} - {:else} -
- {#if description} - {@render description()} - {/if} - {#if !hideTooltips} - + {#if drawerLoading} +
+ +

Loading...

+
+ {:else} +
+ {#if edit} Changes can take up to 30 seconds to take effect. {:else} - New SQS triggers can take up to 30 seconds to start listening. + New postgres triggers can take up to 30 seconds to start listening. {/if} - {/if} -
-
-
-
+
+
+ +
- {#if !hideTarget}

Pick a script or flow to be triggered @@ -343,35 +218,30 @@ allowFlow={true} bind:itemKind bind:scriptPath={script_path} - allowRefresh={can_write} - allowEdit={!$userStore?.operator} + allowRefresh /> {#if emptyString(script_path)} - Create from template - {/if}

- {/if} - -
- {/if} -{/snippet} + +
+ {/if} + + diff --git a/frontend/src/lib/components/triggers/sqs/SqsTriggerPanel.svelte b/frontend/src/lib/components/triggers/sqs/SqsTriggerPanel.svelte index 24bd1137f0..e981d7dedf 100644 --- a/frontend/src/lib/components/triggers/sqs/SqsTriggerPanel.svelte +++ b/frontend/src/lib/components/triggers/sqs/SqsTriggerPanel.svelte @@ -1,69 +1,135 @@ + { + loadTriggers() + }} + bind:this={sqsTriggerEditor} +/> + {#if !$enterpriseLicense} SQS triggers are an enterprise only feature. +{:else if isCloudHosted()} + + SQS triggers are disabled in the multi-tenant cloud. + {:else}
- - {#snippet description()} - {#if cloudDisabled} - - SQS triggers are disabled in the multi-tenant cloud. - - {:else} - - SQS triggers allow you to execute scripts and flows in response to messages in an AWS - SQS queue. They can be configured to filter messages based on message attributes. - - {/if} - {/snippet} - + SQS triggers allow your scripts/flows to process messages from Amazon Simple Queue Service + (SQS) in real time. Each trigger listens to an SQS queue and executes a script or a flow when + new messages arrive. + + + {#if !newItem && sqsTriggers && sqsTriggers.length > 0} +
+
+
+ {#each sqsTriggers as sqsTriggers (sqsTriggers.path)} +
+
{sqsTriggers.path}
+
+ {sqsTriggers.queue_url} +
+
+ +
+
+ {/each} +
+
+
+ {/if} + + { + sqsTriggerEditor?.openNew(isFlow, path, e.detail.config) + }} + on:addPreprocessor + on:updateSchema + on:testWithArgs + cloudDisabled={false} + triggerType="sqs" + {isFlow} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + bind:showCapture={dontCloseOnLoad} + />
{/if} diff --git a/frontend/src/lib/components/triggers/sqs/utils.ts b/frontend/src/lib/components/triggers/sqs/utils.ts deleted file mode 100644 index 7ef1afa60b..0000000000 --- a/frontend/src/lib/components/triggers/sqs/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { SqsTriggerService } from '$lib/gen' -import { sendUserToast } from '$lib/toast' -import { get, type Writable } from 'svelte/store' - -export async function saveSqsTriggerFromCfg( - initialPath: string, - cfg: Record, - edit: boolean, - workspace: string, - usedTriggerKinds: Writable -): Promise { - const requestBody = { - path: cfg.path, - script_path: cfg.script_path, - is_flow: cfg.is_flow, - aws_resource_path: cfg.aws_resource_path, - queue_url: cfg.queue_url, - message_attributes: cfg.message_attributes, - aws_auth_resource_type: cfg.aws_auth_resource_type, - enabled: cfg.enabled - } - try { - if (edit) { - await SqsTriggerService.updateSqsTrigger({ - workspace, - path: initialPath, - requestBody - }) - sendUserToast(`SQS trigger ${cfg.path} updated`) - } else { - await SqsTriggerService.createSqsTrigger({ - workspace, - requestBody: { ...requestBody, enabled: true } - }) - sendUserToast(`SQS trigger ${cfg.path} created`) - } - - if (!get(usedTriggerKinds).includes('sqs')) { - usedTriggerKinds.update((t) => [...t, 'sqs']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/components/triggers/testingBadge.svelte b/frontend/src/lib/components/triggers/testingBadge.svelte deleted file mode 100644 index eb8d288fbf..0000000000 --- a/frontend/src/lib/components/triggers/testingBadge.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Config used for creating a testing endpoint - diff --git a/frontend/src/lib/components/triggers/triggers.svelte.ts b/frontend/src/lib/components/triggers/triggers.svelte.ts deleted file mode 100644 index 5f40fb6126..0000000000 --- a/frontend/src/lib/components/triggers/triggers.svelte.ts +++ /dev/null @@ -1,456 +0,0 @@ -import { - KafkaTriggerService, - MqttTriggerService, - NatsTriggerService, - PostgresTriggerService, - ScheduleService, - SqsTriggerService, - WebsocketTriggerService, - type GcpTrigger, - type KafkaTrigger, - type PostgresTrigger, - type Schedule, - type TriggersCount, - type HttpTrigger, - HttpTriggerService, - GcpTriggerService -} from '$lib/gen' -import { getLightConfig, sortTriggers, updateTriggersCount, type Trigger } from './utils' -import type { Writable } from 'svelte/store' -import type { TriggerType } from './utils' -import type { UserExt } from '$lib/stores' -import type { ScheduleTrigger } from '../triggers' -import { canWrite, formatCron } from '$lib/utils' - -export class Triggers { - #triggers = $state([]) - #selectedTriggerIndex = $state(undefined) - #selectedTrigger = $derived( - this.#selectedTriggerIndex !== undefined - ? this.#triggers[this.#selectedTriggerIndex] - : undefined - ) - #updateDraftCallback: (() => void) | undefined = undefined - - constructor( - triggers: Trigger[] = [], - selectedIndex?: number, - updateDraftCallback?: (() => void) | undefined - ) { - this.#triggers = triggers - this.#selectedTriggerIndex = selectedIndex - this.#updateDraftCallback = updateDraftCallback - } - - get selectedTrigger(): Trigger | undefined { - return this.#selectedTrigger - } - - get selectedTriggerIndex(): number | undefined { - return this.#selectedTriggerIndex - } - - set selectedTriggerIndex(index: number | undefined) { - if (index === undefined || index < 0 || index >= this.#triggers.length) { - this.#selectedTriggerIndex = undefined - } else { - this.#selectedTriggerIndex = index - } - this.#updateDraftCallback?.() - } - - get triggers(): Trigger[] { - return this.#triggers - } - - setTriggers(triggers: Trigger[]) { - this.#triggers = triggers - this.#updateDraftCallback?.() - } - - setDraftConfig(triggerIndex: number, draftConfig: Record | undefined) { - if (triggerIndex === undefined || triggerIndex < 0 || triggerIndex >= this.#triggers.length) { - return - } - this.#triggers[triggerIndex].draftConfig = draftConfig - this.#updateDraftCallback?.() - } - - getDraftTriggersSnapshot(): Trigger[] | undefined { - const draftTriggers = this.#triggers.filter((t) => t.draftConfig) - return draftTriggers.length > 0 ? $state.snapshot(draftTriggers) : undefined - } - - getSelectedTriggerSnapshot(): number | undefined { - return $state.snapshot(this.#selectedTriggerIndex) - } - - addDraftTrigger( - triggersCountStore: Writable, - type: TriggerType, - path?: string, - draftCfg?: Record - ): number { - const primaryScheduleExists = this.#triggers.some((t) => t.type === 'schedule' && t.isPrimary) - - // Create the new draft trigger - const draftId = crypto.randomUUID() - const isPrimary = type === 'schedule' && !primaryScheduleExists - const newTrigger = { - id: draftId, - type, - path, - isPrimary, - isDraft: true, - draftConfig: draftCfg - } - - this.#triggers.push(newTrigger) - this.#updateDraftCallback?.() - - updateTriggersCount(triggersCountStore, type, 'add', newTrigger.draftConfig) - - return this.#triggers.length - 1 - } - - deleteTrigger( - triggersCountStore: Writable, - triggerIndex: number - ): void { - if (triggerIndex === undefined || triggerIndex < 0 || triggerIndex >= this.#triggers.length) { - return - } - const { type } = this.#triggers[triggerIndex] - - this.#triggers = this.#triggers.filter((_, index) => index !== triggerIndex) - - updateTriggersCount(triggersCountStore, type, 'remove') - this.#updateDraftCallback?.() - } - - updateTriggers( - remoteTriggers: any[], - type: TriggerType, - user: UserExt | undefined = undefined - ): number { - const currentTriggers = this.#triggers - // Identify triggers with draftConfig to preserve - const configuredTriggers = currentTriggers.filter( - (t) => t.type === type && !t.isDraft && t.draftConfig - ) - - const configMap = new Map }>() - - configuredTriggers.forEach((t) => { - configMap.set(t.path ?? '', { draftConfig: t.draftConfig! }) - }) - - const backendTriggers = remoteTriggers.map((trigger) => { - const { draftConfig } = configMap.get(trigger.path) ?? {} - return { - type: type as TriggerType, - path: trigger.path, - isPrimary: type === 'schedule' && trigger.path === trigger.script_path, - isDraft: false, - canWrite: canWrite(trigger.path, trigger.extra_perms, user), - draftConfig: draftConfig, - lightConfig: getLightConfig(type, trigger) - } - }) - - const filteredTriggers = currentTriggers.filter((t) => t.type !== type || t.isDraft) - const newTriggers = sortTriggers([...filteredTriggers, ...backendTriggers]) - this.#triggers = newTriggers - - this.#updateDraftCallback?.() - return newTriggers.filter((t) => t.type === type).length - } - - async fetchSchedules( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - primarySchedule?: ScheduleTrigger | undefined | false, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - //First update the store with legacy primary schedule - if (primarySchedule && !this.#triggers.some((s) => s.isPrimary)) { - const primary = { - type: 'schedule' as TriggerType, - path, - isPrimary: true, - isDraft: false, - draftConfig: { - schedule: primarySchedule.cron ? formatCron(primarySchedule.cron) : undefined, - args: primarySchedule.args, - timezone: primarySchedule.timezone, - summary: primarySchedule.summary, - description: primarySchedule.description, - enabled: primarySchedule.enabled - } - } - this.#triggers = [...this.#triggers, primary] - } - - const allDeployedSchedules: Schedule[] = await ScheduleService.listSchedules({ - workspace: workspaceId, - path, - isFlow - }) - - const scheduleCount = this.updateTriggers(allDeployedSchedules, 'schedule', user) - const updatedPrimarySchedule = this.#triggers.find((s) => s.isPrimary) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - schedule_count: scheduleCount, - primary_schedule: { - schedule: - updatedPrimarySchedule?.draftConfig?.schedule ?? - updatedPrimarySchedule?.lightConfig?.schedule - } - } - }) - - return - } catch (error) { - console.error('Failed to fetch schedules:', error) - return - } - } - - async fetchWebsocketTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const wsTriggers = await WebsocketTriggerService.listWebsocketTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const wsCount = this.updateTriggers(wsTriggers, 'websocket', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - websocket_count: wsCount - } - }) - } catch (error) { - console.error('Failed to fetch Websocket triggers:', error) - } - } - - async fetchPostgresTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const pgTriggers: PostgresTrigger[] = await PostgresTriggerService.listPostgresTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const pgCount = this.updateTriggers(pgTriggers, 'postgres', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - postgres_count: pgCount - } - }) - } catch (error) { - console.error('Failed to fetch Postgres triggers:', error) - } - } - - async fetchKafkaTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const kafkaTriggers: KafkaTrigger[] = await KafkaTriggerService.listKafkaTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const kafkaCount = this.updateTriggers(kafkaTriggers, 'kafka', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - kafka_count: kafkaCount - } - }) - } catch (error) { - console.error('Failed to fetch Kafka triggers:', error) - } - } - - async fetchNatsTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const natsTriggers = await NatsTriggerService.listNatsTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const natsCount = this.updateTriggers(natsTriggers, 'nats', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - nats_count: natsCount - } - }) - } catch (error) { - console.error('Failed to fetch NATS triggers:', error) - } - } - - async fetchMqttTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const mqttTriggers = await MqttTriggerService.listMqttTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const mqttCount = this.updateTriggers(mqttTriggers, 'mqtt', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - mqtt_count: mqttCount - } - }) - } catch (error) { - console.error('Failed to fetch MQTT triggers:', error) - } - } - - async fetchSqsTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const sqsTriggers = await SqsTriggerService.listSqsTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const sqsCount = this.updateTriggers(sqsTriggers, 'sqs', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - sqs_count: sqsCount - } - }) - } catch (error) { - console.error('Failed to fetch SQS triggers:', error) - } - } - - async fetchGcpTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const gcpTriggers: GcpTrigger[] = await GcpTriggerService.listGcpTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const gcpCount = this.updateTriggers(gcpTriggers, 'gcp', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - gcp_count: gcpCount - } - }) - } catch (error) { - console.error('Failed to fetch GCP Pub/Sub triggers:', error) - } - } - - async fetchHttpTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - try { - const httpTriggers: HttpTrigger[] = await HttpTriggerService.listHttpTriggers({ - workspace: workspaceId, - path, - isFlow - }) - const httpCount = this.updateTriggers(httpTriggers, 'http', user) - triggersCountStore.update((triggersCount) => { - return { - ...(triggersCount ?? {}), - http_routes_count: httpCount - } - }) - } catch (error) { - console.error('Failed to fetch HTTP triggers:', error) - } - } - - async fetchTriggers( - triggersCountStore: Writable, - workspaceId: string | undefined, - path: string, - isFlow: boolean, - primarySchedule: ScheduleTrigger | undefined | false = undefined, - user: UserExt | undefined = undefined - ): Promise { - if (!workspaceId) return - - // Fetch each type of trigger - await Promise.all([ - this.fetchSchedules(triggersCountStore, workspaceId, path, isFlow, primarySchedule, user), - this.fetchHttpTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchWebsocketTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchPostgresTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchKafkaTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchNatsTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchMqttTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchSqsTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchGcpTriggers(triggersCountStore, workspaceId, path, isFlow, user) - ]) - } -} diff --git a/frontend/src/lib/components/triggers/utils.ts b/frontend/src/lib/components/triggers/utils.ts deleted file mode 100644 index 7daf829366..0000000000 --- a/frontend/src/lib/components/triggers/utils.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { Webhook, Mail, Calendar, Route, Unplug, Database, Terminal } from 'lucide-svelte' -import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte' -import NatsIcon from '$lib/components/icons/NatsIcon.svelte' -import MqttIcon from '$lib/components/icons/MqttIcon.svelte' -import AwsIcon from '$lib/components/icons/AwsIcon.svelte' -import GoogleCloudIcon from '$lib/components/icons/GoogleCloudIcon.svelte' -import type { CaptureTriggerKind, Flow, NewScript, TriggersCount } from '$lib/gen/types.gen' -import type { Writable } from 'svelte/store' -import SchedulePollIcon from '../icons/SchedulePollIcon.svelte' -import { type TriggerKind } from '$lib/components/triggers' -import { saveScheduleFromCfg } from '$lib/components/flows/scheduleUtils' -import { saveHttpRouteFromCfg } from './http/utils' -import { saveWebsocketTriggerFromCfg } from './websocket/utils' -import { savePostgresTriggerFromCfg } from './postgres/utils' -import { saveKafkaTriggerFromCfg } from './kafka/utils' -import { saveSqsTriggerFromCfg } from './sqs/utils' -import { saveNatsTriggerFromCfg } from './nats/utils' -import { saveMqttTriggerFromCfg } from './mqtt/utils' -import { saveGcpTriggerFromCfg } from './gcp/utils' -import type { Triggers } from './triggers.svelte' -import { emptyString } from '$lib/utils' - -export const CLOUD_DISABLED_TRIGGER_TYPES = [ - 'nats', - 'kafka', - 'sqs', - 'mqtt', - 'gcp', - 'websocket', - 'postgres' -] - -export type TriggerType = - | 'webhook' - | 'email' - | 'schedule' - | 'http' - | 'websocket' - | 'postgres' - | 'kafka' - | 'nats' - | 'mqtt' - | 'sqs' - | 'gcp' - | 'poll' - | 'cli' - -export type Trigger = { - type: TriggerType - path?: string - isDraft?: boolean - isPrimary?: boolean - canWrite?: boolean - id?: string - draftConfig?: Record - captureConfig?: Record - extra?: Record - lightConfig?: Record -} - -// Map of trigger kinds to icons -export const triggerIconMap = { - webhook: Webhook, - email: Mail, - schedule: Calendar, - http: Route, - websocket: Unplug, - postgres: Database, - kafka: KafkaIcon, - nats: NatsIcon, - mqtt: MqttIcon, - sqs: AwsIcon, - gcp: GoogleCloudIcon, - primary_schedule: Calendar, - poll: SchedulePollIcon, - cli: Terminal -} - -/** - * Converts a TriggerType to a CaptureTriggerKind when a mapping exists - * @param triggerType The trigger type to convert - * @returns The corresponding CaptureTriggerKind or undefined if no mapping exists - */ -export function triggerTypeToCaptureKind(triggerType: TriggerType): CaptureTriggerKind | undefined { - // Define types that can be mapped to CaptureTriggerKind - const capturableTriggerTypes: TriggerType[] = [ - 'webhook', - 'email', - 'http', - 'websocket', - 'postgres', - 'kafka', - 'nats', - 'mqtt', - 'sqs', - 'gcp', - 'cli' - ] - - if (capturableTriggerTypes.includes(triggerType)) { - return triggerType as CaptureTriggerKind - } - - return undefined -} - -export function updateTriggersCount( - triggersCountStore: Writable, - type: TriggerType, - action: 'add' | 'remove', - primaryCfg?: Record, - isPrimary?: boolean -) { - // Map trigger types to their corresponding count property names - const countPropertyMap: Record = { - webhook: undefined, - email: undefined, - schedule: 'schedule_count', - http: 'http_routes_count', - websocket: 'websocket_count', - postgres: 'postgres_count', - kafka: 'kafka_count', - nats: 'nats_count', - mqtt: 'mqtt_count', - sqs: 'sqs_count', - gcp: 'gcp_count', - poll: undefined, - cli: undefined - } - - const countProperty = countPropertyMap[type] - - triggersCountStore.update((triggersCount) => { - // Handle special case for schedule - if (type === 'schedule') { - if (action === 'add' && primaryCfg) { - return { - ...(triggersCount ?? {}), - schedule_count: (triggersCount?.schedule_count ?? 0) + 1, - primary_schedule: primaryCfg?.schedule - } - } else if (action === 'remove') { - return { - ...(triggersCount ?? {}), - schedule_count: (triggersCount?.schedule_count ?? 1) - 1, - primary_schedule: isPrimary ? undefined : triggersCount?.primary_schedule - } - } - } - - // Handle standard count updates - if (countProperty && action === 'add') { - return { - ...(triggersCount ?? {}), - [countProperty]: (triggersCount?.[countProperty] ?? 0) + 1 - } - } else if (countProperty && action === 'remove') { - return { - ...(triggersCount ?? {}), - [countProperty]: (triggersCount?.[countProperty] ?? 1) - 1 - } - } - - return triggersCount - }) -} - -// TODO: Remove this once we've migrated all the trigger kinds to the new TriggerType enum -export function triggerKindToTriggerType(kind: TriggerKind): TriggerType | undefined { - switch (kind) { - case 'webhooks': - return 'webhook' - case 'emails': - return 'email' - case 'schedules': - return 'schedule' - case 'routes': - return 'http' - case 'websockets': - return 'websocket' - case 'postgres': - return 'postgres' - case 'kafka': - return 'kafka' - case 'nats': - return 'nats' - case 'mqtt': - return 'mqtt' - case 'sqs': - return 'sqs' - case 'gcp': - return 'gcp' - case 'scheduledPoll': - return 'poll' - default: - throw new Error(`Unknown TriggerKind: ${kind}`) - } -} - -export async function deployTriggers( - triggersToDeploy: Trigger[], - workspaceId: string | undefined, - isAdmin: boolean, - usedTriggerKinds: Writable, - initialPath?: string, - isNew?: boolean -) { - if (!workspaceId) return - - if (isNew && initialPath) { - triggersToDeploy.forEach((trigger) => { - trigger.draftConfig = { - ...trigger.draftConfig, - script_path: initialPath - } - }) - } - - // Map of trigger types to their save functions - const triggerSaveFunctions: Record = { - webhook: undefined, - email: undefined, - schedule: (trigger: Trigger) => { - if (trigger.isPrimary && initialPath) { - trigger.draftConfig = { - ...trigger.draftConfig, - path: initialPath, - script_path: initialPath - } - } - return saveScheduleFromCfg(trigger.draftConfig ?? {}, !trigger.isDraft, workspaceId) - }, - http: (trigger: Trigger) => - saveHttpRouteFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - isAdmin, - usedTriggerKinds - ), - websocket: (trigger: Trigger) => - saveWebsocketTriggerFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - usedTriggerKinds - ), - postgres: (trigger: Trigger) => - savePostgresTriggerFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - usedTriggerKinds - ), - kafka: (trigger: Trigger) => - saveKafkaTriggerFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - usedTriggerKinds - ), - nats: (trigger: Trigger) => - saveNatsTriggerFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - usedTriggerKinds - ), - mqtt: (trigger: Trigger) => - saveMqttTriggerFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - usedTriggerKinds - ), - sqs: (trigger: Trigger) => - saveSqsTriggerFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - usedTriggerKinds - ), - gcp: (trigger: Trigger) => - saveGcpTriggerFromCfg( - trigger.path ?? trigger.draftConfig?.path ?? '', - trigger.draftConfig ?? {}, - !trigger.isDraft, - workspaceId, - usedTriggerKinds - ), - poll: undefined, - cli: undefined - } - - await Promise.all( - triggersToDeploy.map(async (trigger) => { - const saveFunction = triggerSaveFunctions[trigger.type] - if (saveFunction) { - await saveFunction(trigger) - } else { - console.warn(`No save function defined for trigger type: ${trigger.type}`) - } - }) - ) -} - -export function handleSelectTriggerFromKind( - triggersState: Triggers, - triggersCountStore: Writable, - initialPath: string | undefined, - triggerKind: TriggerKind -) { - const triggerType = triggerKindToTriggerType(triggerKind) - - if (!triggerType) { - return - } - - const existingTriggerIndex = triggersState.triggers.findIndex( - (trigger) => trigger.type === triggerType - ) - - if (existingTriggerIndex !== -1) { - triggersState.selectedTriggerIndex = existingTriggerIndex - } else { - const newTrigger = triggersState.addDraftTrigger( - triggersCountStore, - triggerType, - triggerType === 'schedule' ? initialPath : undefined - ) - triggersState.selectedTriggerIndex = newTrigger - } -} - -export function handleConfigChange( - nCfg: Record, - initialConfig: Record | undefined, - saveDisabled: boolean, - edit: boolean, - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void -) { - let updated = false - if (!edit || !initialConfig) { - updated = true - } else { - // We ignore changes to enabled - let newCfg = { ...nCfg } - if ('enabled' in newCfg) { - delete newCfg.enabled - } - let initialCfg = { ...initialConfig } - if ('enabled' in initialCfg) { - delete initialCfg.enabled - } - if (JSON.stringify(newCfg) !== JSON.stringify(initialCfg)) { - updated = true - } - } - - onConfigChange?.(nCfg, saveDisabled, updated) -} - -export function getLightConfig( - triggerType: TriggerType, - trigger: Record -): Record | undefined { - if (triggerType === 'schedule') { - return { schedule: trigger.schedule, enable: trigger.enable, summary: trigger.summary } - } else if (triggerType === 'http') { - return { route_path: trigger.route_path, http_method: trigger.http_method } - } else if (triggerType === 'websocket') { - return { url: trigger.url } - } else if (triggerType === 'postgres') { - return { postgres_resource_path: trigger.postgres_resource_path } - } else if (triggerType === 'kafka') { - return { kafka_resource_path: trigger.kafka_resource_path, topics: trigger.topics } - } else if (triggerType === 'nats') { - return { nats_resource_path: trigger.nats_resource_path, subjects: trigger.subjects } - } else if (triggerType === 'mqtt') { - return { - mqtt_resource_path: trigger.mqtt_resource_path, - subscribe_topics: trigger.subscribe_topics - } - } else if (triggerType === 'sqs') { - return { queue_url: trigger.queue_url } - } else if (triggerType === 'gcp') { - return { gcp_resource_path: trigger.gcp_resource_path, topic: trigger.topic } - } else { - return undefined - } -} - -export function getTriggerLabel(trigger: Trigger): string { - const { type, isDraft, draftConfig, lightConfig, path } = trigger - const config = draftConfig ?? lightConfig - - if (type === 'webhook') { - return 'Webhook' - } else if (type === 'email') { - return 'Email' - } else if (type === 'cli') { - return 'CLI' - } else if (type === 'http' && !emptyString(config?.route_path)) { - return `${(draftConfig?.http_method ?? lightConfig?.http_method ?? 'post').toUpperCase()} ${draftConfig?.route_path ?? lightConfig?.route_path}` - } else if (type === 'schedule' && config?.summary) { - return `${config?.summary}` - } else if (type === 'kafka' && config?.topics && config?.kafka_resource_path) { - return `${config?.kafka_resource_path} - ${config?.topics.join(', ')}` - } else if (type === 'nats' && config?.subjects && config?.nats_resource_path) { - return `${config?.nats_resource_path} - ${config?.subjects.join(', ')}` - } else if (type === 'mqtt' && config?.subscribe_topics && config?.mqtt_resource_path) { - const topics = config?.subscribe_topics.map((topic: any) => topic.topic).join(', ') - return `${config?.mqtt_resource_path} - ${topics}` - } else if (type === 'sqs' && config?.queue_url) { - return `${config?.queue_url}` - } else if (type === 'gcp' && config?.gcp_resource_path && config?.topic) { - return `${config?.gcp_resource_path} - ${config?.topic}` - } else if (type === 'websocket' && config?.url) { - return `${config?.url}` - } else if (isDraft && draftConfig?.path) { - return `${draftConfig?.path}` - } else if (isDraft) { - return `New ${type.replace(/s$/, '')} trigger` - } else { - return path ?? '' - } -} - -export function sortTriggers(triggers: Trigger[]): Trigger[] { - const triggerTypeOrder = [ - 'webhook', - 'cli', - 'email', - 'poll', - 'schedule', - 'http', - 'websocket', - 'postgres', - 'kafka', - 'nats', - 'mqtt', - 'sqs', - 'gcp' - ] - - return triggers.sort((a, b) => { - // Draft triggers always come last - if (a.isDraft && !b.isDraft) return 1 - if (!a.isDraft && b.isDraft) return -1 - - // If both are drafts or both are not drafts, sort by type order - if (a.isDraft === b.isDraft) { - const aIndex = triggerTypeOrder.indexOf(a.type) - const bIndex = triggerTypeOrder.indexOf(b.type) - - // If both types are in the order array, sort by their position - if (aIndex >= 0 && bIndex >= 0) { - return aIndex - bIndex - } - - // If only one type is in the order array, it comes first - if (aIndex >= 0) return -1 - if (bIndex >= 0) return 1 - - // If neither type is in the order array, maintain original order - return 0 - } - - return 0 - }) -} - -export type FlowWithDraftAndDraftTriggers = Flow & { - draft?: Flow & { - draft_triggers?: Trigger[] - } -} - -export type NewScriptWithDraftAndDraftTriggers = NewScript & { - draft?: NewScript & { draft_triggers?: Trigger[] } - hash: string -} - -// Get rid of deployed triggers from the saved flow in the case there is a match with a deployed trigger -export function filterDraftTriggers( - savedValue: FlowWithDraftAndDraftTriggers | NewScriptWithDraftAndDraftTriggers, - triggersState: Triggers -): FlowWithDraftAndDraftTriggers | NewScriptWithDraftAndDraftTriggers { - const deployedTriggers = triggersState.triggers.filter((t) => !t.draftConfig && !t.isDraft) - let newSavedValue = savedValue - - const filterMatchingTriggers = (savedTriggers: Trigger[], deployedTriggers: Trigger[]) => { - return savedTriggers.filter( - (savedTrigger) => - !deployedTriggers.some( - (deployedTrigger) => - deployedTrigger.path === savedTrigger.draftConfig?.path && - deployedTrigger.type === savedTrigger.type - ) - ) - } - - const savedDraftTriggersFiltered = filterMatchingTriggers( - newSavedValue?.draft?.draft_triggers ?? [], - deployedTriggers - ) - if (newSavedValue?.draft?.draft_triggers) { - newSavedValue = { - ...newSavedValue, - draft: { - ...newSavedValue.draft, - draft_triggers: - savedDraftTriggersFiltered.length > 0 ? savedDraftTriggersFiltered : undefined - } - } as typeof newSavedValue - } - triggersState.setTriggers([ - ...triggersState.triggers.filter((t) => !t.draftConfig), - ...savedDraftTriggersFiltered - ]) - return newSavedValue -} diff --git a/frontend/src/lib/components/triggers/webhook/WebhooksCapture.svelte b/frontend/src/lib/components/triggers/webhook/WebhooksCapture.svelte deleted file mode 100644 index b6392bf41b..0000000000 --- a/frontend/src/lib/components/triggers/webhook/WebhooksCapture.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - -{#if captureInfo} - - {#snippet description()} - {#if captureInfo.active} -

- Send a POST request to the URL below to simulate a webhook event. -

- {:else} -

- Start capturing to listen to webhook events on this test URL. -

- {/if} - {/snippet} - - - -
-{/if} diff --git a/frontend/src/lib/components/triggers/webhook/WebhooksConfigSection.svelte b/frontend/src/lib/components/triggers/webhook/WebhooksConfigSection.svelte index 1ee21df607..bd8bb7d044 100644 --- a/frontend/src/lib/components/triggers/webhook/WebhooksConfigSection.svelte +++ b/frontend/src/lib/components/triggers/webhook/WebhooksConfigSection.svelte @@ -21,6 +21,9 @@ import { workspaceStore, userStore } from '$lib/stores' import UserSettings from '../../UserSettings.svelte' import { generateRandomString } from '$lib/utils' + import CopyableCodeBlock from '../../details/CopyableCodeBlock.svelte' + import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte' + import CaptureTable from '../CaptureTable.svelte' export let isFlow: boolean = false export let path: string = '' @@ -29,6 +32,9 @@ export let runnableArgs: any export let triggerTokens: TriggerTokens | undefined = undefined export let scopes: string[] = [] + export let showCapture: boolean = false + export let captureTable: CaptureTable | undefined = undefined + export let captureInfo: CaptureInfo | undefined = undefined let webhooks: { async: { @@ -192,6 +198,17 @@ function waitForJobCompletion(UUID) { return `${mainFunction}\n\n${triggerJobFunction}\n\n${waitForJobCompletionFunction}` } + let captureUrl = `${location.origin}/api/w/${$workspaceStore}/capture_u/webhook/${ + isFlow ? 'flow' : 'script' + }/${path}` + + function captureCurlCode() { + return `curl \\ +-X POST ${captureUrl} \\ +-H 'Content-Type: application/json' \\ +-d '${JSON.stringify(cleanedRunnableArgs ?? {}, null, 2)}'` + } + function curlCode() { return `TOKEN='${token}' ${requestType !== 'get_path' ? `BODY='${JSON.stringify(cleanedRunnableArgs ?? {})}'` : ''} @@ -246,162 +263,195 @@ done` {scopes} /> -
- {#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON} - + + {/if} -
-
-
Request type
- - - - -
-
-
Call method
- - - {#if !isFlow} +
+ {#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON} + + {/if} + +
+
+
Request type
+ + + +
+
+
Call method
+ + + {#if !isFlow} + + {/if} + + + +
+
+
Token configuration
+ + + + +
+
+ + +
+ + REST + {#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL} + Curl {/if} + Fetch - - -
-
-
Token configuration
- - - - -
-
- - - -
- - REST - {#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL} - Curl - {/if} - Fetch - - - {#key token} - -
- - - {#if requestType !== 'get_path'} -
-
- - {#key runnableArgs} - {#key requestType} - {#key webhookType} - {#key tokenType} - {#key token} -
{ - e.preventDefault() - copyToClipboard(fetchCode()) - }} - > - - -
- {/key}{/key}{/key}{/key} - {/key} -
- {/key} -
-
+ + {/key} + + +
+
-
diff --git a/frontend/src/lib/components/triggers/webhook/WebhooksPanel.svelte b/frontend/src/lib/components/triggers/webhook/WebhooksPanel.svelte index 5893ee2a17..485a433e97 100644 --- a/frontend/src/lib/components/triggers/webhook/WebhooksPanel.svelte +++ b/frontend/src/lib/components/triggers/webhook/WebhooksPanel.svelte @@ -2,40 +2,30 @@ import { Alert } from '$lib/components/common' import Description from '$lib/components/Description.svelte' import HighlightTheme from '$lib/components/HighlightTheme.svelte' - import WebhooksConfigSection from './WebhooksConfigSection.svelte' - import { Section } from '$lib/components/common' + import TriggersEditorSection from '../TriggersEditorSection.svelte' - interface Props { - token: string - args?: Record - scopes?: string[] - isFlow?: boolean - hash?: string | undefined - path: string - newItem?: boolean - } + export let token: string + export let args: Record = {} + export let scopes: string[] = [] + export let isFlow: boolean = false + export let hash: string | undefined = undefined + export let path: string + export let newItem: boolean = false + export let isEditor: boolean = false + export let canHavePreprocessor: boolean = false + export let hasPreprocessor: boolean = false - let { - token, - args = {}, - scopes = [], - isFlow = false, - hash = undefined, - path, - newItem = false - }: Props = $props() - - let data = $derived({ + $: data = { hash, token, scopes, args - }) + } -
+
Webhooks trigger scripts or flows via HTTP requests. Each webhook can be configured to run synchronously or asynchronously. You can secure webhooks using tokens with specific permissions. @@ -47,12 +37,22 @@ {/if} - -
+
diff --git a/frontend/src/lib/components/triggers/websocket/WebsocketCapture.svelte b/frontend/src/lib/components/triggers/websocket/WebsocketCapture.svelte deleted file mode 100644 index ba64404e12..0000000000 --- a/frontend/src/lib/components/triggers/websocket/WebsocketCapture.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - -{#if captureInfo} - - - {#if captureInfo.active} - {#if captureInfo.connectionInfo?.connected} -

- Listenning to websocket events... -

- {:else} -

- Connecting to websocket... -

- {/if} - {:else} -

- Start capturing to listen to websocket events. -

- {/if} -
-
-{/if} diff --git a/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte b/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte index a229b57215..4f773de7ed 100644 --- a/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte @@ -5,20 +5,23 @@ import ScriptPicker from '$lib/components/ScriptPicker.svelte' import Required from '$lib/components/Required.svelte' import { Loader2 } from 'lucide-svelte' + import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte' + import CaptureTable from '../CaptureTable.svelte' import { sendUserToast } from '$lib/utils' import type { Schema } from '$lib/common' import { FlowService, ScriptService, type Flow, type Script } from '$lib/gen' import { workspaceStore } from '$lib/stores' import TestTriggerConnection from '../TestTriggerConnection.svelte' - import TestingBadge from '$lib/components/triggers/testingBadge.svelte' export let url: string | undefined export let url_runnable_args: Record | undefined export let dirtyUrl: boolean = false export let can_write: boolean = false export let headless: boolean = false + export let showCapture: boolean = false + export let captureTable: CaptureTable | undefined = undefined + export let captureInfo: CaptureInfo | undefined = undefined export let isValid: boolean = false - export let showTestingBadge: boolean = false let areRunnableArgsValid: boolean = true @@ -32,11 +35,11 @@ ? await FlowService.getFlowByPath({ workspace: $workspaceStore!, path: url.split(':')[1] - }) + }) : await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: url.split(':')[1] - }) + }) urlRunnableSchema = scriptOrFlow.schema as Schema } catch (err) { sendUserToast( @@ -78,12 +81,20 @@
+ {#if showCapture && captureInfo} + + {/if}
- - {#if showTestingBadge} - - {/if} -
- - + +
{#if url?.startsWith('$')} @@ -117,7 +126,6 @@ const { path, itemKind } = ev.detail url = `$${itemKind}:${path ?? ''}` }} - disabled={!can_write} />
{dirtyUrl ? urlError : ''} @@ -138,7 +146,6 @@ bind:isValid={areRunnableArgsValid} shouldHideNoInputs class="text-xs" - disabled={!can_write} /> {/key} {/await} diff --git a/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditor.svelte b/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditor.svelte index d1112ccd20..65c0f7f1b3 100644 --- a/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditor.svelte +++ b/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditor.svelte @@ -1,23 +1,8 @@ {#if open} - + {/if} diff --git a/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditorInner.svelte index fda17690e8..fea948ee30 100644 --- a/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/websocket/WebsocketTriggerEditorInner.svelte @@ -16,118 +16,45 @@ } from '$lib/gen' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, emptySchema, emptyString, sendUserToast } from '$lib/utils' + import { createEventDispatcher } from 'svelte' import Section from '$lib/components/Section.svelte' - import { Loader2, X, Plus } from 'lucide-svelte' + import { Loader2, Save, X, Plus } from 'lucide-svelte' import Label from '$lib/components/Label.svelte' import { fade } from 'svelte/transition' import type { Schema } from '$lib/common' import JsonEditor from '$lib/components/JsonEditor.svelte' import Toggle from '$lib/components/Toggle.svelte' import WebsocketEditorConfigSection from './WebsocketEditorConfigSection.svelte' - import type { Snippet } from 'svelte' - import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte' - import { saveWebsocketTriggerFromCfg } from './utils' - import { handleConfigChange } from '../utils' - - interface Props { - useDrawer?: boolean - description?: Snippet | undefined - hideTarget?: boolean - hideTooltips?: boolean - useEditButton?: boolean - isEditor?: boolean - allowDraft?: boolean - hasDraft?: boolean - isDraftOnly?: boolean - isDeployed?: boolean - cloudDisabled?: boolean - customLabel?: Snippet - onConfigChange?: (cfg: Record, saveDisabled: boolean, updated: boolean) => void - onCaptureConfigChange?: (cfg: Record, isValid: boolean) => void - onUpdate?: (path?: string) => void - onDelete?: () => void - onReset?: () => void - } - - let { - useDrawer = true, - description = undefined, - hideTarget = false, - hideTooltips = false, - isEditor = false, - allowDraft = false, - hasDraft = false, - isDraftOnly = false, - isDeployed = false, - customLabel = undefined, - onConfigChange = undefined, - onCaptureConfigChange = undefined, - onUpdate = undefined, - onDelete = undefined, - onReset = undefined, - cloudDisabled = false - }: Props = $props() - - let drawer: Drawer | undefined = $state() - let is_flow: boolean = $state(false) - let initialPath = $state('') - let edit = $state(true) - let itemKind: 'flow' | 'script' = $state('script') - let script_path = $state('') - let initialScriptPath = $state('') - let fixedScriptPath = $state('') - let path: string = $state('') - let pathError = $state('') - let url = $state('') - let dirtyUrl = $state(false) - let enabled = $state(false) + let drawer: Drawer + let is_flow: boolean = false + let initialPath = '' + let edit = true + let itemKind: 'flow' | 'script' = 'script' + let script_path = '' + let initialScriptPath = '' + let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let url = '' + let dirtyUrl = false + let enabled = false let filters: { key: string value: any - }[] = $state([]) - let initial_messages: WebsocketTriggerInitialMessage[] = $state([]) - let url_runnable_args: Record | undefined = $state({}) - let can_return_message = $state(false) - let dirtyPath = $state(false) - let can_write = $state(true) - let drawerLoading = $state(true) - let showLoading = $state(false) - let initialConfig: Record | undefined = undefined - let deploymentLoading = $state(false) - let isValid = $state(false) + }[] = [] + let initial_messages: WebsocketTriggerInitialMessage[] = [] + let url_runnable_args: Record | undefined = {} + let can_return_message = false + let dirtyPath = false + let can_write = true + let drawerLoading = true - const websocketCfg = $derived.by(getSaveCfg) - const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({})) - const saveDisabled = $derived.by(() => { - const invalidInitialMessages = initial_messages.some((v) => { - if ('runnable_result' in v) { - return !v.runnable_result.path - } - return false - }) - return ( - pathError !== '' || - !isValid || - invalidInitialMessages || - drawerLoading || - !can_write || - emptyString(script_path) - ) - }) + const dispatch = createEventDispatcher() - $effect(() => { - is_flow = itemKind === 'flow' - }) + $: is_flow = itemKind === 'flow' - export async function openEdit( - ePath: string, - isFlow: boolean, - defaultConfig?: Record - ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms + export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true try { drawer?.openDrawer() @@ -136,16 +63,11 @@ edit = true dirtyPath = false dirtyUrl = false - await loadTrigger(defaultConfig) + await loadTrigger() } catch (err) { sendUserToast(`Could not load websocket trigger: ${err}`, true) } finally { - if (!defaultConfig) { - initialConfig = structuredClone($state.snapshot(getSaveCfg())) - } - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } @@ -154,9 +76,6 @@ fixedScriptPath_?: string, defaultValues?: Record ) { - let loadingTimeout = setTimeout(() => { - showLoading = true - }, 100) // Do not show loading spinner for the first 100ms drawerLoading = true try { drawer?.openDrawer() @@ -168,7 +87,7 @@ initialScriptPath = '' fixedScriptPath = fixedScriptPath_ ?? '' script_path = fixedScriptPath - path = defaultValues?.path ?? '' + path = '' initialPath = '' filters = [] initial_messages = [] @@ -176,55 +95,31 @@ dirtyPath = false can_return_message = false } finally { - clearTimeout(loadingTimeout) drawerLoading = false - showLoading = false } } - function loadTriggerConfig(cfg?: Record): void { - script_path = cfg?.script_path - initialScriptPath = cfg?.script_path - is_flow = cfg?.is_flow - path = cfg?.path - url = cfg?.url - enabled = cfg?.enabled - filters = cfg?.filters - initial_messages = cfg?.initial_messages ?? [] - url_runnable_args = cfg?.url_runnable_args - can_return_message = cfg?.can_return_message - can_write = canWrite(path, cfg?.extra_perms, $userStore) + async function loadTrigger(): Promise { + const s = await WebsocketTriggerService.getWebsocketTrigger({ + workspace: $workspaceStore!, + path: initialPath + }) + script_path = s.script_path + initialScriptPath = s.script_path + + is_flow = s.is_flow + path = s.path + url = s.url + enabled = s.enabled + filters = s.filters + initial_messages = s.initial_messages ?? [] + url_runnable_args = s.url_runnable_args + can_return_message = s.can_return_message + + can_write = canWrite(s.path, s.extra_perms, $userStore) } - function getSaveCfg() { - return { - script_path, - initialScriptPath, - is_flow, - path, - url, - filters, - initial_messages, - url_runnable_args, - can_return_message, - enabled - } - } - - async function loadTrigger(defaultConfig?: Record): Promise { - if (defaultConfig) { - loadTriggerConfig(defaultConfig) - return - } else { - const s = await WebsocketTriggerService.getWebsocketTrigger({ - workspace: $workspaceStore!, - path: initialPath - }) - loadTriggerConfig(s) - } - } - - let initialMessageRunnableSchemas: Record = $state({}) + let initialMessageRunnableSchemas: Record = {} async function loadInitialMessageRunnableSchemas( initialMessageRunnables: { path: string @@ -252,152 +147,133 @@ } } } - let initialMessageRunnables = $derived( - initial_messages - .map((v) => ('runnable_result' in v ? v.runnable_result : undefined)) - .filter((v): v is { path: string; is_flow: boolean; args: ScriptArgs } => !!v) - ) - $effect(() => { - loadInitialMessageRunnableSchemas(initialMessageRunnables) + $: initialMessageRunnables = initial_messages + .map((v) => ('runnable_result' in v ? v.runnable_result : undefined)) + .filter((v): v is { path: string; is_flow: boolean; args: ScriptArgs } => !!v) + $: loadInitialMessageRunnableSchemas(initialMessageRunnables) + + $: invalidInitialMessages = initial_messages.some((v) => { + if ('runnable_result' in v) { + return !v.runnable_result.path + } + return false }) async function updateTrigger(): Promise { - deploymentLoading = true - const saveCfg = getSaveCfg() - const isSaved = await saveWebsocketTriggerFromCfg( - initialPath, - saveCfg, - edit, - $workspaceStore!, - usedTriggerKinds - ) - if (isSaved) { - onUpdate?.(saveCfg.path) - drawer?.closeDrawer() - } - deploymentLoading = false - } - - function getCaptureConfig() { - return { - url, - url_runnable_args, - path - } - } - - async function handleToggleEnabled(newEnabled: boolean) { - enabled = newEnabled - if (!isDraftOnly && !hasDraft) { - await WebsocketTriggerService.setWebsocketTriggerEnabled({ - path: initialPath, + if (edit) { + await WebsocketTriggerService.updateWebsocketTrigger({ workspace: $workspaceStore!, - requestBody: { enabled: newEnabled } + path: initialPath, + requestBody: { + path, + script_path, + is_flow, + url, + filters, + initial_messages, + url_runnable_args, + can_return_message + } }) - sendUserToast(`${newEnabled ? 'enabled' : 'disabled'} websocket trigger ${initialPath}`) + sendUserToast(`Websocket trigger ${path} updated`) + } else { + await WebsocketTriggerService.createWebsocketTrigger({ + workspace: $workspaceStore!, + requestBody: { + path, + script_path, + is_flow, + url, + enabled: true, + filters, + initial_messages, + url_runnable_args, + can_return_message + } + }) + sendUserToast(`Websocket trigger ${path} created`) } + if (!$usedTriggerKinds.includes('ws')) { + $usedTriggerKinds = [...$usedTriggerKinds, 'ws'] + } + dispatch('update') + drawer.closeDrawer() } - $effect(() => { - onCaptureConfigChange?.(captureConfig, isValid) - }) - - $effect(() => { - if (!drawerLoading) { - handleConfigChange(websocketCfg, initialConfig, saveDisabled, edit, onConfigChange) - } - }) + let isValid = false -{#if useDrawer} - - - - {@render actionsButtons()} - - {@render config()} - - -{:else} -
- - {#if customLabel} - {@render customLabel()} + + + + {#if !drawerLoading} + {#if edit} +
+ { + await WebsocketTriggerService.setWebsocketTriggerEnabled({ + path: initialPath, + workspace: $workspaceStore ?? '', + requestBody: { enabled: e.detail } + }) + sendUserToast( + `${e.detail ? 'enabled' : 'disabled'} websocket trigger ${initialPath}` + ) + }} + /> +
+ {/if} + {#if can_write} + + {/if} {/if}
- - {@render actionsButtons()} - - {@render config()} -
-{/if} - -{#snippet actionsButtons()} - {#if !drawerLoading} - - {/if} -{/snippet} - -{#snippet config()} - {#if drawerLoading} - {#if showLoading} + {#if drawerLoading} - {/if} - {:else} -
- {#if description} - {@render description()} - {/if} - {#if !hideTooltips} - - {#if edit} - Changes can take up to 30 seconds to take effect. - {:else} - New WebSocket triggers can take up to 30 seconds to start listening. - {/if} - - {/if} -
-
-
- -
+ {:else} + + {#if edit} + Changes can take up to 30 seconds to take effect. + {:else} + New WebSocket triggers can take up to 30 seconds to start listening. + {/if} + +
+
+ +
- {#if !hideTarget}

@@ -419,12 +295,9 @@ btnClasses="ml-4 mt-2" color="dark" size="xs" - disabled={!can_write} href={itemKind === 'flow' ? '/flows/add?hub=64' : '/scripts/add?hub=hub%2F19660'} - target="_blank" + target="_blank">Create from template - Create from template - {/if}

@@ -439,244 +312,230 @@ rightTooltip: 'Whether the runnable result should be sent as a message to the websocket server when not null.' }} - disabled={!can_write} />
- {/if} - + -
-

- Initial messages are sent at the beginning of the connection. They are sent in order.
- Raw messages and runnable results are supported. -

-
- {#each initial_messages as v, i} -
-
-
- -
- {#if 'raw_message' in v} -
-
- Raw JSON message (if a string, wrapping quotes will be discarded) + }} + value={'runnable_result' in v ? 'runnable_result' : 'raw_message'} + > + + + + +
+ {#if 'raw_message' in v} +
+
+ Raw JSON message (if a string, wrapping quotes will be discarded) +
+ { + const { code } = ev.detail + v = { + raw_message: code + } + }} + code={v.raw_message} + />
- { - const { code } = ev.detail - initial_messages[i] = { - raw_message: code - } - }} - code={v.raw_message} - disabled={!can_write} - /> -
- {:else if 'runnable_result' in v} -
-
Runnable
- { - const { path, itemKind } = ev.detail - initial_messages[i] = { - runnable_result: { - path: path ?? '', - args: {}, - is_flow: itemKind === 'flow' + {:else if 'runnable_result' in v} +
+
Runnable
+ { + const { path, itemKind } = ev.detail + v = { + runnable_result: { + path: path ?? '', + args: {}, + is_flow: itemKind === 'flow' + } } - } - }} - disabled={!can_write} - /> + }} + /> - {#if v.runnable_result?.path} - {@const schema = - initialMessageRunnableSchemas[ - v.runnable_result.is_flow - ? 'flow/' + v.runnable_result.path - : v.runnable_result.path - ]} - {#if schema} -

Arguments

- {#await import('$lib/components/SchemaForm.svelte')} + {#if v.runnable_result?.path} + {@const schema = + initialMessageRunnableSchemas[ + v.runnable_result.is_flow + ? 'flow/' + v.runnable_result.path + : v.runnable_result.path + ]} + {#if schema} +

Arguments

+ {#await import('$lib/components/SchemaForm.svelte')} + + {:then Module} + + {/await} + {#if schema && schema.properties && Object.keys(schema.properties).length === 0} +
This runnable takes no arguments
+ {/if} + {:else} - {:then Module} - - {/await} - {#if schema && schema.properties && Object.keys(schema.properties).length === 0} -
This runnable takes no arguments
{/if} - {:else} - {/if} - {/if} -
- {:else} - Unknown type - {/if} +
+ {:else} + Unknown type + {/if} +
+
- + Add item +
- {/each} - -
-
-
- + -
-

- Filters will limit the execution of the trigger to only messages that match all criteria.
- The JSON filter checks if the value at the key is equal or a superset of the filter value. -

-
- {#each filters as v, i} -
-
-
- +
+ + +
- - - +
- -
- {/each} + {/each} -
- +
+ +
- -
- - {/if} -{/snippet} + + + {/if} + + diff --git a/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte b/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte index 269d69bb94..12142f8343 100644 --- a/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte +++ b/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte @@ -1,61 +1,127 @@ -
- - {#snippet description()} - {#if cloudDisabled} - - WebSocket triggers are disabled in the multi-tenant cloud. - - {:else} - - WebSocket triggers allow real-time bidirectional communication between your scripts/flows - and external systems. Each trigger creates a unique WebSocket endpoint. - - {/if} - {/snippet} - -
+ { + loadTriggers() + }} + bind:this={wsTriggerEditor} +/> + +{#if isCloudHosted()} + + WebSocket triggers are disabled in the multi-tenant cloud. + +{:else} +
+ + WebSocket triggers allow real-time bidirectional communication between your scripts/flows and + external systems. Each trigger creates a unique WebSocket endpoint. + + + {#if !newItem && wsTriggers && wsTriggers.length > 0} +
+
+
+ {#each wsTriggers as wsTriggers (wsTriggers.path)} +
+
{wsTriggers.path}
+
+ {wsTriggers.url} +
+
+ +
+
+ {/each} +
+
+
+ {/if} + + { + wsTriggerEditor?.openNew(isFlow, path, e.detail.config) + }} + on:addPreprocessor + on:updateSchema + on:testWithArgs + cloudDisabled={false} + triggerType="websocket" + {isFlow} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + /> +
+{/if} diff --git a/frontend/src/lib/components/triggers/websocket/utils.ts b/frontend/src/lib/components/triggers/websocket/utils.ts deleted file mode 100644 index c112cb0f1a..0000000000 --- a/frontend/src/lib/components/triggers/websocket/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { WebsocketTriggerService } from '$lib/gen' -import { sendUserToast } from '$lib/toast' -import type { Writable } from 'svelte/store' -import { get } from 'svelte/store' - -export async function saveWebsocketTriggerFromCfg( - initialPath: string, - triggerCfg: Record, - edit: boolean, - workspace: string, - usedTriggerKinds: Writable -): Promise { - const requestBody = { - path: triggerCfg.path, - script_path: triggerCfg.script_path, - is_flow: triggerCfg.is_flow, - url: triggerCfg.url, - filters: triggerCfg.filters, - initial_messages: triggerCfg.initial_messages, - url_runnable_args: triggerCfg.url_runnable_args, - can_return_message: triggerCfg.can_return_message - } - try { - if (edit) { - await WebsocketTriggerService.updateWebsocketTrigger({ - workspace: workspace, - path: initialPath, - requestBody: requestBody - }) - sendUserToast(`Websocket trigger ${triggerCfg.path} updated`) - } else { - await WebsocketTriggerService.createWebsocketTrigger({ - workspace: workspace, - requestBody: { ...requestBody, enabled: true } - }) - sendUserToast(`Websocket trigger ${triggerCfg.path} created`) - } - if (!get(usedTriggerKinds).includes('ws')) { - usedTriggerKinds.update((t) => [...t, 'ws']) - } - return true - } catch (error) { - sendUserToast(error.body || error.message, true) - return false - } -} diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index ceb6644c42..1f61ecc553 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -498,15 +498,28 @@ const FETCH_INIT_CODE = `export async function main( }` const BASH_INIT_CODE = `# shellcheck shell=bash -# arguments of the form X="$I" are parsed as parameters X of type string +# Arguments of the form X="$1" are parsed as parameters X of type string. msg="$1" dflt="\${2:-default value}" -# the last line of the stdout is the return value -# unless you write json to './result.json' or a string to './result.out' +# Output behavior: +# By default, the last line printed to stdout will be used as the return value. +# +# To override this behavior: +# - Write a valid JSON object to './result.json' if the output should be parsed as structured JSON. +# ⚠️ The contents must be valid JSON, or the result will be rejected. +# Example: +# echo '{ "message": "Hello world" }' > ./result.json +# +# - Write a plain string to './result.out' if the output should be returned as a raw string. +# Example: +# echo "Hello world" > ./result.out + +# Default behavior (return the string via stdout) echo "Hello $msg" ` + const DENO_INIT_CODE_TRIGGER = `import * as wmill from "npm:windmill-client@${__pkg__.version}" export async function main() { diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 6c2bde6431..607428fae3 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -11,7 +11,7 @@ import { deepEqual } from 'fast-equals' import YAML from 'yaml' import { type UserExt } from './stores' import { sendUserToast } from './toast' -import type { Job, Script } from './gen' +import type { Job, Script, ScriptLang } from './gen' import type { EnumType, SchemaProperty } from './common' import type { Schema } from './common' export { sendUserToast } @@ -22,10 +22,49 @@ import type { TriggerKind } from './components/triggers' export function isJobCancelable(j: Job): boolean { return j.type === 'QueuedJob' && !j.schedule_path && !j.canceled } + export function isJobReRunnable(j: Job): boolean { return (j.job_kind === 'script' || j.job_kind === 'flow') && j.parent_job === undefined } +export enum AgentKnownSuffix { + ENABLE_LIVE_SHELL = 'enable.live.shell' +} + +export function concatAgentSuffix( + suffix: string | undefined, + suffixToAppend: string +): string | undefined { + return suffix === undefined ? suffixToAppend : suffix.concat(`_${suffixToAppend}`) +} + +export function checkIfWorkerHasSpecificSuffix( + suffixToCheck: AgentKnownSuffix, + suffix: string +): boolean { + return suffix + .split('_') + .slice(1) + .some((part) => part === suffixToCheck) +} + +export function removeAgentSuffix( + suffix: string | undefined, + suffixToRemove: string +): string | undefined { + if (suffix) { + const regex = new RegExp(`(?:_${suffixToRemove}|${suffixToRemove})$`) + suffix = suffix.replace(regex, '') + if (suffix.trim().length === 0) { + suffix = undefined + } + } + return suffix +} + +export const WORKER_NAME_PREFIX = 'wk' +export const AGENT_WORKER_NAME_PREFIX = 'ag' + export function isJobSelectable(selectionType: RunsSelectionMode) { const f: (j: Job) => boolean = { cancel: isJobCancelable, @@ -566,6 +605,10 @@ export function formatCron(inp: string): string { } } +export function scriptLangArrayToCommaList(languages: ScriptLang[]): string { + return languages.join(',') +} + export function cronV1toV2(inp: string): string { let splitted = inp.split(' ') splitted = splitted.filter(String) @@ -1161,7 +1204,6 @@ export type Item = { disabled?: boolean type?: 'action' | 'delete' hide?: boolean | undefined - extra?: Snippet } export function isObjectTooBig(obj: any): boolean { @@ -1246,15 +1288,6 @@ export function formatDateShort(dateString: string | undefined): string { }).format(date) } -export function toJsonStr(result: any) { - try { - // console.log(result) - return JSON.stringify(result ?? null, null, 4) ?? 'null' - } catch (e) { - return 'error stringifying object: ' + e.toString() - } -} - export function getOS() { const userAgent = window.navigator.userAgent const platform = window.navigator.platform @@ -1278,7 +1311,6 @@ export function getOS() { import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' -import type { Snippet } from 'svelte' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) diff --git a/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte index 056424922b..e291917235 100644 --- a/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte @@ -13,9 +13,9 @@ import { decodeState, emptySchema } from '$lib/utils' import { tick } from 'svelte' import { writable } from 'svelte/store' + import type { ScheduleTrigger } from '$lib/components/triggers' import type { GetInitialAndModifiedValues } from '$lib/components/common/confirmationModal/unsavedTypes' import { replaceScriptPlaceholderWithItsValues } from '$lib/hub' - import type { Trigger } from '$lib/components/triggers/utils' let nodraft = $page.url.searchParams.get('nodraft') @@ -43,7 +43,6 @@ initialArgs = $initialArgsStore $initialArgsStore = undefined } - let flowBuilder: FlowBuilder | undefined = undefined export const flowStore = writable({ summary: '', @@ -57,8 +56,7 @@ }) const flowStateStore = writable({}) - let draftTriggersFromUrl: Trigger[] | undefined = undefined - let selectedTriggerIndexFromUrl: number | undefined = undefined + let savedPrimarySchedule: ScheduleTrigger | undefined = undefined async function loadFlow() { loading = true let flow: Flow = { @@ -103,10 +101,7 @@ flow = state.flow pathStoreInit = state.path - draftTriggersFromUrl = state.draft_triggers - selectedTriggerIndexFromUrl = state.selected_trigger - flowBuilder?.setDraftTriggers(draftTriggersFromUrl) - flowBuilder?.setSelectedTriggerIndex(selectedTriggerIndexFromUrl) + savedPrimarySchedule = state.primarySchedule state?.selectedId && (selectedId = state?.selectedId) } else { if (templatePath) { @@ -158,6 +153,7 @@ loadFlow() let getSelectedId: (() => string) | undefined = undefined + let flowBuilder: FlowBuilder | undefined = undefined let getInitialAndModifiedValues: GetInitialAndModifiedValues | undefined = undefined @@ -185,8 +181,7 @@ {flowStateStore} {selectedId} {loading} - {draftTriggersFromUrl} - {selectedTriggerIndexFromUrl} + {savedPrimarySchedule} > diff --git a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte index 8001ee0281..51e0e9d6e3 100644 --- a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte @@ -15,7 +15,6 @@ import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte' import type { ScheduleTrigger } from '$lib/components/triggers' import type { GetInitialAndModifiedValues } from '$lib/components/common/confirmationModal/unsavedTypes' - import type { Trigger } from '$lib/components/triggers/utils' let version: undefined | number = undefined let nodraft = $page.url.searchParams.get('nodraft') @@ -61,9 +60,6 @@ let savedPrimarySchedule: ScheduleTrigger | undefined = stateLoadedFromUrl?.primarySchedule - let draftTriggersFromUrl: Trigger[] | undefined = undefined - let selectedTriggerIndexFromUrl: number | undefined = undefined - let flowBuilder: FlowBuilder | undefined = undefined async function loadFlow(): Promise { @@ -86,19 +82,12 @@ }) const draftOrDeployed = cleanValueProperties(savedFlow?.draft || savedFlow) - const urlScript = cleanValueProperties({ - ...stateLoadedFromUrl.flow, - draft_triggers: stateLoadedFromUrl.draft_triggers - }) + const urlScript = cleanValueProperties(stateLoadedFromUrl.flow) flow = stateLoadedFromUrl.flow - draftTriggersFromUrl = stateLoadedFromUrl.draft_triggers - selectedTriggerIndexFromUrl = stateLoadedFromUrl.selected_trigger - flowBuilder?.setDraftTriggers(draftTriggersFromUrl) - flowBuilder?.setSelectedTriggerIndex(selectedTriggerIndexFromUrl) - const selectedId = stateLoadedFromUrl?.selectedId ?? 'settings-metadata' + savedPrimarySchedule = stateLoadedFromUrl.primarySchedule const reloadAction = () => { stateLoadedFromUrl = undefined - goto(`/flows/edit/${statePath}?selected=${selectedId}`) + goto(`/flows/edit/${statePath}`) loadFlow() } if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(urlScript)) { @@ -144,18 +133,15 @@ ? { ...structuredClone(flowWithDraft.draft), path: flowWithDraft.draft.path ?? flowWithDraft.path // backward compatibility for old drafts missing path - } + } : undefined } as Flow & { - draft?: Flow & { - draft_triggers?: Trigger[] - } + draft?: Flow } if (flowWithDraft.draft != undefined && !nobackenddraft) { flow = flowWithDraft.draft savedPrimarySchedule = flowWithDraft?.draft?.['primary_schedule'] flowBuilder?.setPrimarySchedule(savedPrimarySchedule) - flowBuilder?.setDraftTriggers(flowWithDraft?.draft?.['draft_triggers']) if (!flowWithDraft.draft_only) { const deployed = cleanValueProperties(flowWithDraft) @@ -192,7 +178,6 @@ } } else { flow = flowWithDraft - flowBuilder?.setDraftTriggers(undefined) } } @@ -215,7 +200,6 @@ return } diffDrawer.closeDrawer() - stateLoadedFromUrl = undefined goto(`/flows/edit/${savedFlow.draft.path}`) loadFlow() } @@ -233,7 +217,6 @@ path: savedFlow.path }) } - stateLoadedFromUrl = undefined goto(`/flows/edit/${savedFlow.path}`) loadFlow() } @@ -268,8 +251,6 @@ bind:savedFlow {diffDrawer} {savedPrimarySchedule} - {draftTriggersFromUrl} - {selectedTriggerIndexFromUrl} bind:version bind:getInitialAndModifiedValues > diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index 55bf1d5d1c..a20cfd427a 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -41,26 +41,35 @@ } from 'lucide-svelte' import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte' + import WebhooksPanel from '$lib/components/triggers/webhook/WebhooksPanel.svelte' + import CliHelpBox from '$lib/components/CliHelpBox.svelte' import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte' + import RunPageSchedules from '$lib/components/RunPageSchedules.svelte' import { createAppFromFlow } from '$lib/components/details/createAppFromScript' import { importStore } from '$lib/components/apps/store' import TimeAgo from '$lib/components/TimeAgo.svelte' + import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte' import FlowGraphViewerStep from '$lib/components/FlowGraphViewerStep.svelte' import GfmMarkdown from '$lib/components/GfmMarkdown.svelte' import FlowHistory from '$lib/components/flows/FlowHistory.svelte' + import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte' import Star from '$lib/components/Star.svelte' + import RoutesPanel from '$lib/components/triggers/http/RoutesPanel.svelte' import { Highlight } from 'svelte-highlight' import json from 'svelte-highlight/languages/json' import { writable } from 'svelte/store' - import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte' - import Toggle from '$lib/components/Toggle.svelte' - import { onDestroy, tick } from 'svelte' - import LogViewer from '$lib/components/LogViewer.svelte' - import TriggersEditor from '$lib/components/triggers/TriggersEditor.svelte' - import type { TriggerContext } from '$lib/components/triggers' - import { setContext } from 'svelte' import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte' - import { Triggers } from '$lib/components/triggers/triggers.svelte' + import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte' + import WebsocketTriggersPanel from '$lib/components/triggers/websocket/WebsocketTriggersPanel.svelte' + import KafkaTriggersPanel from '$lib/components/triggers/kafka/KafkaTriggersPanel.svelte' + import NatsTriggersPanel from '$lib/components/triggers/nats/NatsTriggersPanel.svelte' + import PostgresTriggersPanel from '$lib/components/triggers/postgres/PostgresTriggersPanel.svelte' + import Toggle from '$lib/components/Toggle.svelte' + import MqttTriggersPanel from '$lib/components/triggers/mqtt/MqttTriggersPanel.svelte' + import SqsTriggerPanel from '$lib/components/triggers/sqs/SqsTriggerPanel.svelte' + import { onDestroy } from 'svelte' + import LogViewer from '$lib/components/LogViewer.svelte' + import GcpTriggerPanel from '$lib/components/triggers/gcp/GcpTriggerPanel.svelte' let flow: Flow | undefined let can_write = false @@ -76,29 +85,9 @@ let intervalId: NodeJS.Timeout | undefined = undefined - $: { - const cliTrigger = triggersState.triggers.find((t) => t.type === 'cli') - if (cliTrigger) { - cliTrigger.extra = { - cliCommand: `wmill flow run ${flow?.path} -d '${JSON.stringify(args)}'` - } - } - } - const triggersCount = writable(undefined) - // Add triggers context store - const triggersState = new Triggers([ - { type: 'webhook', path: '', isDraft: false }, - { type: 'email', path: '', isDraft: false }, - { type: 'cli', path: '', isDraft: false } - ]) - setContext('TriggerContext', { - triggersCount, - simplifiedPoll: writable(false), - showCaptureHint: writable(undefined), - triggersState - }) + $: cliCommand = `wmill flow run ${flow?.path} -d '${JSON.stringify(args)}'` let previousPath: string | undefined = undefined $: { @@ -107,7 +96,6 @@ previousPath = path loadFlow() loadTriggersCount() - loadTriggers() } } } @@ -136,17 +124,6 @@ }) } - async function loadTriggers(): Promise { - await triggersState.fetchTriggers( - triggersCount, - $workspaceStore, - path, - true, - undefined, - $userStore - ) - } - async function loadFlow(): Promise { flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, @@ -394,7 +371,7 @@ } } let stepDetail: FlowModule | string | undefined = undefined - + let token = 'TOKEN_TO_CREATE' let rightPaneSelected = 'saved_inputs' let savedInputsV2: SavedInputsV2 | undefined = undefined let flowHistory: FlowHistory | undefined = undefined @@ -421,6 +398,7 @@ {#if flow} { + on:select={() => { rightPaneSelected = 'triggers' - await tick() - triggersState.selectedTriggerIndex = triggerIndex }} - small={false} /> {#if $workspaceStore} @@ -616,23 +590,89 @@ + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ +
+ + +
+
+ {#if stepDetail} {/if} - - -
{/if} diff --git a/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte index 2fd36a1cc1..9fcaa0f202 100644 --- a/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte @@ -263,7 +263,7 @@ - + - + - + - + - + - + - + { @@ -120,6 +122,7 @@ searchParams={$page.url.searchParams} {script} {showMeta} + {savedPrimarySchedule} replaceStateFn={(path) => replaceState(path, $page.state)} > diff --git a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte index 803b29bce8..fbe85f83c0 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte @@ -12,7 +12,6 @@ import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte' import type { ScheduleTrigger } from '$lib/components/triggers' import type { GetInitialAndModifiedValues } from '$lib/components/common/confirmationModal/unsavedTypes' - import type { Trigger } from '$lib/components/triggers/utils' let initialState = window.location.hash != '' ? window.location.hash.slice(1) : undefined let initialArgs = {} @@ -27,8 +26,7 @@ let scriptLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined - - let script: (NewScript & { draft_triggers?: Trigger[] }) | undefined = undefined + let script: NewScript | undefined = undefined let initialPath: string = '' @@ -39,7 +37,7 @@ let savedScript: NewScriptWithDraft | undefined = undefined let fullyLoaded = false - let savedPrimarySchedule: ScheduleTrigger | undefined = undefined + let savedPrimarySchedule: ScheduleTrigger | undefined = scriptLoadedFromUrl?.primarySchedule async function loadScript(): Promise { fullyLoaded = false @@ -100,12 +98,10 @@ savedScript = structuredClone(scriptWithDraft) if (scriptWithDraft.draft != undefined) { script = scriptWithDraft.draft - scriptBuilder?.setDraftTriggers(script.draft_triggers) if (script['primary_schedule']) { savedPrimarySchedule = script['primary_schedule'] scriptBuilder?.setPrimarySchedule(savedPrimarySchedule) } - if (!scriptWithDraft.draft_only) { reloadAction = async () => { scriptLoadedFromUrl = undefined @@ -157,7 +153,6 @@ if (script) { initialPath = script.path - scriptBuilder?.setDraftTriggers(script.draft_triggers) scriptBuilder?.setCode(script.content) if (topHash) { script.parent_hash = topHash @@ -181,7 +176,6 @@ } diffDrawer.closeDrawer() goto(`/scripts/edit/${savedScript.draft.path}`) - scriptLoadedFromUrl = undefined loadScript() } @@ -199,7 +193,6 @@ }) } goto(`/scripts/edit/${savedScript.path}`) - scriptLoadedFromUrl = undefined loadScript() } diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index dd4f7fa15f..0a7d41a4dc 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -21,7 +21,7 @@ import { enterpriseLicense, hubBaseUrlStore, userStore, workspaceStore } from '$lib/stores' import { isDeployable, ALL_DEPLOYABLE } from '$lib/utils_deployable' - import { onDestroy, setContext, tick } from 'svelte' + import { onDestroy } from 'svelte' import HighlightCode from '$lib/components/HighlightCode.svelte' import { Tabs, @@ -42,8 +42,10 @@ import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' import SavedInputsV2 from '$lib/components/SavedInputsV2.svelte' + import WebhooksPanel from '$lib/components/triggers/webhook/WebhooksPanel.svelte' import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte' import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte' + import CliHelpBox from '$lib/components/CliHelpBox.svelte' import { Activity, Archive, @@ -66,22 +68,30 @@ import { scriptToHubUrl } from '$lib/hub' import SharedBadge from '$lib/components/SharedBadge.svelte' import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte' + import RunPageSchedules from '$lib/components/RunPageSchedules.svelte' import { createAppFromScript } from '$lib/components/details/createAppFromScript' import { importStore } from '$lib/components/apps/store' import TimeAgo from '$lib/components/TimeAgo.svelte' + import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte' import PersistentScriptDrawer from '$lib/components/PersistentScriptDrawer.svelte' import GfmMarkdown from '$lib/components/GfmMarkdown.svelte' + import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte' import Star from '$lib/components/Star.svelte' import LogViewer from '$lib/components/LogViewer.svelte' + import RoutesPanel from '$lib/components/triggers/http/RoutesPanel.svelte' import { Highlight } from 'svelte-highlight' import json from 'svelte-highlight/languages/json' import { writable } from 'svelte/store' + import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte' + import WebsocketTriggersPanel from '$lib/components/triggers/websocket/WebsocketTriggersPanel.svelte' + import KafkaTriggersPanel from '$lib/components/triggers/kafka/KafkaTriggersPanel.svelte' + import NatsTriggersPanel from '$lib/components/triggers/nats/NatsTriggersPanel.svelte' + import PostgresTriggersPanel from '$lib/components/triggers/postgres/PostgresTriggersPanel.svelte' import Toggle from '$lib/components/Toggle.svelte' import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte' - import type { TriggerContext } from '$lib/components/triggers' - import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte' - import TriggersEditor from '$lib/components/triggers/TriggersEditor.svelte' - import { Triggers } from '$lib/components/triggers/triggers.svelte' + import MqttTriggersPanel from '$lib/components/triggers/mqtt/MqttTriggersPanel.svelte' + import SqsTriggerPanel from '$lib/components/triggers/sqs/SqsTriggerPanel.svelte' + import GcpTriggerPanel from '$lib/components/triggers/gcp/GcpTriggerPanel.svelte' let script: Script | undefined let topHash: string | undefined @@ -97,14 +107,7 @@ let inputSelected: 'saved' | 'history' | undefined = undefined let jsonView = false - $: { - const cliTrigger = triggersState.triggers.find((t) => t.type === 'cli') - if (cliTrigger) { - cliTrigger.extra = { - cliCommand: `wmill script run ${script?.path} -d '${JSON.stringify(args)}'` - } - } - } + $: cliCommand = `wmill script run ${script?.path} -d '${JSON.stringify(args)}'` $: loading = !script @@ -118,19 +121,6 @@ const triggersCount = writable(undefined) - // Add triggers context store - const triggersState = new Triggers([ - { type: 'webhook', path: '', isDraft: false }, - { type: 'email', path: '', isDraft: false }, - { type: 'cli', path: '', isDraft: false } - ]) - setContext('TriggerContext', { - triggersCount, - simplifiedPoll: writable(false), - showCaptureHint: writable(undefined), - triggersState - }) - async function deleteScript(hash: string): Promise { try { await ScriptService.deleteScriptByHash({ workspace: $workspaceStore!, hash }) @@ -177,15 +167,11 @@ } let starred: boolean | undefined = undefined - async function loadTriggers(path: string): Promise { - await triggersState.fetchTriggers( - triggersCount, - $workspaceStore, - path, - false, - undefined, - $userStore - ) + async function loadTriggersCount(path: string) { + $triggersCount = await ScriptService.getTriggersCountOfScript({ + workspace: $workspaceStore!, + path: path + }) } async function loadScript(hash: string): Promise { @@ -208,7 +194,7 @@ can_write = script.workspace_id == $workspaceStore && canWrite(script.path, script.extra_perms!, $userStore) - loadTriggers(script.path) + loadTriggersCount(script.path) if (script.path && script.archived) { const script_by_path = await ScriptService.getScriptByPath({ @@ -511,6 +497,7 @@ } } + let token = 'TOKEN_TO_CREATE' let rightPaneSelected = 'saved_inputs' let savedInputsV2: SavedInputsV2 | undefined = undefined @@ -546,7 +533,11 @@ {#key script.hash} - + { - rightPaneSelected = 'triggers' - }} > { - if (rightPaneSelected !== 'triggers') { - rightPaneSelected = 'triggers' - } - await tick() - triggersState.selectedTriggerIndex = triggerIndex + selected={rightPaneSelected == 'triggers'} + on:select={() => { + rightPaneSelected = 'triggers' }} /> @@ -757,18 +740,76 @@ /> {/if} - - + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
@@ -837,6 +878,12 @@ {/if} + +
+ + +
+
{/key} {/if} diff --git a/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte index f117702fba..b5a5522652 100644 --- a/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte @@ -220,7 +220,7 @@ - + - + import AssignableTags from '$lib/components/AssignableTags.svelte' import CenteredPage from '$lib/components/CenteredPage.svelte' - import { Button, Skeleton, Tab, Tabs } from '$lib/components/common' + import { Alert, Button, Skeleton, Tab, Tabs } from '$lib/components/common' import Popover from '$lib/components/meltComponents/Popover.svelte' import Badge from '$lib/components/common/badge/Badge.svelte' import Drawer from '$lib/components/common/drawer/Drawer.svelte' @@ -24,8 +24,16 @@ userWorkspaces } from '$lib/stores' import { sendUserToast } from '$lib/toast' - import { displayDate, groupBy, pluralize, truncate } from '$lib/utils' - import { AlertTriangle, LineChart, List, Plus, Search } from 'lucide-svelte' + import { + AGENT_WORKER_NAME_PREFIX, + AgentKnownSuffix, + checkIfWorkerHasSpecificSuffix, + displayDate, + groupBy, + pluralize, + truncate + } from '$lib/utils' + import { AlertTriangle, LineChart, List, Plus, Search, Terminal } from 'lucide-svelte' import { getContext, onDestroy, onMount } from 'svelte' import AutoComplete from 'simple-svelte-autocomplete' @@ -33,15 +41,14 @@ import { DEFAULT_TAGS_WORKSPACES_SETTING } from '$lib/consts' import AutoscalingEvents from '$lib/components/AutoscalingEvents.svelte' import HttpAgentWorkerDrawer from '$lib/components/HttpAgentWorkerDrawer.svelte' + import WorkerRepl from '$lib/components/WorkerRepl.svelte' let workers: WorkerPing[] | undefined = undefined let workerGroups: Record | undefined = undefined let groupedWorkers: [string, [string, WorkerPing[]][]][] = [] let intervalId: NodeJS.Timeout | undefined - const splitter = '_%%%_' let customTags: string[] | undefined = undefined - $: groupedWorkers = groupWorkers(workers, workerGroups) function groupWorkers( @@ -145,7 +152,7 @@ let importConfigDrawer: Drawer | undefined = undefined let importConfigCode = '' - + let tag: string = '' async function importSingleWorkerConfig(c: any) { if (typeof c === 'object' && c !== null) { if (!c.name || typeof c.name !== 'string') { @@ -257,8 +264,8 @@ return Math.ceil(occupancy_rate * 100) + '%' } - let newHttpAgentWorkerDrawer: Drawer | undefined = undefined + let replForWorkerDrawer: Drawer | undefined = undefined {#if $superadmin} @@ -291,6 +298,24 @@ + + { + tag = '' + replForWorkerDrawer?.closeDrawer?.() + }} + > +
+ + If no command has been run in the past 2 minutes, the next one may take up to 15 seconds to + start. + + +
+
+
+ {#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.workers} - + + + + {/each} {/if} diff --git a/frontend/src/routes/flows/dev/+page.svelte b/frontend/src/routes/flows/dev/+page.svelte index 52ed7818ab..cffef6240c 100644 --- a/frontend/src/routes/flows/dev/+page.svelte +++ b/frontend/src/routes/flows/dev/+page.svelte @@ -20,10 +20,9 @@ import { userStore, workspaceStore } from '$lib/stores' import { getUserExt } from '$lib/user' import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte' - import type { TriggerContext } from '$lib/components/triggers' + import type { ScheduleTrigger, TriggerContext } from '$lib/components/triggers' import type { FlowPropPickerConfig, PropPickerContext } from '$lib/components/prop_picker' import type { PickableProperties } from '$lib/components/flows/previousResults' - import { Triggers } from '$lib/components/triggers/triggers.svelte' let token = $page.url.searchParams.get('wm_token') ?? undefined let workspace = $page.url.searchParams.get('workspace') ?? undefined @@ -78,12 +77,19 @@ const testStepStore = writable>({}) const selectedIdStore = writable('settings-metadata') + const primaryScheduleStore = writable(undefined) const triggersCount = writable(undefined) + const selectedTriggerStore = writable< + 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll' + >('webhooks') setContext('TriggerContext', { + primarySchedule: primaryScheduleStore, + selectedTrigger: selectedTriggerStore, triggersCount: triggersCount, simplifiedPoll: writable(false), - showCaptureHint: writable(undefined), - triggersState: new Triggers() + defaultValues: writable(undefined), + captureOn: writable(undefined), + showCaptureHint: writable(undefined) }) setContext('FlowEditorContext', {