Compare commits
13 Commits
react2
...
aider-fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eb5ca5ba9 | ||
|
|
c3fa90f245 | ||
|
|
5d5286d627 | ||
|
|
29f92ea297 | ||
|
|
7a43893616 | ||
|
|
ba4c89e7db | ||
|
|
d6bf6f6b55 | ||
|
|
d223b0b12e | ||
|
|
b0b3ab595a | ||
|
|
a5979810eb | ||
|
|
07c2ff5668 | ||
|
|
065a814d35 | ||
|
|
422a02d8f7 |
@@ -5,7 +5,7 @@ alwaysApply: false
|
||||
---
|
||||
# Svelte 5 Best Practices
|
||||
|
||||
This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. They should be applied on every new files created, but not on existing svelte 4 files unless specifically asked to.
|
||||
This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. These rules MUST NOT be applied on svelte 4 files unless explicitly asked to do so.
|
||||
|
||||
## Reactivity with Runes
|
||||
|
||||
|
||||
76
.cursor/rules/windmill-overview.mdc
Normal file
76
.cursor/rules/windmill-overview.mdc
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Windmill Overview
|
||||
|
||||
Windmill is an open-source developer platform for building internal tools, API integrations, background jobs, workflows, and user interfaces. It offers a unified system where scripts are automatically turned into sharable UIs and can be composed into flows or embedded in custom applications.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Script Development and Execution**: Write and run scripts in Python, TypeScript/JavaScript (Deno/Bun), Go, Bash, SQL, and other languages
|
||||
- **Workflow Orchestration**: Compose scripts into multi-step flows with conditional logic, loops, and error handling
|
||||
- **UI Generation**: Automatically generate UIs from scripts or build custom applications with a low-code editor
|
||||
- **Job Scheduling**: Trigger scripts and flows on schedules, webhooks, or external events
|
||||
- **Resource Management**: Securely store and use credentials, databases, and other connections
|
||||
|
||||
## Platform Architecture
|
||||
|
||||
The Windmill platform consists of several key components:
|
||||
|
||||
- **Frontend UI**: Web-based interface for script and flow development, app building, and result visualization
|
||||
- **API Server**: Central API that handles authentication, resource management, and job coordination
|
||||
- **Workers**: Execute scripts in their respective environments with proper sandboxing
|
||||
- **Database**: PostgreSQL database for storage of scripts, flows, resources, job results, and more
|
||||
- **Job Queue**: Queue system for managing job execution, implemented in PostgreSQL
|
||||
- **Client Libraries**: Libraries for interacting with Windmill from Python, TypeScript, or command line
|
||||
|
||||
# Windmill Backend Architecture
|
||||
|
||||
The Windmill backend is written in Rust and consists of several services working together. These services are designed for horizontal scaling with stateless API servers and workers that can be deployed across multiple machines.
|
||||
|
||||
## Key Components
|
||||
|
||||
- **API Server (`windmill-api`)**: Handles HTTP requests, authentication, and resource management
|
||||
- **Queue Manager (`windmill-queue`)**: Manages the job queue in PostgreSQL
|
||||
- **Worker System (`windmill-worker`)**: Executes jobs in sandboxed environments
|
||||
- **Common Utilities (`windmill-common`)**: Shared code used by multiple services
|
||||
- **Git Sync (`windmill-git-sync`)**: Synchronizes scripts with Git repositories
|
||||
|
||||
## Job Execution System
|
||||
|
||||
The job execution process follows these steps:
|
||||
|
||||
1. The API server receives a request to run a script or flow and creates a job record in the database
|
||||
2. The job is added to the queue system in PostgreSQL
|
||||
3. Workers continuously poll the queue for jobs matching their capabilities
|
||||
4. When a job is picked up, it's routed to the appropriate language executor
|
||||
5. The script is executed in a sandboxed environment using NSJAIL for security
|
||||
6. Results are processed and stored in the database
|
||||
7. For flows, each step creates a new job that goes through the same process
|
||||
|
||||
Windmill supports worker tags and groups to route jobs to workers with specific capabilities or resource access.
|
||||
|
||||
# Windmill Frontend Architecture
|
||||
|
||||
The Windmill frontend is built with Svelte and provides several key interfaces for interacting with the platform.
|
||||
|
||||
## Key Components
|
||||
|
||||
- **Script Builder**: Code editor with language support, schema inference, and dependency management
|
||||
- **Flow Builder**: Visual editor for creating multi-step workflows with branching and looping
|
||||
- **App Editor**: Grid-based editor for building custom UIs that integrate scripts and flows
|
||||
- **Schema Form System**: Generates form interfaces from script parameters automatically
|
||||
- **Result Viewer**: Visualizes job results, logs, and execution status
|
||||
|
||||
The frontend uses the Monaco editor (same as VS Code) for code editing, with specialized language support for all supported script languages.
|
||||
|
||||
## UI Framework
|
||||
|
||||
The frontend is built with Svelte, providing a reactive and component-based architecture. Key frontend technologies include:
|
||||
|
||||
- **Svelte/SvelteKit**: Core framework for UI components and routing
|
||||
- **Monaco Editor**: Code editing experience similar to VS Code
|
||||
- **Schema Form**: Automatic UI generation from TypeScript/JSON schemas
|
||||
- **Tailwind CSS**: Utility-first CSS framework for styling
|
||||
60
.github/workflows/aider-after-review.yaml
vendored
60
.github/workflows/aider-after-review.yaml
vendored
@@ -70,6 +70,8 @@ jobs:
|
||||
|
||||
# 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
|
||||
@@ -78,10 +80,6 @@ jobs:
|
||||
# PR_BODY=$(echo "$PR_DETAILS" | jq -r .body)
|
||||
|
||||
# Get all PR review comments
|
||||
REVIEW_COMMENTS=$(gh pr view $PR_NUMBER --json reviews -q '.reviews[] | select(.state == "CHANGES_REQUESTED") | .body' --repo $GITHUB_REPOSITORY)
|
||||
REVIEW_BODY_Q=$(printf '%q' "$REVIEW_BODY")
|
||||
|
||||
# Update query to get review comments from all review types, not just "CHANGES_REQUESTED"
|
||||
ALL_REVIEW_COMMENTS=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
@@ -89,15 +87,63 @@ jobs:
|
||||
| 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."
|
||||
printf "%s\nREVIEW:\n%s\nREVIEW_COMMENTS:\n%s" \
|
||||
"$BASE_PROMPT" "$REVIEW_BODY_Q" "$ALL_REVIEW_COMMENTS" > "$PROMPT_FILE_PATH"
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
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 review prompt
|
||||
run: |
|
||||
aider \
|
||||
--read .cursor/rules/rust-best-practices.mdc \
|
||||
--read .cursor/rules/svelte5-best-practices.mdc \
|
||||
--read .cursor/rules/windmill-overview.mdc \
|
||||
${{ env.FILES_TO_EDIT }} \
|
||||
--model gemini/gemini-2.5-pro-preview-05-06 \
|
||||
--message-file .github/aider/review-prompt.txt \
|
||||
--yes \
|
||||
@@ -110,6 +156,7 @@ jobs:
|
||||
# Check if there are any changes to commit
|
||||
if [[ -z "$(git status --porcelain)" ]]; then
|
||||
echo "No changes detected after running Aider."
|
||||
echo "HAS_CHANGES=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -125,6 +172,7 @@ jobs:
|
||||
echo "Attempting to push changes to PR branch $CURRENT_BRANCH_NAME for PR #${{ github.event.pull_request.number }}"
|
||||
|
||||
# Pull latest changes to avoid rejection due to non-fast-forward
|
||||
git config pull.rebase true
|
||||
git pull origin $CURRENT_BRANCH_NAME
|
||||
|
||||
if git push origin $CURRENT_BRANCH_NAME; then
|
||||
|
||||
1
.github/workflows/aider.yaml
vendored
1
.github/workflows/aider.yaml
vendored
@@ -266,6 +266,7 @@ jobs:
|
||||
aider \
|
||||
--read .cursor/rules/rust-best-practices.mdc \
|
||||
--read .cursor/rules/svelte5-best-practices.mdc \
|
||||
--read .cursor/rules/windmill-overview.mdc \
|
||||
${{ env.FILES_TO_EDIT }} \
|
||||
--model gemini/gemini-2.5-pro-preview-05-06 \
|
||||
--message-file .github/aider/issue-prompt.txt \
|
||||
|
||||
35
.github/workflows/claude.yml
vendored
Normal file
35
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
2
.github/workflows/discord-notification.yml
vendored
2
.github/workflows/discord-notification.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
notify_discord_when_pr_opened:
|
||||
if: github.event.pull_request.draft == false
|
||||
if: (github.event.pull_request.draft == false) && (github.event.action == 'opened' || github.event.action == 'ready_for_review')
|
||||
uses: ./.github/workflows/shareable-discord-notification.yml
|
||||
with:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
|
||||
223
.github/workflows/linear-issue.yaml
vendored
Normal file
223
.github/workflows/linear-issue.yaml
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
name: External Aider Issue Fix
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [external_issue_fix]
|
||||
|
||||
jobs:
|
||||
auto-fix:
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
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 }}
|
||||
|
||||
steps:
|
||||
- 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: 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: 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}}')
|
||||
|
||||
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. 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" "$INSTRUCTION" > "$PROMPT_FILE_PATH"
|
||||
else
|
||||
echo "::warning::API call failed (HTTP $HTTP_CODE). Using raw issue content."
|
||||
printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
|
||||
"$BASE_PROMPT" "$ISSUE_BODY" "$INSTRUCTION" > "$PROMPT_FILE_PATH"
|
||||
fi
|
||||
rm -f /tmp/api_response.txt
|
||||
|
||||
echo "Prompt created and written to $PROMPT_FILE_PATH"
|
||||
echo "PROMPT_FILE_PATH=$PROMPT_FILE_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Store the issue title for PR creation
|
||||
ISSUE_TITLE_SAFE=$(echo "$ISSUE_TITLE" | tr -d '\n' | sed 's/"/\\"/g')
|
||||
echo "ISSUE_TITLE=$ISSUE_TITLE_SAFE" >> $GITHUB_OUTPUT
|
||||
|
||||
# Generate unique branch name using timestamp and issue info
|
||||
ISSUE_ID="${{ github.event.client_payload.issue_id }}"
|
||||
BRANCH_NAME="aider-fix-linear-issue-$ISSUE_ID"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Probe Chat for Relevant Files
|
||||
id: probe_files
|
||||
env:
|
||||
PROMPT_CONTENT_FILE: ${{ steps.create_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 .cursor/rules/rust-best-practices.mdc \
|
||||
--read .cursor/rules/svelte5-best-practices.mdc \
|
||||
--read .cursor/rules/windmill-overview.mdc \
|
||||
${{ 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: |
|
||||
BRANCH_NAME="${{ steps.create_prompt.outputs.BRANCH_NAME }}"
|
||||
|
||||
# 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
|
||||
|
||||
# Check if there are any changes to commit
|
||||
if git diff --quiet && git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -am "Auto-fix using Aider for external issue [skip ci]" || echo "No changes to commit"
|
||||
fi
|
||||
|
||||
git push origin $BRANCH_NAME
|
||||
echo "Pushed to branch $BRANCH_NAME"
|
||||
echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success() && 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_TITLE: ${{ steps.create_prompt.outputs.ISSUE_TITLE }}
|
||||
ISSUE_ID: ${{ github.event.client_payload.issue_id }}
|
||||
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 an external issue: ${ISSUE_TITLE}
|
||||
|
||||
## Aider Output
|
||||
\`\`\`
|
||||
$(cat .github/aider/aider-output.txt || echo "No output available")
|
||||
\`\`\`
|
||||
EOL
|
||||
|
||||
# Create PR using the file for the body content
|
||||
gh pr create \
|
||||
--title "[Aider PR] Fix: ${ISSUE_TITLE}" \
|
||||
--body-file /tmp/pr-description.md \
|
||||
--head "$PR_BRANCH" \
|
||||
--base main || echo "PR already exists or couldn't be created"
|
||||
71
CLAUDE.md
Normal file
71
CLAUDE.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Windmill Overview
|
||||
|
||||
Windmill is an open-source developer platform for building internal tools, API integrations, background jobs, workflows, and user interfaces. It offers a unified system where scripts are automatically turned into sharable UIs and can be composed into flows or embedded in custom applications.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Script Development and Execution**: Write and run scripts in Python, TypeScript/JavaScript (Deno/Bun), Go, Bash, SQL, and other languages
|
||||
- **Workflow Orchestration**: Compose scripts into multi-step flows with conditional logic, loops, and error handling
|
||||
- **UI Generation**: Automatically generate UIs from scripts or build custom applications with a low-code editor
|
||||
- **Job Scheduling**: Trigger scripts and flows on schedules, webhooks, or external events
|
||||
- **Resource Management**: Securely store and use credentials, databases, and other connections
|
||||
|
||||
## Platform Architecture
|
||||
|
||||
The Windmill platform consists of several key components:
|
||||
|
||||
- **Frontend UI**: Web-based interface for script and flow development, app building, and result visualization
|
||||
- **API Server**: Central API that handles authentication, resource management, and job coordination
|
||||
- **Workers**: Execute scripts in their respective environments with proper sandboxing
|
||||
- **Database**: PostgreSQL database for storage of scripts, flows, resources, job results, and more
|
||||
- **Job Queue**: Queue system for managing job execution, implemented in PostgreSQL
|
||||
- **Client Libraries**: Libraries for interacting with Windmill from Python, TypeScript, or command line
|
||||
|
||||
# Windmill Backend Architecture
|
||||
|
||||
The Windmill backend is written in Rust and consists of several services working together. These services are designed for horizontal scaling with stateless API servers and workers that can be deployed across multiple machines.
|
||||
|
||||
## Key Components
|
||||
|
||||
- **API Server (`windmill-api`)**: Handles HTTP requests, authentication, and resource management
|
||||
- **Queue Manager (`windmill-queue`)**: Manages the job queue in PostgreSQL
|
||||
- **Worker System (`windmill-worker`)**: Executes jobs in sandboxed environments
|
||||
- **Common Utilities (`windmill-common`)**: Shared code used by multiple services
|
||||
- **Git Sync (`windmill-git-sync`)**: Synchronizes scripts with Git repositories
|
||||
|
||||
## Job Execution System
|
||||
|
||||
The job execution process follows these steps:
|
||||
|
||||
1. The API server receives a request to run a script or flow and creates a job record in the database
|
||||
2. The job is added to the queue system in PostgreSQL
|
||||
3. Workers continuously poll the queue for jobs matching their capabilities
|
||||
4. When a job is picked up, it's routed to the appropriate language executor
|
||||
5. The script is executed in a sandboxed environment using NSJAIL for security
|
||||
6. Results are processed and stored in the database
|
||||
7. For flows, each step creates a new job that goes through the same process
|
||||
|
||||
Windmill supports worker tags and groups to route jobs to workers with specific capabilities or resource access.
|
||||
|
||||
# Windmill Frontend Architecture
|
||||
|
||||
The Windmill frontend is built with Svelte and provides several key interfaces for interacting with the platform.
|
||||
|
||||
## Key Components
|
||||
|
||||
- **Script Builder**: Code editor with language support, schema inference, and dependency management
|
||||
- **Flow Builder**: Visual editor for creating multi-step workflows with branching and looping
|
||||
- **App Editor**: Grid-based editor for building custom UIs that integrate scripts and flows
|
||||
- **Schema Form System**: Generates form interfaces from script parameters automatically
|
||||
- **Result Viewer**: Visualizes job results, logs, and execution status
|
||||
|
||||
The frontend uses the Monaco editor (same as VS Code) for code editing, with specialized language support for all supported script languages.
|
||||
|
||||
## UI Framework
|
||||
|
||||
The frontend is built with Svelte, providing a reactive and component-based architecture. Key frontend technologies include:
|
||||
|
||||
- **Svelte/SvelteKit**: Core framework for UI components and routing
|
||||
- **Monaco Editor**: Code editing experience similar to VS Code
|
||||
- **Schema Form**: Automatic UI generation from TypeScript/JSON schemas
|
||||
- **Tailwind CSS**: Utility-first CSS framework for styling
|
||||
132
backend/.sqlx/query-c51f9ad5133c46fd7c499b8339dbbf3f3059bbb85de07ee3b4b4cea971984a52.json
generated
Normal file
132
backend/.sqlx/query-c51f9ad5133c46fd7c499b8339dbbf3f3059bbb85de07ee3b4b4cea971984a52.json
generated
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n mqtt_resource_path,\n subscribe_topics as \"subscribe_topics: _\",\n v3_config as \"v3_config: _\",\n v5_config as \"v5_config: _\",\n client_version AS \"client_version: _\",\n client_id,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n mqtt_trigger\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "mqtt_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "subscribe_topics: _",
|
||||
"type_info": "JsonbArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "v3_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "v5_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "client_version: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "mqtt_client_version",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"v3",
|
||||
"v5"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "client_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c51f9ad5133c46fd7c499b8339dbbf3f3059bbb85de07ee3b4b4cea971984a52"
|
||||
}
|
||||
104
backend/CLAUDE.md
Normal file
104
backend/CLAUDE.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Windmill Backend - Rust Best Practices
|
||||
|
||||
## Project Structure
|
||||
|
||||
Windmill uses a workspace-based architecture with multiple crates:
|
||||
|
||||
- **windmill-api**: API server functionality
|
||||
- **windmill-worker**: Job execution
|
||||
- **windmill-common**: Shared code used by all crates
|
||||
- **windmill-queue**: Job & flow queuing
|
||||
- **windmill-audit**: Audit logging
|
||||
- Other specialized crates (git-sync, autoscaling, etc.)
|
||||
|
||||
## Adding New Code
|
||||
|
||||
### Module Organization
|
||||
|
||||
- Place new code in the appropriate crate based on functionality
|
||||
- For API endpoints, create or modify files in `windmill-api/src/` organized by domain
|
||||
- For shared functionality, use `windmill-common/src/`
|
||||
- Use the `_ee.rs` suffix for enterprise-only modules
|
||||
- Follow existing patterns for file structure and organization
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use the custom `Error` enum from `windmill-common::error`
|
||||
- Return `Result<T, Error>` or `JsonResult<T>` for functions that can fail
|
||||
- Use the `?` operator for error propagation
|
||||
- Add location tracking to errors using `#[track_caller]`
|
||||
|
||||
### Database Operations
|
||||
|
||||
- Use `sqlx` for database operations with prepared statements
|
||||
- Leverage existing database helper functions in `db.rs` modules
|
||||
- Use transactions for multi-step operations
|
||||
- Handle database errors properly
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- Follow existing patterns in the `windmill-api` crate
|
||||
- Use axum's routing system and extractors
|
||||
- Group related routes together
|
||||
- Use consistent response formats (JSON)
|
||||
- Follow proper authentication and authorization patterns
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
When generating code, especially involving `serde`, `sqlx`, and `tokio`, prioritize performance by applying the following principles:
|
||||
|
||||
### Serde Optimizations (Serialization & Deserialization)
|
||||
|
||||
- **Specify Structure Explicitly:** When defining structs for Serde (`#[derive(Serialize, Deserialize)]`), use `#[serde(...` attributes extensively. This includes:
|
||||
- `#[serde(rename = "...")]` or `#[serde(alias = "...")]` to map external names precisely, avoiding dynamic lookups.
|
||||
- `#[serde(default)]` for optional fields with default values, reducing parsing complexity.
|
||||
- `#[serde(skip_serializing_if = "...")]` to avoid writing fields that meet a certain condition (e.g., `Option::is_none()`, `Vec::is_empty()`, or a custom function), reducing output size and serialization work.
|
||||
- `#[serde(skip_serializing)]` or `#[serde(skip_deserializing)]` for fields that should _not_ be included.
|
||||
- **Prefer Borrowing:** Where possible and safe (data lifetime allows), use `Cow<'a, str>` or `&'a str` (with `#[serde(borrow)]`) instead of `String` for string fields during deserialization. This avoids allocating new strings, enabling zero-copy reading from the input buffer. Apply this principle to byte slices (`&'a [u8]` / `Cow<'a, [u8]>`) and potentially borrowed vectors as well.
|
||||
- **Avoid Intermediate `Value`:** Unless the data structure is truly dynamic or unknown at compile time, deserialize directly into a well-defined struct or enum rather than into `serde_json::Value` (or equivalent for other formats). This avoids unnecessary heap allocations and type switching.
|
||||
|
||||
### SQLx Optimizations (Database Interaction)
|
||||
|
||||
- **Select Only Necessary Columns:** In `SELECT` queries, list specific column names rather than using `SELECT *`. This reduces data transferred from the database and the work needed for hydration/deserialization.
|
||||
- **Batch Operations:** For multiple `INSERT`, `UPDATE`, or `DELETE` statements, prefer executing them in a single query if the database and driver support it efficiently (e.g., `INSERT INTO ... VALUES (...), (...), ...`). This minimizes round trips to the database.
|
||||
- **Avoid N+1 Queries:** Do not loop through results of one query and execute a separate query for each item (e.g., fetching users, then querying for each user's profile in a loop). Instead, use JOINs or a single query with an `IN` clause to fetch related data efficiently.
|
||||
- **Deserialize Directly:** Use `#[derive(FromRow)]` on structs and ensure the struct fields match the selected columns in the query. This allows SQLx to hydrate objects directly, avoiding intermediate data structures.
|
||||
- **Parameterize Queries:** Always use SQLx's query methods (`.bind(...)`) to pass values as parameters rather than string formatting. This prevents SQL injection and allows the database to cache query plans, improving performance on repeated executions.
|
||||
|
||||
### Tokio Optimizations (Asynchronous Runtime)
|
||||
|
||||
- **Avoid Blocking Operations:** **Crucially**, never perform blocking operations (synchronous file I/O, `std::thread::sleep`, CPU-bound loops, `std::sync::Mutex::lock`, blocking network calls without `tokio::net`) directly within an `async fn` or a standard `tokio::spawn` task. Blocking pauses the entire worker thread, potentially starving other tasks. Use `tokio::task::spawn_blocking` for CPU-intensive work or blocking I/O.
|
||||
- **Use Tokio's Async Primitives:** Prefer `tokio::sync` (channels, mutexes, semaphores), `tokio::io`, `tokio::net`, and `tokio::time` over their `std` counterparts in asynchronous contexts. These are designed to yield control back to the scheduler.
|
||||
- **Manage Concurrency:** Be mindful of how many tasks are spawned. Creating a new task for every tiny piece of work can introduce overhead. Group related asynchronous operations where appropriate.
|
||||
- **Handle Shared State Efficiently:** Use `Arc` for shared ownership in concurrent tasks. When shared state needs mutation, prefer `tokio::sync::Mutex` over `std::sync::Mutex` in `async` code. Consider `tokio::sync::RwLock` if reads significantly outnumber writes. Minimize the duration for which locks are held.
|
||||
- **Understand `.await`:** Place `.await` strategically to allow the runtime to switch to other ready tasks. Ensure that `.await` points to genuinely asynchronous operations.
|
||||
- **Backpressure:** If dealing with data streams or queues between tasks, implement backpressure mechanisms (e.g., bounded channels like `tokio::sync::mpsc::channel`) to prevent one component from overwhelming another or critical resources like the database.
|
||||
|
||||
## Enterprise Features
|
||||
|
||||
- Use feature flags for enterprise functionality
|
||||
- Conditionally compile with `#[cfg(feature = "enterprise")]`
|
||||
- Isolate enterprise code in separate modules
|
||||
|
||||
## Code Style
|
||||
|
||||
- Group imports by external and internal crates
|
||||
- Place struct/enum definitions before implementations
|
||||
- Group similar functionality together
|
||||
- Use descriptive naming consistent with the codebase
|
||||
- Follow existing patterns for async code using tokio
|
||||
|
||||
## Testing
|
||||
|
||||
- Write unit tests for core functionality
|
||||
- Use the `#[cfg(test)]` module for test code
|
||||
- For database tests, use the existing test utilities
|
||||
|
||||
## Common Crates Used
|
||||
|
||||
- **tokio**: For async runtime
|
||||
- **axum**: For web server and routing
|
||||
- **sqlx**: For database operations
|
||||
- **serde**: For serialization/deserialization
|
||||
- **tracing**: For logging and diagnostics
|
||||
- **reqwest**: For HTTP client functionality
|
||||
@@ -235,7 +235,7 @@ itertools = "^0"
|
||||
regex = "^1"
|
||||
semver = "^1"
|
||||
|
||||
v8 = "=130.0.7" # Exact version
|
||||
v8 = "=130.0.7" # Exact version NOTE: Do not forget to update version and hash in flake.nix
|
||||
deno_fetch = "0.214.0"
|
||||
deno_tls = "0.177.0"
|
||||
deno_console = "0.190.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
3efa7fa51e9f93f60e141fef5b8b9338528cf955
|
||||
11917062c8a5ea230f27fe750cbde1dbdc0512f9
|
||||
@@ -1122,10 +1122,13 @@ pub async fn reload_s3_cache_setting(db: &DB) {
|
||||
if let Err(e) = setting {
|
||||
tracing::error!("Error parsing s3 cache config: {:?}", e)
|
||||
} else {
|
||||
let s3_client = build_object_store_from_settings(setting.unwrap()).await;
|
||||
let setting = setting.unwrap();
|
||||
let bucket = setting.get_bucket().map(|b| b.to_string());
|
||||
let s3_client = build_object_store_from_settings(setting).await;
|
||||
if let Err(e) = s3_client {
|
||||
tracing::error!("Error building s3 client from settings: {:?}", e)
|
||||
} else {
|
||||
tracing::info!("Loaded object store {:?}", bucket);
|
||||
*s3_cache_settings = Some(s3_client.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16316,6 +16316,7 @@ components:
|
||||
- schedule
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
repositories:
|
||||
type: array
|
||||
items:
|
||||
@@ -16383,6 +16384,7 @@ components:
|
||||
- schedule
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
required:
|
||||
- script_path
|
||||
- git_repo_resource_path
|
||||
|
||||
@@ -44,6 +44,7 @@ use windmill_common::{
|
||||
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
use windmill_queue::TriggerKind;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -397,6 +398,17 @@ async fn create_trigger(
|
||||
|
||||
increase_trigger_version_and_commit(tx).await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
|
||||
Some(format!("HTTP trigger '{}' created", ct.path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, format!("{}", ct.path)))
|
||||
}
|
||||
|
||||
@@ -544,20 +556,32 @@ async fn update_trigger(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"http_triggers.update",
|
||||
ActionKind::Create,
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(path),
|
||||
Some(&ct.path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
increase_trigger_version_and_commit(tx).await?;
|
||||
|
||||
Ok(path.to_string())
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
|
||||
Some(format!("HTTP trigger '{}' updated", ct.path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ct.path.to_string())
|
||||
}
|
||||
|
||||
async fn delete_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> error::Result<String> {
|
||||
@@ -587,6 +611,17 @@ async fn delete_trigger(
|
||||
|
||||
increase_trigger_version_and_commit(tx).await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: path.to_string() },
|
||||
Some(format!("HTTP trigger '{}' deleted", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("HTTP trigger {path} deleted"))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
trigger_helpers::TriggerJobArgs,
|
||||
users::fetch_api_authed,
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::TriggerKind;
|
||||
|
||||
use axum::{
|
||||
@@ -244,25 +245,25 @@ pub struct EditMqttTrigger {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct MqttTrigger {
|
||||
mqtt_resource_path: String,
|
||||
subscribe_topics: Vec<SqlxJson<SubscribeTopic>>,
|
||||
v3_config: Option<SqlxJson<MqttV3Config>>,
|
||||
v5_config: Option<SqlxJson<MqttV5Config>>,
|
||||
client_id: Option<String>,
|
||||
pub mqtt_resource_path: String,
|
||||
pub subscribe_topics: Vec<SqlxJson<SubscribeTopic>>,
|
||||
pub v3_config: Option<SqlxJson<MqttV3Config>>,
|
||||
pub v5_config: Option<SqlxJson<MqttV5Config>>,
|
||||
pub client_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client_version: Option<MqttClientVersion>,
|
||||
path: String,
|
||||
script_path: String,
|
||||
is_flow: bool,
|
||||
workspace_id: String,
|
||||
edited_by: String,
|
||||
email: String,
|
||||
edited_at: chrono::DateTime<chrono::Utc>,
|
||||
extra_perms: Option<serde_json::Value>,
|
||||
error: Option<String>,
|
||||
server_id: Option<String>,
|
||||
last_server_ping: Option<chrono::DateTime<chrono::Utc>>,
|
||||
enabled: bool,
|
||||
pub client_version: Option<MqttClientVersion>,
|
||||
pub path: String,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub workspace_id: String,
|
||||
pub edited_by: String,
|
||||
pub email: String,
|
||||
pub edited_at: chrono::DateTime<chrono::Utc>,
|
||||
pub extra_perms: Option<serde_json::Value>,
|
||||
pub error: Option<String>,
|
||||
pub server_id: Option<String>,
|
||||
pub last_server_ping: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
@@ -515,13 +516,14 @@ pub async fn test_mqtt_connection(
|
||||
|
||||
pub async fn create_mqtt_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(new_mqtt_trigger): Json<NewMqttTrigger>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
if *CLOUD_HOSTED {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Mqtt triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
|
||||
"MQTT triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -606,7 +608,18 @@ pub async fn create_mqtt_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok((StatusCode::CREATED, path.to_string()))
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::MqttTrigger { path: path.to_string() },
|
||||
Some(format!("MQTT trigger '{}' created", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, format!("{}", path.to_string())))
|
||||
}
|
||||
|
||||
pub async fn list_mqtt_triggers(
|
||||
@@ -719,6 +732,7 @@ pub async fn get_mqtt_trigger(
|
||||
|
||||
pub async fn update_mqtt_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(mqtt_trigger): Json<EditMqttTrigger>,
|
||||
@@ -787,7 +801,7 @@ pub async fn update_mqtt_trigger(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"mqtt_triggers.update",
|
||||
ActionKind::Create,
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&path),
|
||||
None,
|
||||
@@ -796,11 +810,23 @@ pub async fn update_mqtt_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(workspace_path.to_string())
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::MqttTrigger { path: path.clone() },
|
||||
Some(format!("MQTT trigger '{}' updated", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(path.to_string())
|
||||
}
|
||||
|
||||
pub async fn delete_mqtt_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> error::Result<String> {
|
||||
@@ -834,7 +860,18 @@ pub async fn delete_mqtt_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Mqtt trigger {path} deleted"))
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::MqttTrigger { path: path.to_string() },
|
||||
Some(format!("MQTT trigger '{}' deleted", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("MQTT trigger {path} deleted"))
|
||||
}
|
||||
|
||||
pub async fn exists_mqtt_trigger(
|
||||
@@ -864,6 +901,7 @@ pub async fn exists_mqtt_trigger(
|
||||
|
||||
pub async fn set_enabled(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(payload): Json<SetEnabled>,
|
||||
@@ -913,6 +951,17 @@ pub async fn set_enabled(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::MqttTrigger { path: path.to_string() },
|
||||
Some(format!("MQTT trigger '{}' updated", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"successfully updated mqtt trigger at path {} to status {}",
|
||||
path, payload.enabled
|
||||
|
||||
@@ -28,6 +28,7 @@ use windmill_common::{
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
|
||||
use super::{
|
||||
create_logical_replication_slot_query, create_publication_query, drop_publication_query,
|
||||
@@ -453,6 +454,17 @@ pub async fn create_postgres_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::PostgresTrigger { path: path.to_string() },
|
||||
Some(format!("Postgres trigger '{}' created", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, path.to_string()))
|
||||
}
|
||||
|
||||
@@ -1158,7 +1170,7 @@ pub async fn update_postgres_trigger(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"postgres_triggers.update",
|
||||
ActionKind::Create,
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&path),
|
||||
None,
|
||||
@@ -1167,11 +1179,23 @@ pub async fn update_postgres_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::PostgresTrigger { path: path.to_string() },
|
||||
Some(format!("Postgres trigger '{}' updated", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(workspace_path.to_string())
|
||||
}
|
||||
|
||||
pub async fn delete_postgres_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
@@ -1203,6 +1227,17 @@ pub async fn delete_postgres_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::PostgresTrigger { path: path.to_string() },
|
||||
Some(format!("Postgres trigger '{}' deleted", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Postgres trigger {path} deleted"))
|
||||
}
|
||||
|
||||
@@ -1231,6 +1266,7 @@ pub async fn exists_postgres_trigger(
|
||||
|
||||
pub async fn set_enabled(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(payload): Json<SetEnabled>,
|
||||
@@ -1279,6 +1315,17 @@ pub async fn set_enabled(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::PostgresTrigger { path: path.to_string() },
|
||||
Some(format!("Postgres trigger '{}' updated", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"succesfully updated postgres trigger at path {} to status {}",
|
||||
path, payload.enabled
|
||||
|
||||
@@ -28,6 +28,7 @@ use windmill_common::{
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
INSTANCE_NAME,
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
use windmill_queue::PushArgsOwned;
|
||||
|
||||
use windmill_queue::TriggerKind;
|
||||
@@ -195,6 +196,7 @@ async fn get_websocket_trigger(
|
||||
|
||||
async fn create_websocket_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ct): Json<NewWebsocketTrigger>,
|
||||
@@ -244,11 +246,23 @@ async fn create_websocket_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::WebsocketTrigger { path: ct.path.clone() },
|
||||
Some(format!("WebSocket trigger '{}' created", ct.path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, format!("{}", ct.path)))
|
||||
}
|
||||
|
||||
async fn update_websocket_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ct): Json<EditWebsocketTrigger>,
|
||||
@@ -287,16 +301,27 @@ async fn update_websocket_trigger(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"websocket_triggers.update",
|
||||
ActionKind::Create,
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(path),
|
||||
Some(&ct.path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(path.to_string())
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::WebsocketTrigger { path: ct.path.clone() },
|
||||
Some(format!("WebSocket trigger '{}' updated", ct.path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ct.path.to_string())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -306,6 +331,7 @@ pub struct SetEnabled {
|
||||
|
||||
pub async fn set_enabled(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(payload): Json<SetEnabled>,
|
||||
@@ -339,6 +365,17 @@ pub async fn set_enabled(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::WebsocketTrigger { path: path.to_string() },
|
||||
Some(format!("WebSocket trigger '{}' updated", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"succesfully updated WebSocket trigger at path {} to status {}",
|
||||
path, payload.enabled
|
||||
@@ -347,6 +384,7 @@ pub async fn set_enabled(
|
||||
|
||||
async fn delete_websocket_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> error::Result<String> {
|
||||
@@ -373,6 +411,17 @@ async fn delete_websocket_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::WebsocketTrigger { path: path.to_string() },
|
||||
Some(format!("WebSocket trigger '{}' deleted", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("WebSocket trigger {path} deleted"))
|
||||
}
|
||||
|
||||
|
||||
@@ -763,6 +763,45 @@ pub(crate) async fn tarball_workspace(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "mqtt_trigger"))]
|
||||
{
|
||||
let mqtt_triggers = sqlx::query_as!(
|
||||
crate::mqtt_triggers::MqttTrigger,
|
||||
r#"
|
||||
SELECT
|
||||
mqtt_resource_path,
|
||||
subscribe_topics as "subscribe_topics: _",
|
||||
v3_config as "v3_config: _",
|
||||
v5_config as "v5_config: _",
|
||||
client_version AS "client_version: _",
|
||||
client_id,
|
||||
workspace_id,
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
edited_by,
|
||||
email,
|
||||
edited_at,
|
||||
server_id,
|
||||
last_server_ping,
|
||||
extra_perms,
|
||||
error,
|
||||
enabled
|
||||
FROM
|
||||
mqtt_trigger
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for trigger in mqtt_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
archive
|
||||
.write_to_archive(&trigger_str, &format!("{}.mqtt_trigger.json", trigger.path))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if include_users.unwrap_or(false) {
|
||||
|
||||
@@ -120,62 +120,6 @@ impl UserDB {
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// set_session_context(
|
||||
// username TEXT,
|
||||
// groups TEXT,
|
||||
// pgroups TEXT,
|
||||
// folders_read TEXT,
|
||||
// folders_write TEXT
|
||||
// )
|
||||
|
||||
// sqlx::query!(
|
||||
// "SELECT set_config('session.user', $1, true)",
|
||||
// authed.username()
|
||||
// )
|
||||
// .fetch_optional(&mut *tx)
|
||||
// .await?;
|
||||
|
||||
// sqlx::query!(
|
||||
// "SELECT set_config('session.groups', $1, true)",
|
||||
// &authed.groups().join(",")
|
||||
// )
|
||||
// .fetch_optional(&mut *tx)
|
||||
// .await?;
|
||||
|
||||
// sqlx::query!(
|
||||
// "SELECT set_config('session.pgroups', $1, true)",
|
||||
// &authed
|
||||
// .groups()
|
||||
// .iter()
|
||||
// .map(|x| format!("g/{}", x))
|
||||
// .collect::<Vec<_>>()
|
||||
// .join(",")
|
||||
// )
|
||||
// .fetch_optional(&mut *tx)
|
||||
// .await?;
|
||||
|
||||
// sqlx::query!(
|
||||
// "SELECT set_config('session.folders_read', $1, true)",
|
||||
// folders_read
|
||||
// .iter()
|
||||
// .map(|x| x.0.clone())
|
||||
// .collect::<Vec<_>>()
|
||||
// .join(",")
|
||||
// )
|
||||
// .fetch_optional(&mut *tx)
|
||||
// .await?;
|
||||
|
||||
// sqlx::query!(
|
||||
// "SELECT set_config('session.folders_write', $1, true)",
|
||||
// folders_write
|
||||
// .iter()
|
||||
// .map(|x| x.0.clone())
|
||||
// .collect::<Vec<_>>()
|
||||
// .join(",")
|
||||
// )
|
||||
// .fetch_optional(&mut *tx)
|
||||
// .await?;
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ pub async fn register_metric_for_job(
|
||||
.bind(timeseries_int)
|
||||
.bind(timeseries_float)
|
||||
.execute(db)
|
||||
.warn_after_seconds(1)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
|
||||
Ok(metric_id)
|
||||
|
||||
@@ -414,6 +414,15 @@ pub enum ObjectSettings {
|
||||
Azure(AzureBlobResource),
|
||||
}
|
||||
|
||||
impl ObjectSettings {
|
||||
pub fn get_bucket(&self) -> Option<&String> {
|
||||
match self {
|
||||
ObjectSettings::S3(s3_settings) => s3_settings.bucket.as_ref(),
|
||||
ObjectSettings::Azure(azure_settings) => Some(&azure_settings.container_name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
pub async fn build_object_store_from_settings(
|
||||
settings: ObjectSettings,
|
||||
|
||||
@@ -516,6 +516,7 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
|
||||
pub struct PythonAnnotations {
|
||||
pub no_cache: bool,
|
||||
pub no_postinstall: bool,
|
||||
pub skip_result_postprocessing: bool,
|
||||
pub py310: bool,
|
||||
pub py311: bool,
|
||||
pub py312: bool,
|
||||
|
||||
@@ -27,6 +27,14 @@ pub enum DeployedObject {
|
||||
ResourceType { path: String },
|
||||
User { email: String },
|
||||
Group { name: String },
|
||||
HttpTrigger { path: String },
|
||||
WebsocketTrigger { path: String },
|
||||
KafkaTrigger { path: String },
|
||||
NatsTrigger { path: String },
|
||||
PostgresTrigger { path: String },
|
||||
MqttTrigger { path: String },
|
||||
SqsTrigger { path: String },
|
||||
GcpTrigger { path: String },
|
||||
}
|
||||
|
||||
impl DeployedObject {
|
||||
@@ -42,6 +50,14 @@ impl DeployedObject {
|
||||
DeployedObject::ResourceType { path, .. } => path.to_owned(),
|
||||
DeployedObject::User { email } => format!("users/{email}"),
|
||||
DeployedObject::Group { name } => format!("groups/{name}"),
|
||||
DeployedObject::HttpTrigger { path } => path.to_owned(),
|
||||
DeployedObject::WebsocketTrigger { path } => path.to_owned(),
|
||||
DeployedObject::KafkaTrigger { path } => path.to_owned(),
|
||||
DeployedObject::NatsTrigger { path } => path.to_owned(),
|
||||
DeployedObject::PostgresTrigger { path } => path.to_owned(),
|
||||
DeployedObject::MqttTrigger { path } => path.to_owned(),
|
||||
DeployedObject::SqsTrigger { path } => path.to_owned(),
|
||||
DeployedObject::GcpTrigger { path } => path.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +80,14 @@ impl DeployedObject {
|
||||
DeployedObject::ResourceType { .. } => None,
|
||||
DeployedObject::User { .. } => None,
|
||||
DeployedObject::Group { .. } => None,
|
||||
DeployedObject::HttpTrigger { .. } => None,
|
||||
DeployedObject::WebsocketTrigger { .. } => None,
|
||||
DeployedObject::KafkaTrigger { .. } => None,
|
||||
DeployedObject::NatsTrigger { .. } => None,
|
||||
DeployedObject::PostgresTrigger { .. } => None,
|
||||
DeployedObject::MqttTrigger { .. } => None,
|
||||
DeployedObject::SqsTrigger { .. } => None,
|
||||
DeployedObject::GcpTrigger { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ pub async fn update_workflow_as_code_status(
|
||||
parent_job
|
||||
)
|
||||
.execute(db)
|
||||
.warn_after_seconds(5)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
tracing::error!(
|
||||
|
||||
@@ -4715,4 +4715,4 @@ pub async fn get_same_worker_job(
|
||||
same_worker_job.job_id, e
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn append_logs_with_compaction(
|
||||
&w_id,
|
||||
)
|
||||
.fetch_one(db)
|
||||
.warn_after_seconds(1)
|
||||
.warn_after_seconds(10)
|
||||
.await;
|
||||
match log_length {
|
||||
Ok(length) => {
|
||||
|
||||
@@ -842,6 +842,8 @@ pub async fn handle_python_job(
|
||||
) -> windmill_common::error::Result<Box<RawValue>> {
|
||||
let script_path = crate::common::use_flow_root_path(job.runnable_path());
|
||||
|
||||
let annotations = PythonAnnotations::parse(inner_content);
|
||||
|
||||
let (py_version, mut additional_python_paths) = handle_python_deps(
|
||||
job_dir,
|
||||
requirements_o,
|
||||
@@ -856,10 +858,10 @@ pub async fn handle_python_job(
|
||||
canceled_by,
|
||||
&mut Some(occupancy_metrics),
|
||||
precomputed_agent_info,
|
||||
annotations,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let PythonAnnotations { no_postinstall, .. } = PythonAnnotations::parse(inner_content);
|
||||
tracing::debug!("Finished handling python dependencies");
|
||||
let python_path = get_python_path(
|
||||
py_version,
|
||||
@@ -872,7 +874,7 @@ pub async fn handle_python_job(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !no_postinstall {
|
||||
if !annotations.no_postinstall {
|
||||
if let Err(e) = postinstall(&mut additional_python_paths, job_dir, job, conn).await {
|
||||
tracing::error!("Postinstall stage has failed. Reason: {e}");
|
||||
}
|
||||
@@ -938,6 +940,8 @@ pub async fn handle_python_job(
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let postprocessor = get_result_postprocessor(annotations.skip_result_postprocessing);
|
||||
|
||||
let os_main_override = if let Some(main_override) = main_name.as_ref() {
|
||||
format!("os.environ[\"MAIN_OVERRIDE\"] = \"{main_override}\"\n")
|
||||
} else {
|
||||
@@ -984,7 +988,8 @@ def res_to_json(res):
|
||||
for k, v in res.items():
|
||||
if type(v).__name__ == 'bytes':
|
||||
res[k] = to_b_64(v)
|
||||
return re.sub(replace_invalid_fields, ' null ', json.dumps(res, separators=(',', ':'), default=str).replace('\n', ''))
|
||||
unprocessed = json.dumps(res, separators=(',', ':'), default=str).replace('\n', '')
|
||||
return {postprocessor}
|
||||
|
||||
try:
|
||||
{preprocessor}
|
||||
@@ -1428,6 +1433,7 @@ async fn handle_python_deps(
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
|
||||
precomputed_agent_info: Option<PrecomputedAgentInfo>,
|
||||
annotations: PythonAnnotations,
|
||||
) -> error::Result<(PyVersion, Vec<String>)> {
|
||||
create_dependencies_dir(job_dir).await;
|
||||
|
||||
@@ -1445,7 +1451,6 @@ async fn handle_python_deps(
|
||||
let mut annotated_pyv_numeric = None;
|
||||
let is_deployed = requirements_o.is_some();
|
||||
let instance_pyv = PyVersion::from_instance_version(job_id, w_id, conn).await;
|
||||
let annotations = windmill_common::worker::PythonAnnotations::parse(inner_content);
|
||||
let requirements = match requirements_o {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
@@ -2326,6 +2331,15 @@ fn get_pyv_from_requirements_lines(requirements_lines: &[&str]) -> PyVersion {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns code snippet that needs to be injected into wrapper to post-process results or leave unprocessed
|
||||
fn get_result_postprocessor<'a>(skip: bool) -> &'a str {
|
||||
if skip {
|
||||
"unprocessed"
|
||||
} else {
|
||||
"re.sub(replace_invalid_fields, ' null ', unprocessed)"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::JobCompletedSender;
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -2373,6 +2387,7 @@ pub async fn start_worker(
|
||||
.await
|
||||
.to_vec();
|
||||
|
||||
let annotations = PythonAnnotations::parse(inner_content);
|
||||
let context_envs = build_envs_map(context).await;
|
||||
let (_, additional_python_paths) = handle_python_deps(
|
||||
job_dir,
|
||||
@@ -2388,6 +2403,7 @@ pub async fn start_worker(
|
||||
&mut canceled_by,
|
||||
&mut None,
|
||||
None,
|
||||
annotations,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2405,6 +2421,7 @@ pub async fn start_worker(
|
||||
) = prepare_wrapper(job_dir, false, None, None, inner_content, script_path).await?;
|
||||
|
||||
{
|
||||
let postprocessor = get_result_postprocessor(annotations.skip_result_postprocessing);
|
||||
let indented_transforms = transforms
|
||||
.lines()
|
||||
.map(|x| format!(" {}", x))
|
||||
@@ -2456,7 +2473,8 @@ for line in sys.stdin:
|
||||
for k, v in res.items():
|
||||
if type(v).__name__ == 'bytes':
|
||||
res[k] = to_b_64(v)
|
||||
res_json = re.sub(replace_invalid_fields, ' null ', json.dumps(res, separators=(',', ':'), default=str).replace('\n', ''))
|
||||
unprocessed = json.dumps(res, separators=(',', ':'), default=str).replace('\n', '')
|
||||
res_json = {postprocessor}
|
||||
sys.stdout.write("wm_res[success]:" + res_json + "\n")
|
||||
except BaseException as e:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
|
||||
@@ -543,7 +543,7 @@ pub async fn process_completed_job(
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.warn_after_seconds(20)
|
||||
.await?;
|
||||
add_time!(bench, "updated flow status END");
|
||||
return Ok(r);
|
||||
@@ -583,7 +583,7 @@ pub async fn process_completed_job(
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.warn_after_seconds(20)
|
||||
.await?;
|
||||
return Ok(r);
|
||||
}
|
||||
|
||||
@@ -1294,7 +1294,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
job_completed_tx,
|
||||
worker_name,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.warn_after_seconds(20)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
@@ -1606,7 +1606,7 @@ pub async fn handle_flow(
|
||||
let schedule_path = schedule_path.as_ref().unwrap();
|
||||
|
||||
let schedule = get_schedule_opt(db, &flow_job.workspace_id, schedule_path)
|
||||
.warn_after_seconds(5)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
|
||||
if let Some(schedule) = schedule {
|
||||
@@ -1617,7 +1617,7 @@ pub async fn handle_flow(
|
||||
flow_job.runnable_path.as_ref().unwrap(),
|
||||
&flow_job.workspace_id,
|
||||
)
|
||||
.warn_after_seconds(5)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
{
|
||||
match err {
|
||||
@@ -1647,7 +1647,7 @@ pub async fn handle_flow(
|
||||
worker_dir,
|
||||
worker_name,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.warn_after_seconds(20)
|
||||
.await?;
|
||||
match next {
|
||||
PushNextFlowJob::Rec(nrec) => {
|
||||
@@ -1663,7 +1663,7 @@ pub async fn handle_flow(
|
||||
);
|
||||
job_completed_tx
|
||||
.send(SendResult::UpdateFlow(update_flow), false)
|
||||
.warn_after_seconds(3)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal_err(format!(
|
||||
@@ -1797,7 +1797,7 @@ async fn push_next_flow_job(
|
||||
flow_job.workspace_id.as_str()
|
||||
)
|
||||
.fetch_one(db)
|
||||
.warn_after_seconds(3)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
if no_flow_overlap {
|
||||
let overlapping = sqlx::query_scalar!(
|
||||
@@ -1818,7 +1818,7 @@ async fn push_next_flow_job(
|
||||
flow_job.runnable_path()
|
||||
)
|
||||
.fetch_all(db)
|
||||
.warn_after_seconds(3)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
if overlapping.len() > 0 {
|
||||
let overlapping_str = overlapping
|
||||
|
||||
64
flake.nix
64
flake.nix
@@ -13,6 +13,8 @@
|
||||
config.allowUnfree = true;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
lib = pkgs.lib;
|
||||
stdenv = pkgs.stdenv;
|
||||
rust = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [
|
||||
"rust-src" # for rust-analyzer
|
||||
@@ -26,6 +28,7 @@
|
||||
libxml2.dev
|
||||
xmlsec.dev
|
||||
libxslt.dev
|
||||
libclang.dev
|
||||
libtool
|
||||
nodejs
|
||||
postgresql
|
||||
@@ -42,16 +45,15 @@
|
||||
PKG_CONFIG_PATH = pkgs.lib.makeSearchPath "lib/pkgconfig"
|
||||
(with pkgs; [ openssl.dev libxml2.dev xmlsec.dev libxslt.dev ]);
|
||||
RUSTY_V8_ARCHIVE = let
|
||||
version = "130.0.1";
|
||||
# NOTE: needs to be same as in Cargo.toml
|
||||
version = "130.0.7";
|
||||
target = pkgs.hostPlatform.rust.rustcTarget;
|
||||
sha256 = {
|
||||
x86_64-linux =
|
||||
"sha256-qc25H3Aj2KRhsAZ+2SD1c4RmweVK07oW71opZXRuUoc=";
|
||||
aarch64-linux =
|
||||
"sha256-qc25H3Aj2KRhsAZ+2SD1c4RmweVK07oW71opZXRuUoc=";
|
||||
"sha256-pkdsuU6bAkcIHEZUJOt5PXdzK424CEgTLXjLtQ80t10=";
|
||||
aarch64-linux = pkgs.lib.fakeHash;
|
||||
x86_64-darwin = pkgs.lib.fakeHash;
|
||||
aarch64-darwin =
|
||||
"sha256-d1QTLt8gOUFxACes4oyIYgDF/srLOEk+5p5Oj1ECajQ=";
|
||||
aarch64-darwin = pkgs.lib.fakeHash;
|
||||
}.${system};
|
||||
in pkgs.fetchurl {
|
||||
name = "librusty_v8-${version}";
|
||||
@@ -77,25 +79,31 @@
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = buildInputs ++ (with pkgs; [
|
||||
# Essentials
|
||||
rust
|
||||
git
|
||||
xcaddy
|
||||
sqlx-cli
|
||||
flock
|
||||
sccache
|
||||
nsjail
|
||||
deno
|
||||
|
||||
# Python
|
||||
flock
|
||||
python3
|
||||
python3Packages.pip
|
||||
uv
|
||||
|
||||
# Other languages
|
||||
deno
|
||||
nushell
|
||||
go
|
||||
bun
|
||||
uv
|
||||
nushell
|
||||
dotnet-sdk_9
|
||||
oracle-instantclient
|
||||
ansible
|
||||
|
||||
# LSP/Local dev
|
||||
svelte-language-server
|
||||
ansible
|
||||
taplo
|
||||
]);
|
||||
packages = [
|
||||
@@ -185,6 +193,40 @@
|
||||
ANSIBLE_GALAXY_PATH = "${pkgs.ansible}/bin/ansible-galaxy";
|
||||
RUST_LOG = "debug";
|
||||
SQLX_OFFLINE = "true";
|
||||
|
||||
# See this issue: https://github.com/NixOS/nixpkgs/issues/370494
|
||||
# Allows to build jemalloc on nixos
|
||||
CFLAGS = "-Wno-error=int-conversion";
|
||||
|
||||
# Need to tell bindgen where to find libclang
|
||||
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||
|
||||
# LD_LIBRARY_PATH = "${pkgs.gcc.lib}/lib";
|
||||
|
||||
# Set C flags for Rust's bindgen program. Unlike ordinary C
|
||||
# compilation, bindgen does not invoke $CC directly. Instead it
|
||||
# uses LLVM's libclang. To make sure all necessary flags are
|
||||
# included we need to look in a few places.
|
||||
# See https://web.archive.org/web/20220523141208/https://hoverbear.org/blog/rust-bindgen-in-nix/
|
||||
BINDGEN_EXTRA_CLANG_ARGS =
|
||||
"${builtins.readFile "${stdenv.cc}/nix-support/libc-crt1-cflags"} ${
|
||||
builtins.readFile "${stdenv.cc}/nix-support/libc-cflags"
|
||||
}${builtins.readFile "${stdenv.cc}/nix-support/cc-cflags"}${
|
||||
builtins.readFile "${stdenv.cc}/nix-support/libcxx-cxxflags"
|
||||
} -idirafter ${pkgs.libiconv}/include ${
|
||||
lib.optionalString stdenv.cc.isClang
|
||||
"-idirafter ${stdenv.cc.cc}/lib/clang/${
|
||||
lib.getVersion stdenv.cc.cc
|
||||
}/include"
|
||||
}${
|
||||
lib.optionalString stdenv.cc.isGNU
|
||||
"-isystem ${stdenv.cc.cc}/include/c++/${
|
||||
lib.getVersion stdenv.cc.cc
|
||||
} -isystem ${stdenv.cc.cc}/include/c++/${
|
||||
lib.getVersion stdenv.cc.cc
|
||||
}/${stdenv.hostPlatform.config} -idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/14.2.1/include"
|
||||
}"; # NOTE: It is hardcoded to 14.2.1 -------------------------------------------------------------^^^^^^
|
||||
# Please update the version here as well if you want to update flake.
|
||||
};
|
||||
packages.default = self.packages.${system}.windmill;
|
||||
packages.windmill-client = pkgs.buildNpmPackage {
|
||||
|
||||
241
frontend/CLAUDE.md
Normal file
241
frontend/CLAUDE.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Svelte 5 Best Practices
|
||||
|
||||
This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. These rules MUST NOT be applied on svelte 4 files unless explicitly asked to do so.
|
||||
|
||||
## Reactivity with Runes
|
||||
|
||||
Svelte 5 introduces Runes for more explicit and flexible reactivity.
|
||||
|
||||
1. **Embrace Runes for State Management**:
|
||||
|
||||
- Use `$state` for reactive local component state.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={increment}>
|
||||
Clicked {count} {count === 1 ? 'time' : 'times'}
|
||||
</button>
|
||||
```
|
||||
|
||||
- Use `$derived` for computed values based on other reactive state.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
const doubled = $derived(count * 2);
|
||||
</script>
|
||||
|
||||
<p>{count} * 2 = {doubled}</p>
|
||||
```
|
||||
|
||||
- Use `$effect` for side effects that need to run when reactive values change (e.g., logging, manual DOM manipulation, data fetching). Remember `$effect` does not run on the server.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
console.log('The count is now', count);
|
||||
if (count > 5) {
|
||||
alert('Count is too high!');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
2. **Props with `$props`**:
|
||||
|
||||
- Declare component props using `$props()`. This offers better clarity and flexibility compared to `export let`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
// ChildComponent.svelte
|
||||
let { name, age = $state(30) } = $props();
|
||||
</script>
|
||||
|
||||
<p>Name: {name}</p>
|
||||
<p>Age: {age}</p>
|
||||
```
|
||||
|
||||
- For bindable props, use `$bindable`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
// MyInput.svelte
|
||||
let { value = $bindable() } = $props();
|
||||
</script>
|
||||
|
||||
<input bind:value />
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
- **Use direct event attributes**: Svelte 5 moves away from `on:` directives for DOM events.
|
||||
- **Do**: `<button onclick={handleClick}>...</button>`
|
||||
- **Don't**: `<button on:click={handleClick}>...</button>`
|
||||
- **For component events, prefer callback props**: Instead of `createEventDispatcher`, pass functions as props.
|
||||
|
||||
```svelte
|
||||
<!-- Parent.svelte -->
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
let message = $state('');
|
||||
function handleChildEvent(detail) {
|
||||
message = detail;
|
||||
}
|
||||
</script>
|
||||
<Child onCustomEvent={handleChildEvent} />
|
||||
<p>Message from child: {message}</p>
|
||||
|
||||
<!-- Child.svelte -->
|
||||
<script>
|
||||
let { onCustomEvent } = $props();
|
||||
function emitEvent() {
|
||||
onCustomEvent('Hello from child!');
|
||||
}
|
||||
</script>
|
||||
<button onclick={emitEvent}>Send Event</button>
|
||||
```
|
||||
|
||||
## Snippets for Content Projection
|
||||
|
||||
- **Use `{#snippet ...}` and `{@render ...}` instead of slots**: Snippets are more powerful and flexible.
|
||||
|
||||
```svelte
|
||||
<!-- Parent.svelte -->
|
||||
<script>
|
||||
import Card from './Card.svelte';
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
{#snippet title()}
|
||||
My Awesome Title
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<p>Some interesting content here.</p>
|
||||
{/snippet}
|
||||
</Card>
|
||||
|
||||
<!-- Card.svelte -->
|
||||
<script>
|
||||
let { title, content } = $props();
|
||||
</script>
|
||||
|
||||
<article>
|
||||
<header>{@render title()}</header>
|
||||
<div>{@render content()}</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
- Default content is passed via the `children` prop (which is a snippet).
|
||||
```svelte
|
||||
<!-- Wrapper.svelte -->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
<div>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Component Design
|
||||
|
||||
1. **Create Small, Reusable Components**: Break down complex UIs into smaller, focused components. Each component should have a single responsibility. This also aids performance by limiting the scope of reactivity updates.
|
||||
2. **Descriptive Naming**: Use clear and descriptive names for variables, functions, and components.
|
||||
3. **Minimize Logic in Components**: Move complex business logic to utility functions or services. Keep components focused on presentation and interaction.
|
||||
|
||||
## State Management (Stores)
|
||||
|
||||
1. **Segment Stores**: Avoid a single global store. Create multiple stores, each responsible for a specific piece of global state (e.g., `userStore.js`, `themeStore.js`). This can help limit reactivity updates to only the parts of the UI that depend on specific state segments.
|
||||
2. **Use Custom Stores for Complex Logic**: For stores with related methods, create custom stores.
|
||||
|
||||
```javascript
|
||||
// counterStore.js
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
function createCounter() {
|
||||
const { subscribe, set, update } = writable(0)
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
increment: () => update((n) => n + 1),
|
||||
decrement: () => update((n) => n - 1),
|
||||
reset: () => set(0)
|
||||
}
|
||||
}
|
||||
export const counter = createCounter()
|
||||
```
|
||||
|
||||
3. **Use Context API for Localized State**: For state shared within a component subtree, consider Svelte's context API (`setContext`, `getContext`) instead of global stores when the state doesn't need to be truly global.
|
||||
|
||||
## Performance Optimizations (Svelte 5)
|
||||
|
||||
When generating Svelte 5 code, prioritize frontend performance by applying the following principles:
|
||||
|
||||
### General Svelte 5 Principles
|
||||
|
||||
- **Leverage the Compiler:** Trust Svelte's compiler to generate optimized JavaScript. Avoid manual DOM manipulation (`document.querySelector`, etc.) unless absolutely necessary for integrating third-party libraries that lack Svelte adapters.
|
||||
- **Keep Components Small and Focused:** Reinforcing from Component Design, smaller components lead to less complex reactivity graphs and more targeted, efficient updates.
|
||||
|
||||
### Reactivity & State Management
|
||||
|
||||
- **Optimize Computations with `$derived`:** Always use `$derived` for computed values that depend on other state. This ensures the computation only runs when its specific dependencies change, avoiding unnecessary work compared to recomputing derived values in `$effect` or less efficient methods.
|
||||
- **Minimize `$effect` Usage:** Use `$effect` sparingly and only for true side effects that interact with the outside world or non-Svelte state. Avoid putting complex logic or state updates _within_ an `$effect` unless those updates are explicitly intended as a reaction to external changes or non-Svelte state. Excessive or complex effects can impact rendering performance.
|
||||
- **Structure State for Fine-Grained Updates:** Design your `$state` objects or variables such that updates affect only the necessary parts of the UI. Avoid putting too much unrelated state into a single large object that gets frequently updated, as this can potentially trigger broader updates than necessary. Consider normalizing complex, nested state.
|
||||
|
||||
### List Rendering (`{#each}`)
|
||||
|
||||
- **Mandate `key` Attribute:** Always use a `key` attribute (`{#each items as item (item.id)}`) that refers to a unique, stable identifier for each item in a list. This is critical for allowing Svelte to efficiently update, reorder, add, or remove list items without destroying and re-creating unnecessary DOM elements and component instances.
|
||||
|
||||
### Component Loading & Bundling
|
||||
|
||||
- **Implement Lazy Loading/Code Splitting:** For routes, components, or modules that are not immediately needed on page load, use dynamic imports (`import(...)`) to split the code bundle. SvelteKit handles this automatically for routes, but it can be applied manually to components using helper patterns if needed.
|
||||
- **Be Mindful of Third-Party Libraries:** When incorporating external libraries, import only the necessary functions or components to minimize the final bundle size. Prefer libraries designed to be tree-shakeable.
|
||||
|
||||
### Rendering & DOM
|
||||
|
||||
- **Use CSS for Animations/Transitions:** Prefer CSS animations or transitions where possible for performance. Svelte's built-in `transition:` directive is also highly optimized and should be used for complex state-driven transitions, but simple cases can often use plain CSS.
|
||||
- **Optimize Image Loading:** Implement best practices for images: use optimized formats (WebP, AVIF), lazy loading (`loading="lazy"`), and responsive images (`<picture>`, `srcset`) to avoid loading unnecessarily large images.
|
||||
|
||||
### Server-Side Rendering (SSR) & Hydration
|
||||
|
||||
- **Ensure SSR Compatibility:** Write components that can be rendered on the server for faster initial page loads. Avoid relying on browser-specific APIs (like `window` or `document`) in the main `<script>` context. If necessary, use `$effect` or check `if (browser)` inside effects to run browser-specific code only on the client.
|
||||
- **Minimize Work During Hydration:** Structure components and data fetching such that minimal complex setup or computation is required when the client-side Svelte code takes over from the server-rendered HTML. Heavy synchronous work during hydration can block the main thread.
|
||||
|
||||
## General Clean Code Practices
|
||||
|
||||
1. **Organized File Structure**: Group related files together. A common structure:
|
||||
```
|
||||
/src
|
||||
|-- /routes // Page components (if using a router like SvelteKit)
|
||||
|-- /lib // Utility functions, services, constants (SvelteKit often uses this)
|
||||
| |-- /stores
|
||||
| |-- /utils
|
||||
| |-- /services
|
||||
| |-- /components // Reusable UI components
|
||||
|-- App.svelte
|
||||
|-- main.js (or main.ts)
|
||||
```
|
||||
2. **Scoped Styles**: Keep CSS scoped to components to avoid unintended side effects and improve maintainability. Avoid `:global` where possible.
|
||||
3. **Immutability**: With Svelte 5 and `$state`, direct assignments to properties of `$state` objects (`obj.prop = value;`) are generally fine as Svelte's reactivity system handles updates. However, for non-rune state or when interacting with other systems, understanding and sometimes preferring immutable updates (creating new objects/arrays) can still be relevant.
|
||||
4. **Use `class:` and `style:` directives**: For dynamic classes and styles, use Svelte's built-in directives for cleaner templates and potentially optimized updates.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let isActive = $state(true);
|
||||
let color = $state('blue');
|
||||
</script>
|
||||
|
||||
<div class:active={isActive} style:color={color}>
|
||||
Hello
|
||||
</div>
|
||||
```
|
||||
|
||||
5. **Stay Updated**: Keep Svelte and its related packages up to date to benefit from the latest features, performance improvements, and security fixes.
|
||||
6383
frontend/package-lock.json
generated
6383
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -80,12 +80,13 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-css-language-features": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-html-language-features": "^11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-json-language-features": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-languages": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-typescript-language-features": "~11.1.2",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~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",
|
||||
@@ -113,10 +114,10 @@
|
||||
"idb": "^8.0.2",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~11.1.2",
|
||||
"monaco-editor-wrapper": "6.1.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~16.1.1",
|
||||
"monaco-editor-wrapper": "6.7.0",
|
||||
"monaco-graphql": "^1.6.0",
|
||||
"monaco-languageclient": "9.1.1",
|
||||
"monaco-languageclient": "9.6.0",
|
||||
"monaco-vim": "^0.4.1",
|
||||
"ol": "^7.4.0",
|
||||
"openai": "^4.87.1",
|
||||
@@ -132,9 +133,9 @@
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"vscode": "npm:@codingame/monaco-vscode-api@~11.1.2",
|
||||
"vscode": "npm:@codingame/monaco-vscode-extension-api@~16.1.1",
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.0.8",
|
||||
"vscode-uri": "~3.1.0",
|
||||
"vscode-ws-jsonrpc": "~3.4.0",
|
||||
"windmill-parser-wasm-csharp": "^1.437.1",
|
||||
"windmill-parser-wasm-go": "^1.429.0",
|
||||
|
||||
@@ -547,7 +547,7 @@
|
||||
bind:selected={value}
|
||||
options={itemsType?.multiselect ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
on:open={() => {
|
||||
onopen={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
/>
|
||||
@@ -568,7 +568,7 @@
|
||||
}
|
||||
options={itemsType?.enum ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
on:open={() => {
|
||||
onopen={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -207,25 +207,23 @@
|
||||
<MultiSelect
|
||||
outerDivClass="text-secondary !bg-surface-disabled !border-0"
|
||||
selected={config.custom_tags}
|
||||
on:change={(e) => {
|
||||
console.log(e.detail.type, config?.custom_tags)
|
||||
if (e.detail && config?.custom_tags) {
|
||||
if (e.detail.type === 'add') {
|
||||
onchange={(e) => {
|
||||
console.log(e.type, config?.custom_tags)
|
||||
if (e && config?.custom_tags) {
|
||||
if (e.type === 'add') {
|
||||
config.custom_tags = [
|
||||
...config.custom_tags,
|
||||
...(e.detail.option ? [e.detail.option.toString()] : [])
|
||||
...(e.option ? [e.option.toString()] : [])
|
||||
]
|
||||
} else if (e.detail.type === 'remove') {
|
||||
config.custom_tags = config.custom_tags.filter((t) => t !== e.detail.option)
|
||||
} else if (e.type === 'remove') {
|
||||
config.custom_tags = config.custom_tags.filter((t) => t !== e.option)
|
||||
if (config?.custom_tags && config.custom_tags.length == 0) {
|
||||
config.custom_tags = undefined
|
||||
}
|
||||
} else if (e.detail.type === 'removeAll') {
|
||||
} else if (e.type === 'removeAll') {
|
||||
config.custom_tags = undefined
|
||||
} else {
|
||||
console.error(
|
||||
`Priority tags multiselect - unknown event type: '${e.detail.type}'`
|
||||
)
|
||||
console.error(`Priority tags multiselect - unknown event type: '${e.type}'`)
|
||||
}
|
||||
dispatch('dirty')
|
||||
}
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
import { BROWSER } from 'esm-env'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
// import '@codingame/monaco-vscode-standalone-languages'
|
||||
import '@codingame/monaco-vscode-standalone-languages'
|
||||
import '@codingame/monaco-vscode-standalone-json-language-features'
|
||||
import '@codingame/monaco-vscode-standalone-typescript-language-features'
|
||||
import { editor as meditor } from 'monaco-editor'
|
||||
|
||||
import { initializeVscode } from './vscode'
|
||||
import EditorTheme from './EditorTheme.svelte'
|
||||
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
|
||||
buildWorkerDefinition()
|
||||
|
||||
const SIDE_BY_SIDE_MIN_WIDTH = 700
|
||||
|
||||
export let automaticLayout = true
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
updateOptions,
|
||||
extToLang
|
||||
} from '$lib/editorUtils'
|
||||
import type { Disposable } from 'vscode'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { type Preview, ResourceService, UserService } from '$lib/gen'
|
||||
import type { Text } from 'yjs'
|
||||
@@ -143,7 +142,6 @@
|
||||
import { setupTypeAcquisition, type DepsToGet } from '$lib/ata/index'
|
||||
import { initWasmTs } from '$lib/infer'
|
||||
import { initVim } from './monaco_keybindings'
|
||||
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
|
||||
import { parseTypescriptDeps } from '$lib/relative_imports'
|
||||
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
@@ -219,7 +217,6 @@
|
||||
|
||||
console.log('uri', uri)
|
||||
|
||||
buildWorkerDefinition()
|
||||
|
||||
function computeUri(filePath: string, scriptLang: string | undefined) {
|
||||
let file
|
||||
@@ -435,9 +432,9 @@
|
||||
return scriptLang
|
||||
}
|
||||
|
||||
let command: Disposable | undefined = undefined
|
||||
let command: IDisposable | undefined = undefined
|
||||
|
||||
let sqlTypeCompletor: Disposable | undefined = undefined
|
||||
let sqlTypeCompletor: IDisposable | undefined = undefined
|
||||
|
||||
$: initialized && lang === 'sql' && scriptLang
|
||||
? addSqlTypeCompletions()
|
||||
@@ -498,7 +495,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
let sqlSchemaCompletor: Disposable | undefined = undefined
|
||||
let sqlSchemaCompletor: IDisposable | undefined = undefined
|
||||
|
||||
function updateSchema() {
|
||||
const newSchemaRes = lang === 'graphql' ? args?.api : args?.database
|
||||
@@ -636,7 +633,7 @@
|
||||
|
||||
$: $reviewingChanges && autocompletor?.reject()
|
||||
|
||||
let completorDisposable: Disposable | undefined = undefined
|
||||
let completorDisposable: IDisposable | undefined = undefined
|
||||
let autocompletor: Autocompletor | undefined = undefined
|
||||
function addSuperCompletor(editor: meditor.IStandaloneCodeEditor) {
|
||||
try {
|
||||
|
||||
@@ -495,7 +495,6 @@
|
||||
|
||||
async function updateJobId() {
|
||||
if (jobId !== job?.id) {
|
||||
console.log('updating job id', globalDurationStatuses.length)
|
||||
$localModuleStates = {}
|
||||
flowTimeline?.reset()
|
||||
timeout && clearTimeout(timeout)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { BROWSER } from 'esm-env'
|
||||
|
||||
import { editor as meditor } from 'monaco-editor'
|
||||
// import '@codingame/monaco-vscode-standalone-languages'
|
||||
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
let cssClassesLoaded = $state(false)
|
||||
let tailwindClassesLoaded = $state(false)
|
||||
|
||||
// import '@codingame/monaco-vscode-standalone-languages'
|
||||
import '@codingame/monaco-vscode-standalone-languages'
|
||||
import '@codingame/monaco-vscode-standalone-json-language-features'
|
||||
import '@codingame/monaco-vscode-standalone-css-language-features'
|
||||
import '@codingame/monaco-vscode-standalone-typescript-language-features'
|
||||
@@ -54,7 +54,6 @@
|
||||
type IDisposable
|
||||
} from 'monaco-editor'
|
||||
|
||||
|
||||
import { allClasses } from './apps/editor/componentsPanel/cssUtils'
|
||||
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
@@ -65,7 +64,6 @@
|
||||
import EditorTheme from './EditorTheme.svelte'
|
||||
import { vimMode } from '$lib/stores'
|
||||
import { initVim } from './monaco_keybindings'
|
||||
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
|
||||
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
|
||||
// import { createConfiguredEditor } from 'vscode/monaco'
|
||||
// import type { IStandaloneCodeEditor } from 'vscode/vscode/vs/editor/standalone/browser/standaloneCodeEditor'
|
||||
@@ -131,8 +129,6 @@
|
||||
|
||||
const uri = `file:///${hash}.${langToExt(lang)}`
|
||||
|
||||
buildWorkerDefinition()
|
||||
|
||||
export function getCode(): string {
|
||||
return editor?.getValue() ?? ''
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
import type { AppViewerContext } from './apps/types'
|
||||
import { writable } from 'svelte/store'
|
||||
// import '@codingame/monaco-vscode-standalone-languages'
|
||||
import '@codingame/monaco-vscode-standalone-typescript-language-features'
|
||||
|
||||
// import '@codingame/monaco-vscode-standalone-typescript-language-features'
|
||||
|
||||
import { initializeVscode } from './vscode'
|
||||
import EditorTheme from './EditorTheme.svelte'
|
||||
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
|
||||
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
|
||||
|
||||
export const conf = {
|
||||
@@ -386,8 +386,6 @@
|
||||
|
||||
const uri = `file:///${hash}.ts`
|
||||
|
||||
buildWorkerDefinition()
|
||||
|
||||
export function insertAtCursor(code: string): void {
|
||||
if (editor) {
|
||||
editor.trigger('keyboard', 'type', { text: code })
|
||||
|
||||
@@ -367,28 +367,26 @@
|
||||
outerDivClass="text-secondary !bg-surface-disabled !border-0"
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:selected={selectedPriorityTags}
|
||||
on:change={(e) => {
|
||||
if (e.detail.type === 'add') {
|
||||
onchange={(e) => {
|
||||
if (e.type === 'add') {
|
||||
if (nconfig.priority_tags) {
|
||||
if (e.detail.option && typeof e.detail.option !== 'object') {
|
||||
nconfig.priority_tags[e.detail.option] = 100
|
||||
if (e.option && typeof e.option !== 'object') {
|
||||
nconfig.priority_tags[e.option] = 100
|
||||
}
|
||||
}
|
||||
dirty = true
|
||||
} else if (e.detail.type === 'remove') {
|
||||
} else if (e.type === 'remove') {
|
||||
if (nconfig.priority_tags) {
|
||||
if (e.detail.option && typeof e.detail.option !== 'object') {
|
||||
delete nconfig.priority_tags[e.detail.option]
|
||||
if (e.option && typeof e.option !== 'object') {
|
||||
delete nconfig.priority_tags[e.option]
|
||||
}
|
||||
}
|
||||
dirty = true
|
||||
} else if (e.detail.type === 'removeAll') {
|
||||
nconfig.priority_tags = undefined
|
||||
} else if (e.type === 'removeAll') {
|
||||
nconfig.priority_tags = new Map<string, number>()
|
||||
dirty = true
|
||||
} else {
|
||||
console.error(
|
||||
`Priority tags multiselect - unknown event type: '${e.detail.type}'`
|
||||
)
|
||||
console.error(`Priority tags multiselect - unknown event type: '${e.type}'`)
|
||||
}
|
||||
}}
|
||||
options={nconfig?.worker_tags}
|
||||
|
||||
@@ -157,34 +157,35 @@
|
||||
options={Array.isArray(items) ? items : []}
|
||||
placeholder={resolvedConfig.placeholder}
|
||||
allowUserOptions={resolvedConfig.create}
|
||||
on:change={(event) => {
|
||||
if (event?.detail?.type === 'removeAll') {
|
||||
onchange={(event) => {
|
||||
if (event?.type === 'removeAll') {
|
||||
outputs?.result.set([])
|
||||
} else {
|
||||
outputs?.result.set([...(value ?? [])])
|
||||
}
|
||||
}}
|
||||
on:open={() => {
|
||||
onopen={() => {
|
||||
$selectedComponent = [id]
|
||||
open = true
|
||||
}}
|
||||
on:close={() => {
|
||||
onclose={() => {
|
||||
open = false
|
||||
}}
|
||||
let:option
|
||||
>
|
||||
<!-- needed because portal doesn't work for mouseup event en mobile -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="w-full"
|
||||
on:mouseup|stopPropagation
|
||||
on:pointerdown|stopPropagation={(e) => {
|
||||
let newe = new MouseEvent('mouseup')
|
||||
e.target?.['parentElement']?.dispatchEvent(newe)
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
{#snippet children({ option })}
|
||||
<!-- needed because portal doesn't work for mouseup event en mobile -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="w-full"
|
||||
on:mouseup|stopPropagation
|
||||
on:pointerdown|stopPropagation={(e) => {
|
||||
let newe = new MouseEvent('mouseup')
|
||||
e.target?.['parentElement']?.dispatchEvent(newe)
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
{/snippet}
|
||||
</MultiSelect>
|
||||
<Portal name="app-multiselect">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
|
||||
@@ -18,13 +18,16 @@
|
||||
BoxesIcon,
|
||||
CalendarIcon,
|
||||
Code2Icon,
|
||||
Database,
|
||||
DollarSignIcon,
|
||||
HomeIcon,
|
||||
LayoutDashboardIcon,
|
||||
Loader2,
|
||||
PlayIcon,
|
||||
Route,
|
||||
Search,
|
||||
SearchCode
|
||||
SearchCode,
|
||||
Unplug
|
||||
} from 'lucide-svelte'
|
||||
import JobPreview from '../runs/JobPreview.svelte'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
@@ -33,13 +36,14 @@
|
||||
import ContentSearchInner from '../ContentSearchInner.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import QuickMenuItem from '../search/QuickMenuItem.svelte'
|
||||
import { devopsRole, enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { devopsRole, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import uFuzzy from '@leeoniya/ufuzzy'
|
||||
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
||||
import { scroll_into_view_if_needed_polyfill } from '../multiselect/utils'
|
||||
import { Alert } from '../common'
|
||||
import Popover from '../Popover.svelte'
|
||||
import Logs from 'lucide-svelte/icons/logs'
|
||||
import { AwsIcon, GoogleCloudIcon, KafkaIcon, MqttIcon, NatsIcon } from '../icons'
|
||||
|
||||
let open: boolean = false
|
||||
|
||||
@@ -63,43 +67,122 @@
|
||||
action: () => void
|
||||
icon?: any
|
||||
shortcutKey?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
let switchModeItems: quickMenuItem[] = [
|
||||
{
|
||||
search_id: 'switchto:run-search',
|
||||
label: 'Search across completed runs',
|
||||
label: 'Search across completed runs' + ($enterpriseLicense ? '' : ' (EE)'),
|
||||
action: () => switchMode('runs'),
|
||||
shortcutKey: RUNS_PREFIX,
|
||||
icon: Search
|
||||
icon: Search,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
search_id: 'switchto:content-search',
|
||||
label: 'Search scripts/flows/apps based on content',
|
||||
action: () => switchMode('content'),
|
||||
shortcutKey: CONTENT_SEARCH_PREFIX,
|
||||
icon: SearchCode
|
||||
icon: SearchCode,
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
|
||||
// These items are searchable but do not appear initially on the menu.
|
||||
let hiddenMenuItems = [
|
||||
{
|
||||
search_id: 'nav:http_routes',
|
||||
label: 'Go to HTTP routes',
|
||||
action: () => gotoPage('/routes'),
|
||||
icon: Route,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:web_sockets',
|
||||
label: 'Go to WebSockets',
|
||||
action: () => gotoPage('/websocket_triggers'),
|
||||
icon: Unplug,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:postgres_triggers',
|
||||
label: 'Go to Postgres triggers',
|
||||
action: () => gotoPage('/postgres_triggers'),
|
||||
icon: Database,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:kafka_triggers',
|
||||
label: 'Go to Kafka triggers' + ($enterpriseLicense ? '' : ' (EE)'),
|
||||
action: () => gotoPage('/kafka_triggers'),
|
||||
icon: KafkaIcon,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:nats_triggers',
|
||||
label: 'Go to NATS triggers' + ($enterpriseLicense ? '' : ' (EE)'),
|
||||
action: () => gotoPage('/nats_triggers'),
|
||||
icon: NatsIcon,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:sqs_triggers',
|
||||
label: 'Go to SQS triggers' + ($enterpriseLicense ? '' : ' (EE)'),
|
||||
action: () => gotoPage('/sqs_triggers'),
|
||||
icon: AwsIcon,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:gcp_pub_sub',
|
||||
label: 'Go to GCP Pub/Sub' + ($enterpriseLicense ? '' : ' (EE)'),
|
||||
action: () => gotoPage('/gcp_triggers'),
|
||||
icon: GoogleCloudIcon,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:mqtt_triggers',
|
||||
label: 'Go to MQTT triggers',
|
||||
action: () => gotoPage('/mqtt_triggers'),
|
||||
icon: MqttIcon,
|
||||
disabled: $userStore?.operator
|
||||
}
|
||||
]
|
||||
|
||||
let defaultMenuItems: quickMenuItem[] = [
|
||||
{ search_id: 'nav:home', label: 'Go to Home', action: () => gotoPage('/'), icon: HomeIcon },
|
||||
{ search_id: 'nav:runs', label: 'Go to Runs', action: () => gotoPage('/runs'), icon: PlayIcon },
|
||||
{
|
||||
search_id: 'nav:home',
|
||||
label: 'Go to Home',
|
||||
action: () => gotoPage('/'),
|
||||
icon: HomeIcon,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
search_id: 'nav:runs',
|
||||
label: 'Go to Runs',
|
||||
action: () => gotoPage('/runs'),
|
||||
icon: PlayIcon,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
search_id: 'nav:variables',
|
||||
label: 'Go to Variables',
|
||||
action: () => gotoPage('/variables'),
|
||||
icon: DollarSignIcon
|
||||
icon: DollarSignIcon,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
search_id: 'nav:resources',
|
||||
label: 'Go to Resources',
|
||||
action: () => gotoPage('/resources'),
|
||||
icon: BoxesIcon
|
||||
icon: BoxesIcon,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
search_id: 'nav:schedules',
|
||||
search_id: 'nav:schedules_triggers',
|
||||
label: 'Go to Schedules',
|
||||
action: () => gotoPage('/schedules'),
|
||||
icon: CalendarIcon
|
||||
icon: CalendarIcon,
|
||||
disabled: false
|
||||
},
|
||||
...switchModeItems,
|
||||
{
|
||||
@@ -107,10 +190,13 @@
|
||||
label: 'Explore windmill service logs',
|
||||
action: () => gotoPage('/service_logs'),
|
||||
shortcutKey: LOGS_PREFIX,
|
||||
icon: Logs
|
||||
icon: Logs,
|
||||
disabled: !$devopsRole
|
||||
}
|
||||
]
|
||||
|
||||
let defaultMenuItemsWithHidden = [...defaultMenuItems, ...hiddenMenuItems]
|
||||
|
||||
let itemMap = {
|
||||
default: defaultMenuItems as any[],
|
||||
'switch-mode': switchModeItems,
|
||||
@@ -152,6 +238,7 @@
|
||||
|
||||
let uf = new uFuzzy(opts)
|
||||
let defaultMenuItemLabels = defaultMenuItems.map((item) => item.label)
|
||||
let defaultMenuItemAndHiddenLabels = defaultMenuItemsWithHidden.map((item) => item.label)
|
||||
let switchModeItemLabels = switchModeItems.map((item) => item.label)
|
||||
|
||||
function fuzzyFilter(filter: string, items: any[], itemsPlainText: string[]) {
|
||||
@@ -210,7 +297,14 @@
|
||||
}
|
||||
|
||||
if (tab === 'default') {
|
||||
itemMap['default'] = fuzzyFilter(searchTerm, defaultMenuItems, defaultMenuItemLabels)
|
||||
if (searchTerm === '')
|
||||
itemMap['default'] = fuzzyFilter(searchTerm, defaultMenuItems, defaultMenuItemLabels)
|
||||
else
|
||||
itemMap['default'] = fuzzyFilter(
|
||||
searchTerm,
|
||||
defaultMenuItemsWithHidden,
|
||||
defaultMenuItemAndHiddenLabels
|
||||
)
|
||||
if (combinedItems) {
|
||||
itemMap['default'] = itemMap['default'].concat(
|
||||
fuzzyFilter(
|
||||
@@ -552,20 +646,24 @@
|
||||
</div>
|
||||
<div class="overflow-y-auto relative {maxModalHeight(tab)}">
|
||||
{#if tab === 'default' || tab === 'switch-mode'}
|
||||
{@const items = (itemMap[tab] ?? []).filter((e) => defaultMenuItems.includes(e))}
|
||||
{@const items = (itemMap[tab] ?? []).filter((e) =>
|
||||
defaultMenuItemsWithHidden.includes(e)
|
||||
)}
|
||||
{#if items.length > 0}
|
||||
<div class={tab === 'switch-mode' ? 'p-2' : 'p-2 border-b'}>
|
||||
{#each items as el}
|
||||
<QuickMenuItem
|
||||
on:select={el?.action}
|
||||
on:hover={() => (selectedItem = el)}
|
||||
id={el?.search_id}
|
||||
hovered={el?.search_id === selectedItem?.search_id}
|
||||
label={el?.label}
|
||||
icon={el?.icon}
|
||||
shortcutKey={el?.shortcutKey}
|
||||
bind:mouseMoved
|
||||
/>
|
||||
{#if !el.disabled}
|
||||
<QuickMenuItem
|
||||
on:select={el?.action}
|
||||
on:hover={() => (selectedItem = el)}
|
||||
id={el?.search_id}
|
||||
hovered={el?.search_id === selectedItem?.search_id}
|
||||
label={el?.label}
|
||||
icon={el?.icon}
|
||||
shortcutKey={el?.shortcutKey}
|
||||
bind:mouseMoved
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -718,7 +816,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col w-full justify-center items-center h-48">
|
||||
<div class="flex flex-col h-full w-full justify-center items-center h-48">
|
||||
<div class="text-tertiary text-center">
|
||||
{#if searchTerm === RUNS_PREFIX}
|
||||
<div class="text-2xl font-bold">Enter your search terms</div>
|
||||
|
||||
@@ -186,9 +186,9 @@
|
||||
selected === 'resource'
|
||||
? !!args.kafka_resource_path
|
||||
: isStaticConnectionValid &&
|
||||
args.brokers &&
|
||||
args.brokers.length > 0 &&
|
||||
args.brokers.every((b) => b.length > 0)
|
||||
args.brokers &&
|
||||
args.brokers.length > 0 &&
|
||||
args.brokers.every((b) => b.length > 0)
|
||||
|
||||
$: isValid =
|
||||
isConnectionValid &&
|
||||
@@ -260,7 +260,15 @@
|
||||
{:else}
|
||||
<SchemaForm
|
||||
schema={connnectionSchema}
|
||||
bind:args
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:isValid={isStaticConnectionValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
@@ -280,7 +288,15 @@
|
||||
<Subsection headless={true}>
|
||||
<SchemaForm
|
||||
schema={argsSchema}
|
||||
bind:args
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:isValid={otherArgsValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
|
||||
@@ -186,11 +186,11 @@
|
||||
selected === 'resource'
|
||||
? !!args.nats_resource_path
|
||||
: isStaticConnectionValid &&
|
||||
args.servers &&
|
||||
args.servers.length > 0 &&
|
||||
args.servers.every((b) => b.length > 0) &&
|
||||
args.require_tls !== undefined &&
|
||||
args.require_tls !== null
|
||||
args.servers &&
|
||||
args.servers.length > 0 &&
|
||||
args.servers.every((b) => b.length > 0) &&
|
||||
args.require_tls !== undefined &&
|
||||
args.require_tls !== null
|
||||
|
||||
$: isValid =
|
||||
isConnectionValid &&
|
||||
@@ -268,7 +268,15 @@
|
||||
{:else}
|
||||
<SchemaForm
|
||||
schema={connnectionSchema}
|
||||
bind:args
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:isValid={isStaticConnectionValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
@@ -283,7 +291,15 @@
|
||||
<Subsection headless={true}>
|
||||
<SchemaForm
|
||||
schema={argsSchema}
|
||||
bind:args
|
||||
bind:args={
|
||||
() => args,
|
||||
(v) => {
|
||||
args = {
|
||||
...args,
|
||||
...v
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:isValid={otherArgsValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
|
||||
@@ -128,19 +128,19 @@
|
||||
selected={table_to_track.columns_name ?? []}
|
||||
placeholder="Select columns"
|
||||
--sms-options-margin="4px"
|
||||
on:change={(e) => {
|
||||
const option = e.detail.option?.toString()
|
||||
if (e.detail.type === 'add') {
|
||||
onchange={(e) => {
|
||||
const option = e.option?.toString()
|
||||
if (e.type === 'add') {
|
||||
option && table_to_track.columns_name?.push(option)
|
||||
} else if (e.detail.type === 'remove') {
|
||||
} else if (e.type === 'remove') {
|
||||
table_to_track.columns_name = table_to_track.columns_name?.filter(
|
||||
(column) => column !== option
|
||||
)
|
||||
} else if (e.detail.type === 'removeAll') {
|
||||
} else if (e.type === 'removeAll') {
|
||||
table_to_track.columns_name = []
|
||||
} else {
|
||||
console.error(
|
||||
`Priority tags multiselect - unknown event type: '${e.detail.type}'`
|
||||
`Priority tags multiselect - unknown event type: '${e.type}'`
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -9,6 +9,114 @@ import { editor as meditor } from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
export let isInitialized = false
|
||||
export let isInitializing = false
|
||||
|
||||
import { getEnhancedMonacoEnvironment } from 'monaco-languageclient/vscode/services'
|
||||
|
||||
export function buildWorkerDefinition() {
|
||||
const envEnhanced = getEnhancedMonacoEnvironment()
|
||||
|
||||
const getWorker = (moduleId: string, label: string) => {
|
||||
console.log(`getWorker: moduleId: ${moduleId} label: ${label}`)
|
||||
|
||||
let selector = label
|
||||
|
||||
// const defaultTextEditorWorker = () => new Worker(
|
||||
// new URL('@codingame/monaco-vscode-editor-api/esm/vs/editor/editor.worker.js', import.meta.url),
|
||||
// { type: 'module' }
|
||||
// );
|
||||
// const defaultTextMateWorker = () => new Worker(
|
||||
// new URL('@codingame/monaco-vscode-textmate-service-override/worker', import.meta.url),
|
||||
// { type: 'module' }
|
||||
// );
|
||||
|
||||
let workerLoaders = {
|
||||
TextEditorWorker: () => {
|
||||
return new Worker(
|
||||
new URL(
|
||||
'@codingame/monaco-vscode-editor-api/esm/vs/editor/editor.worker.js',
|
||||
import.meta.url
|
||||
),
|
||||
{
|
||||
type: 'module'
|
||||
}
|
||||
)
|
||||
},
|
||||
// javascript: () => {
|
||||
// return new Worker(new URL('monaco-editor-wrapper/workers/module/ts', import.meta.url), {
|
||||
// type: 'module'
|
||||
// })
|
||||
// },
|
||||
javascript: () => {
|
||||
return new Worker(
|
||||
new URL(
|
||||
'@codingame/monaco-vscode-standalone-typescript-language-features/worker',
|
||||
import.meta.url
|
||||
),
|
||||
{
|
||||
type: 'module'
|
||||
}
|
||||
)
|
||||
},
|
||||
typescript: () => {
|
||||
return new Worker(
|
||||
new URL(
|
||||
'@codingame/monaco-vscode-standalone-typescript-language-features/worker',
|
||||
import.meta.url
|
||||
),
|
||||
{
|
||||
type: 'module'
|
||||
}
|
||||
)
|
||||
},
|
||||
json: () => {
|
||||
return new Worker(
|
||||
new URL(
|
||||
'@codingame/monaco-vscode-standalone-json-language-features/worker',
|
||||
import.meta.url
|
||||
),
|
||||
{
|
||||
type: 'module'
|
||||
}
|
||||
)
|
||||
},
|
||||
html: () => {
|
||||
return new Worker(
|
||||
new URL(
|
||||
'@codingame/monaco-vscode-standalone-html-language-features/worker',
|
||||
import.meta.url
|
||||
),
|
||||
{
|
||||
type: 'module'
|
||||
}
|
||||
)
|
||||
},
|
||||
css: () => {
|
||||
return new Worker(
|
||||
new URL(
|
||||
'@codingame/monaco-vscode-standalone-css-language-features/worker',
|
||||
import.meta.url
|
||||
),
|
||||
{
|
||||
type: 'module'
|
||||
}
|
||||
)
|
||||
},
|
||||
graphql: () => {
|
||||
console.log('Creating graphql worker')
|
||||
return new Worker(new URL(`../monaco_workers/graphql.worker.bundle.js`, import.meta.url), {
|
||||
name: 'graphql'
|
||||
})
|
||||
}
|
||||
}
|
||||
const workerFunc = workerLoaders[selector]
|
||||
if (workerFunc !== undefined) {
|
||||
return workerFunc()
|
||||
} else {
|
||||
throw new Error(`Unimplemented worker ${label} (${moduleId})`)
|
||||
}
|
||||
}
|
||||
envEnhanced.getWorker = getWorker
|
||||
}
|
||||
|
||||
export async function initializeVscode(caller?: string, htmlContainer?: HTMLElement) {
|
||||
if (!isInitialized && !isInitializing) {
|
||||
console.log(`Initializing vscode-api from ${caller ?? 'unknown'}`)
|
||||
@@ -16,20 +124,25 @@ export async function initializeVscode(caller?: string, htmlContainer?: HTMLElem
|
||||
|
||||
try {
|
||||
// init vscode-api
|
||||
await initServices({
|
||||
serviceOverrides: {
|
||||
// ...getThemeServiceOverride(),
|
||||
// ...getTextmateServiceOverride()
|
||||
...getConfigurationServiceOverride(),
|
||||
...getMonarchServiceOverride()
|
||||
await initServices(
|
||||
{
|
||||
serviceOverrides: {
|
||||
// ...getThemeServiceOverride(),
|
||||
// ...getTextmateServiceOverride()
|
||||
...getConfigurationServiceOverride(),
|
||||
...getMonarchServiceOverride()
|
||||
},
|
||||
enableExtHostWorker: false,
|
||||
userConfiguration: {
|
||||
json: JSON.stringify({
|
||||
'editor.experimental.asyncTokenization': true
|
||||
})
|
||||
}
|
||||
},
|
||||
enableExtHostWorker: false,
|
||||
userConfiguration: {
|
||||
json: JSON.stringify({
|
||||
'editor.experimental.asyncTokenization': true
|
||||
})
|
||||
{
|
||||
monacoWorkerFactory: buildWorkerDefinition
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
isInitialized = true
|
||||
meditor.defineTheme('nord', {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { languages } from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
import { ShowLightbulbIconMode } from 'vscode/vscode/vs/editor/common/config/editorOptions'
|
||||
|
||||
import { editor as meditor } from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
|
||||
export function editorConfig(
|
||||
code: string,
|
||||
lang: string,
|
||||
@@ -22,7 +24,7 @@ export function editorConfig(
|
||||
enabled: false
|
||||
},
|
||||
lightbulb: {
|
||||
enabled: ShowLightbulbIconMode.On
|
||||
enabled: meditor.ShowLightbulbIconMode.On
|
||||
},
|
||||
suggest: {
|
||||
showKeywords: true
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"gitSync_4": "hub/11580/sync-script-to-git-repo-windmill",
|
||||
"gitSync_5": "hub/11641/sync-script-to-git-repo-windmill",
|
||||
"gitSync_6": "hub/11666/sync-script-to-git-repo-windmill",
|
||||
"gitSync": "hub/11668/sync-script-to-git-repo-windmill",
|
||||
"gitSync_7": "hub/11668/sync-script-to-git-repo-windmill",
|
||||
"gitSync": "hub/19673/sync-script-to-git-repo-windmill",
|
||||
"gitSyncTest_0": "hub/9073/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest_1": "hub/11499/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest_2": "hub/11667/git-repo-test-read-write-windmill",
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { initEnhancedMonacoEnvironment } from 'monaco-languageclient/vscode/services'
|
||||
|
||||
// import cssWorker from 'monaco-editor-wrapper/workers/module/css?worker&url'
|
||||
// import htmlWorker from 'monaco-editor-wrapper/workers/module/html?worker&url'
|
||||
// import jsonWorker from 'monaco-editor-wrapper/workers/module/json?worker&url'
|
||||
// import editorWorker from 'monaco-editor-wrapper/workers/module/editor?worker&url'
|
||||
|
||||
export function buildWorkerDefinition() {
|
||||
const envEnhanced = initEnhancedMonacoEnvironment()
|
||||
|
||||
const getWorker = (moduleId: string, label: string) => {
|
||||
console.log(`getWorker: moduleId: ${moduleId} label: ${label}`)
|
||||
|
||||
let selector = label
|
||||
let workerLoaders = {
|
||||
editorWorkerService: () => {
|
||||
return new Worker(new URL('monaco-editor-wrapper/workers/module/editor', import.meta.url), {
|
||||
type: 'module'
|
||||
})
|
||||
},
|
||||
javascript: () => {
|
||||
return new Worker(new URL('monaco-editor-wrapper/workers/module/ts', import.meta.url), {
|
||||
type: 'module'
|
||||
})
|
||||
},
|
||||
typescript: () => {
|
||||
return new Worker(new URL('monaco-editor-wrapper/workers/module/ts', import.meta.url), {
|
||||
type: 'module'
|
||||
})
|
||||
},
|
||||
json: () => {
|
||||
return new Worker(new URL('monaco-editor-wrapper/workers/module/json', import.meta.url), {
|
||||
type: 'module'
|
||||
})
|
||||
},
|
||||
html: () => {
|
||||
return new Worker(new URL('monaco-editor-wrapper/workers/module/html', import.meta.url), {
|
||||
type: 'module'
|
||||
})
|
||||
},
|
||||
css: () => {
|
||||
return new Worker(new URL('monaco-editor-wrapper/workers/module/css', import.meta.url), {
|
||||
type: 'module'
|
||||
})
|
||||
},
|
||||
graphql: () => {
|
||||
console.log('Creating graphql worker')
|
||||
return new Worker(new URL(`./graphql.worker.bundle.js`, import.meta.url), {
|
||||
name: 'graphql'
|
||||
})
|
||||
}
|
||||
}
|
||||
const workerFunc = workerLoaders[selector]
|
||||
if (workerFunc !== undefined) {
|
||||
return workerFunc()
|
||||
} else {
|
||||
throw new Error(`Unimplemented worker ${label} (${moduleId})`)
|
||||
}
|
||||
}
|
||||
envEnhanced.getWorker = getWorker
|
||||
}
|
||||
@@ -73,6 +73,7 @@
|
||||
schedules: boolean
|
||||
users: boolean
|
||||
groups: boolean
|
||||
triggers: boolean
|
||||
}
|
||||
type GitSyncType =
|
||||
| 'script'
|
||||
@@ -86,7 +87,7 @@
|
||||
| 'schedule'
|
||||
| 'user'
|
||||
| 'group'
|
||||
|
||||
| 'trigger'
|
||||
let slackInitialPath: string
|
||||
let slackScriptPath: string
|
||||
let teamsInitialPath: string
|
||||
@@ -290,6 +291,9 @@
|
||||
if (typesMap.groups == expectedValue) {
|
||||
result.push('group')
|
||||
}
|
||||
if (typesMap.triggers == expectedValue) {
|
||||
result.push('trigger')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -429,7 +433,8 @@
|
||||
schedules: (settings.exclude_types_override?.indexOf('schedule') ?? -1) >= 0,
|
||||
folders: (settings.exclude_types_override?.indexOf('folder') ?? -1) >= 0,
|
||||
users: (settings.exclude_types_override?.indexOf('user') ?? -1) >= 0,
|
||||
groups: (settings.exclude_types_override?.indexOf('group') ?? -1) >= 0
|
||||
groups: (settings.exclude_types_override?.indexOf('group') ?? -1) >= 0,
|
||||
triggers: (settings.exclude_types_override?.indexOf('trigger') ?? -1) >= 0
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -444,7 +449,8 @@
|
||||
schedules: (settings.git_sync.include_type?.indexOf('schedule') ?? -1) >= 0,
|
||||
folders: (settings.git_sync.include_type?.indexOf('folder') ?? -1) >= 0,
|
||||
users: (settings.git_sync.include_type?.indexOf('user') ?? -1) >= 0,
|
||||
groups: (settings.git_sync.include_type?.indexOf('group') ?? -1) >= 0
|
||||
groups: (settings.git_sync.include_type?.indexOf('group') ?? -1) >= 0,
|
||||
triggers: (settings.git_sync.include_type?.indexOf('trigger') ?? -1) >= 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -462,7 +468,8 @@
|
||||
secrets: false,
|
||||
schedules: false,
|
||||
users: false,
|
||||
groups: false
|
||||
groups: false,
|
||||
triggers: false
|
||||
}
|
||||
}
|
||||
gitSyncTestJobs = []
|
||||
@@ -1163,6 +1170,11 @@
|
||||
on:change={(_) => resetGitSyncRepositoryExclude('groups')}
|
||||
options={{ right: 'Groups' }}
|
||||
/>
|
||||
<Toggle
|
||||
bind:checked={gitSyncSettings.include_type.triggers}
|
||||
on:change={(_) => resetGitSyncRepositoryExclude('triggers')}
|
||||
options={{ right: 'Triggers' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1354,6 +1366,13 @@
|
||||
options={{ right: 'Exclude resource types' }}
|
||||
/>
|
||||
{/if}
|
||||
{#if gitSyncSettings.include_type.triggers}
|
||||
<Toggle
|
||||
color="red"
|
||||
bind:checked={gitSyncRepository.exclude_types_override.triggers}
|
||||
options={{ right: 'Exclude triggers' }}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -1384,7 +1403,8 @@
|
||||
secrets: false,
|
||||
schedules: false,
|
||||
users: false,
|
||||
groups: false
|
||||
groups: false,
|
||||
triggers: false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -48,7 +48,7 @@ const config = {
|
||||
__pkg__: version
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['highlight.js', 'highlight.js/lib/core', 'monaco-vim'],
|
||||
include: ['highlight.js', 'highlight.js/lib/core', 'monaco-vim', 'monaco-editor-wrapper'],
|
||||
exclude: [
|
||||
'@codingame/monaco-vscode-standalone-typescript-language-features',
|
||||
'@codingame/monaco-vscode-standalone-languages'
|
||||
@@ -64,7 +64,7 @@ const config = {
|
||||
alias: {
|
||||
path: 'path-browserify',
|
||||
'monaco-editor/esm/vs/editor/contrib/hover/browser/hover':
|
||||
'vscode/vscode/vs/editor/contrib/hover/browser/hoverContribution'
|
||||
'monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution'
|
||||
},
|
||||
dedupe: ['vscode', 'monaco-editor']
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user