diff --git a/.claude/hooks/guard-main-branch.sh b/.claude/hooks/guard-main-branch.sh new file mode 100755 index 0000000000..c7eeea9475 --- /dev/null +++ b/.claude/hooks/guard-main-branch.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# PreToolUse hook: block destructive git operations when on the main branch. +# Non-git tool calls and read-only git commands pass through silently. + +set -euo pipefail + +input="$(cat)" +tool_name="$(echo "$input" | jq -r '.tool_name // empty')" + +# Only care about Bash tool calls +[[ "$tool_name" == "Bash" ]] || exit 0 + +command="$(echo "$input" | jq -r '.tool_input.command // empty')" + +# Only care about git write commands +if [[ "$command" =~ ^git\ (push|reset|revert|checkout|merge|rebase|commit|add) ]]; then + branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + if [[ "$branch" == "main" ]]; then + echo "BLOCK: You are on the main branch. Create or switch to a feature branch first." + fi +fi diff --git a/.claude/settings.json b/.claude/settings.json index e9b7444a1b..fcd49c3140 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -30,7 +30,15 @@ "Bash(cargo check:*)", "mcp__ide__getDiagnostics", "Bash(npm run generate-backend-client:*)", - "Bash(npm run check:*)" + "Bash(npm run check:*)", + "Bash(git push:*)", + "Bash(git reset:*)", + "Bash(git revert:*)", + "Bash(git checkout:*)", + "Bash(git merge:*)", + "Bash(git rebase:*)", + "Bash(git add:*)", + "Bash(git commit:*)" ], "deny": [ "Read(.env)", @@ -55,17 +63,23 @@ "Bash(chown:*)", "Bash(truncate:*)", "Bash(shred:*)", - "Bash(unlink:*)", - "Bash(git push:*)", - "Bash(git reset:*)", - "Bash(git revert:*)", - "Bash(git checkout:*)", - "Bash(git merge:*)", - "Bash(git rebase:*)" + "Bash(unlink:*)" ] }, "enableAllProjectMcpServers": true, "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-main-branch.sh", + "timeout": 5 + } + ] + } + ], "PostToolUse": [ { "matcher": "Edit|Write", diff --git a/.claude/skills/refine/SKILL.md b/.claude/skills/refine/SKILL.md new file mode 100644 index 0000000000..aaf747cd29 --- /dev/null +++ b/.claude/skills/refine/SKILL.md @@ -0,0 +1,39 @@ +--- +name: refine +user_invocable: true +description: End-of-session reflection. Reviews friction encountered during the session and proposes updates to docs/ to capture lessons learned. +--- + +# Refine Skill + +Reflect on the current session and update documentation with lessons learned. + +## Instructions + +1. **Identify friction**: Review what happened in this session: + - Run `git diff main...HEAD --stat` to see what files were touched + - Think about: what was slow, what failed, what required multiple attempts, what information was missing or hard to find + +2. **Read current docs**: Read the docs that were relevant to this session: + - `docs/validation.md` + - `docs/enterprise.md` + - `docs/autonomous-mode.md` + - Any skills that were invoked + +3. **Propose updates**: For each piece of friction, decide if it warrants a doc update: + - **Missing knowledge**: Information you had to discover that should be documented + - **Wrong guidance**: Instructions that led you astray + - **Missing validation rule**: A check that should be in the validation matrix + - **New pattern**: A codebase pattern worth capturing for next time + +4. **Apply updates**: Edit the relevant `docs/` files. Keep changes minimal and specific — add only what would have saved time this session. + +5. **Report**: Summarize what was added/changed and why. + +## Rules + +- Only add knowledge confirmed by this session — no speculative additions +- Keep docs concise — add a line or two, not a paragraph +- If a whole new doc is needed, create it in `docs/` and add a pointer in `CLAUDE.md` +- Don't update skills unless a coding pattern was genuinely wrong +- Don't add things Claude already knows — only Windmill-specific knowledge diff --git a/.claude/skills/rust-backend/SKILL.md b/.claude/skills/rust-backend/SKILL.md index c2253319a6..f0c52002bc 100644 --- a/.claude/skills/rust-backend/SKILL.md +++ b/.claude/skills/rust-backend/SKILL.md @@ -3,493 +3,105 @@ name: rust-backend description: Rust coding guidelines for the Windmill backend. MUST use when writing or modifying Rust code in the backend directory. --- -# Rust Backend Coding Guidelines +# Windmill Rust Patterns -Apply these patterns when writing or modifying Rust code in the `backend/` directory. - -## Data Structure Design - -Choose between `struct`, `enum`, or `newtype` based on domain needs: - -- Use `enum` for state machines instead of boolean flags or loosely related fields -- Model invariants explicitly using types (e.g., `NonZeroU32`, `Duration`, custom enums) -- Consider ownership of each field: - - Use `&str` vs `String`, slices vs vectors - - Use `Arc` when sharing across threads - - Use `Cow<'a, T>` for flexible ownership - -```rust -// State machine with enum -enum JobState { - Pending { scheduled_for: DateTime }, - Running { started_at: DateTime, worker: String }, - Completed { result: JobResult, duration_ms: i64 }, - Failed { error: String, retries: u32 }, -} - -// Avoid multiple booleans -struct Job { - is_pending: bool, // Don't do this - is_running: bool, - is_completed: bool, -} -``` - -## Impl Block Organization - -Place `impl` blocks immediately below the struct/enum they modify. Group methods logically: - -```rust -struct JobQueue { - jobs: Vec, - capacity: usize, -} - -impl JobQueue { - // Constructors first - pub fn new(capacity: usize) -> Self { ... } - pub fn with_jobs(jobs: Vec) -> Self { ... } - - // Getters - pub fn len(&self) -> usize { ... } - pub fn is_empty(&self) -> bool { ... } - - // Mutation methods - pub fn push(&mut self, job: Job) -> Result<()> { ... } - pub fn pop(&mut self) -> Option { ... } - - // Domain logic - pub fn next_scheduled(&self) -> Option<&Job> { ... } -} -``` - -## Iterator Chains Over For-Loops - -Prefer functional iterator chains (`.filter().map().collect()`) over imperative for-loops: - -```rust -// Preferred -let results: Vec<_> = items - .iter() - .filter(|item| item.is_valid()) - .map(|item| item.transform()) - .collect(); - -// Avoid -let mut results = Vec::new(); -for item in items.iter() { - if item.is_valid() { - results.push(item.transform()); - } -} -``` +Apply these Windmill-specific patterns when writing Rust code in `backend/`. ## Error Handling -Use the `Error` type from `windmill_common::error`. Return `Result` or `JsonResult` for fallible functions: +Use `Error` from `windmill_common::error`. Return `Result` or `JsonResult`: ```rust use windmill_common::error::{Error, Result}; -// Use ? operator for propagation pub async fn get_job(db: &DB, id: Uuid) -> Result { - let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id) + sqlx::query_as!(Job, "SELECT id, workspace_id FROM v2_job WHERE id = $1", id) .fetch_optional(db) .await? .ok_or_else(|| Error::NotFound("job not found".to_string()))?; - Ok(job) } ``` -Prefer `if let` for optional handling. Use `let...else` when early return makes code clearer: +Never panic in library code. Reserve `.unwrap()` for compile-time guarantees. + +## SQLx Patterns + +**Never use `SELECT *`** — always list columns explicitly. Critical for backwards compatibility when workers lag behind API version: ```rust -let Some(config) = get_config() else { - return Err(Error::MissingConfig); -}; +// Correct +sqlx::query_as!(Job, "SELECT id, workspace_id, path FROM v2_job WHERE id = $1", id) + +// Wrong — breaks when columns are added +sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", id) ``` -Never panic in library code. Reserve `.unwrap()` for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity. - -## Early Returns - -Return early to avoid deep nesting. Handle error cases and edge conditions first: +Use batch operations to avoid N+1: ```rust -// Preferred - early returns -fn process_job(job: Option) -> Result { - let Some(job) = job else { - return Ok(Output::default()); - }; - - if !job.is_valid() { - return Err(Error::InvalidJob); - } - - if job.is_cached() { - return Ok(job.cached_result()); - } - - // Main logic at the end, not nested - execute_job(job) -} - -// Avoid - deep nesting -fn process_job(job: Option) -> Result { - if let Some(job) = job { - if job.is_valid() { - if !job.is_cached() { - execute_job(job) - } else { - Ok(job.cached_result()) - } - } else { - Err(Error::InvalidJob) - } - } else { - Ok(Output::default()) - } -} +// Preferred — single query with IN clause +sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await? ``` -## Variable Shadowing - -Shadow variables instead of creating new names with prefixes: - -```rust -// Preferred -let data = fetch_raw_data(); -let data = parse(data); -let data = validate(data)?; - -// Avoid -let raw_data = fetch_raw_data(); -let parsed_data = parse(raw_data); -let validated_data = validate(parsed_data)?; -``` - -## Minimal Comments - -- No inline comments explaining obvious code -- No TODO/FIXME comments in committed code -- Doc comments (`///`) only on public items -- Let code be self-documenting through clear naming - -## Type Safety - -Use enums over boolean flags for clarity: - -```rust -// Preferred -enum JobStatus { - Pending, - Running, - Completed, -} - -// Avoid -struct Job { - is_running: bool, - is_completed: bool, -} -``` - -## Pattern Matching - -Prefer explicit matching. Use wildcards strategically for fallback cases or ignored fields: - -```rust -// Explicit matching preferred -match status { - JobStatus::Pending => handle_pending(), - JobStatus::Running => handle_running(), - JobStatus::Completed => handle_completed(), -} - -// Wildcards OK for fallback -match result { - Ok(value) => process(value), - Err(_) => return default_value(), -} - -// Wildcards OK for ignoring fields in destructuring -let Point { x, y, .. } = point; -``` - -## Destructuring in Function Signatures - -Destructure structs directly in function parameters: - -```rust -// Preferred -async fn process_job( - Extension(db): Extension, - Path((workspace, job_id)): Path<(String, Uuid)>, - Query(pagination): Query, -) -> Result> { - // ... -} - -// Avoid -async fn process_job( - db_ext: Extension, - path: Path<(String, Uuid)>, - query: Query, -) -> Result> { - let Extension(db) = db_ext; - let Path((workspace, job_id)) = path; - // ... -} -``` - -## Trait Implementations - -Use standard trait implementations to simplify conversions and reduce boilerplate: - -```rust -// Implement From/Into for type conversions -impl From for ApiJob { - fn from(db: DbJob) -> Self { - ApiJob { - id: db.id, - status: db.status.into(), - } - } -} - -// Use TryFrom for fallible conversions -impl TryFrom for JobKind { - type Error = Error; - fn try_from(s: String) -> Result { ... } -} -``` - -Apply `derive` macros to reduce boilerplate: - -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Job { ... } -``` - -## Module Structure - -- Use `pub(crate)` instead of `pub` when possible; expose only what needs exposing -- Keep APIs small and expressive; avoid leaking internal types -- Organize code into modules reflecting ownership and domain boundaries - -```rust -// Prefer restricted visibility -pub(crate) fn internal_helper() { ... } - -// Only pub for external API -pub fn create_job(...) -> Result { ... } -``` - -## Code Navigation - -Always use rust-analyzer LSP for: -- Go to definition -- Find references -- Type information -- Import resolution - -Do not guess at module paths or type definitions. +Use transactions for multi-step operations. Parameterize all queries. ## JSON Handling -Prefer `Box` over `serde_json::Value` when: -- Storing JSON in the database (JSONB columns) -- Passing JSON through without modification -- The JSON structure doesn't need inspection +Prefer `Box` over `serde_json::Value` when storing/passing JSON without inspection: ```rust -// Preferred - avoids parsing/serialization overhead pub struct Job { - pub id: Uuid, pub args: Option>, } - -// Only use Value when you need to inspect/modify JSON -let value: serde_json::Value = serde_json::from_str(&json)?; -if let Some(field) = value.get("field") { - // modify or inspect -} ``` -## Serde Optimizations +Only use `serde_json::Value` when you need to inspect or modify the JSON. -Use serde attributes to optimize serialization: +## Serde Optimizations ```rust #[derive(Serialize, Deserialize)] pub struct Job { - #[serde(rename = "jobId")] - pub id: Uuid, - - #[serde(default)] - pub priority: i32, - #[serde(skip_serializing_if = "Option::is_none")] pub parent_job: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] pub tags: Vec, + #[serde(default)] + pub priority: i32, } ``` -Prefer borrowing for zero-copy deserialization when lifetimes allow: +## Async & Concurrency + +Never block the async runtime. Use `spawn_blocking` for CPU-intensive work: ```rust -#[derive(Deserialize)] -pub struct JobInput<'a> { - #[serde(borrow)] - pub workspace_id: Cow<'a, str>, - - #[serde(borrow)] - pub script_path: &'a str, -} +let result = tokio::task::spawn_blocking(move || expensive_computation(&data)).await?; ``` -## SQLx Patterns +**Mutex selection**: Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) for data protection. Only use `tokio::sync::Mutex` when holding locks across `.await` points. -**Never use `SELECT *`** - always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version: +Use `tokio::sync::mpsc` (bounded) for channels. Avoid `std::thread::sleep` in async contexts. + +## Module Structure & Visibility + +- Use `pub(crate)` instead of `pub` when possible +- Place new code in the appropriate crate based on functionality +- API endpoints go in `windmill-api/src/` organized by domain +- Shared functionality goes in `windmill-common/src/` + +## Code Navigation + +Always use rust-analyzer LSP for go-to-definition, find-references, and type info. Do not guess at module paths. + +## Axum Handlers + +Destructure extractors directly in function signatures: ```rust -// Preferred - explicit columns -sqlx::query_as!( - Job, - "SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1", - job_id -) - -// Avoid - breaks when columns are added -sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id) +async fn process_job( + Extension(db): Extension, + Path((workspace, job_id)): Path<(String, Uuid)>, + Query(pagination): Query, +) -> Result> { ... } ``` - -Use batch operations to minimize round trips: - -```rust -// Preferred - single query with multiple values -sqlx::query!( - "INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)", - id1, log1, id2, log2 -) - -// Avoid N+1 queries -for id in ids { - sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?; -} - -// Preferred - single query with IN clause -sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await? -``` - -Use transactions for multi-step operations and parameterize all queries. - -## Async & Tokio Patterns - -Never block the async runtime. Use `spawn_blocking` for CPU-intensive or blocking I/O: - -```rust -// Preferred - offload blocking work -let result = tokio::task::spawn_blocking(move || { - expensive_computation(&data) -}).await?; - -// Avoid - blocks the runtime -let result = expensive_computation(&data); // Don't do this in async -``` - -Use tokio primitives for sleep and channels: - -```rust -use tokio::sync::mpsc; -use tokio::time::sleep; - -// Avoid in async contexts -use std::thread::sleep; // Blocks the runtime -``` - -Use bounded channels for backpressure: - -```rust -// Preferred - bounded channel prevents overwhelming -let (tx, rx) = tokio::sync::mpsc::channel(100); - -// Be careful with unbounded -let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); -``` - -## Mutex Selection in Async Code - -**Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) over `tokio::sync::Mutex`** for protecting data in async code. The async mutex is more expensive and only needed when holding locks across `.await` points. - -```rust -// Preferred for data protection - std mutex is faster -use std::sync::Mutex; - -struct Cache { - data: Mutex>, -} - -impl Cache { - fn get(&self, key: &str) -> Option { - self.data.lock().unwrap().get(key).cloned() - } - - fn insert(&self, key: String, value: Value) { - self.data.lock().unwrap().insert(key, value); - } -} -``` - -**Use `tokio::sync::Mutex` only when you must hold the lock across `.await` points**, typically for IO resources like database connections: - -```rust -use tokio::sync::Mutex; -use std::sync::Arc; - -// Async mutex for IO resources held across await points -let conn = Arc::new(Mutex::new(db_connection)); - -async fn execute_query(conn: Arc>, query: &str) { - let mut lock = conn.lock().await; - lock.execute(query).await; // Lock held across .await -} -``` - -**Common pattern**: Wrap `Arc>` in a struct with non-async methods that lock internally, keeping lock scope minimal: - -```rust -struct SharedState { - inner: std::sync::Mutex, -} - -impl SharedState { - fn update(&self, value: i32) { - self.inner.lock().unwrap().value = value; - } - - fn get(&self) -> i32 { - self.inner.lock().unwrap().value - } -} -``` - -**Alternative for IO resources**: Spawn a dedicated task to manage the resource and communicate via message passing: - -```rust -let (tx, mut rx) = tokio::sync::mpsc::channel(32); - -tokio::spawn(async move { - while let Some(cmd) = rx.recv().await { - handle_io_command(&mut resource, cmd).await; - } -}); -``` - -## Build & Tooling - -Build speed tips: -- Use `cargo check` during rapid iteration over `cargo build` -- Minimize unnecessary dependencies and feature flags \ No newline at end of file diff --git a/.claude/skills/svelte-frontend/SKILL.md b/.claude/skills/svelte-frontend/SKILL.md index f51d36672a..57cac70302 100644 --- a/.claude/skills/svelte-frontend/SKILL.md +++ b/.claude/skills/svelte-frontend/SKILL.md @@ -3,316 +3,78 @@ name: svelte-frontend description: Svelte coding guidelines for the Windmill frontend. MUST use when writing or modifying code in the frontend directory. --- -# Svelte 5 Best Practices +# Windmill Svelte Patterns -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. +Apply these Windmill-specific patterns when writing Svelte code in `frontend/`. For general Svelte 5 syntax (runes, snippets, event handling), use the Svelte MCP server. -## Reactivity with Runes +## Windmill UI Components (MUST use) -Svelte 5 introduces Runes for more explicit and flexible reactivity. +Always use Windmill's design-system components. Never use raw HTML elements. -1. **Embrace Runes for State Management**: - * Use `$state` for reactive local component state. - ```svelte - - - - ``` - * Use `$derived` for computed values based on other reactive state. - ```svelte - - -

{count} * 2 = {doubled}

- ``` - * 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 - - ``` - -2. **Props with `$props`**: - * Declare component props using `$props()`. This offers better clarity and flexibility compared to `export let`. - ```svelte - - -

Name: {name}

-

Age: {age}

- ``` - * For bindable props, use `$bindable`. - ```svelte - - - - ``` - -## Event Handling - -* **Use direct event attributes**: Svelte 5 moves away from `on:` directives for DOM events. - * **Do**: `` - * **Don't**: `` -* **For component events, prefer callback props**: Instead of `createEventDispatcher`, pass functions as props. - ```svelte - - - -

Message from child: {message}

- - - - - ``` - -## Snippets for Content Projection - -* **Use `{#snippet ...}` and `{@render ...}` instead of slots**: Snippets are more powerful and flexible. - ```svelte - - - - - {#snippet title()} - My Awesome Title - {/snippet} - {#snippet content()} -

Some interesting content here.

- {/snippet} -
- - - - -
-
{@render title()}
-
{@render content()}
-
- ``` -* Default content is passed via the `children` prop (which is a snippet). - ```svelte - - -
- {@render children?.()} -
- ``` - -## 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 (``, `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 ` - -
- Hello -
- ``` -5. **Stay Updated**: Keep Svelte and its related packages up to date to benefit from the latest features, performance improvements, and security fixes. - -## Windmill UI Component Rules (MUST follow) - -Always use Windmill's own design-system components instead of raw HTML elements. Using raw HTML elements produces inconsistent styling and breaks the design language. - -### Icons — use `lucide-svelte` - -**Never** write inline SVGs. Import icons from `lucide-svelte`. - -```svelte - - - -``` - -### Buttons — use ` - - - + + {/if}
{#if jobs} @@ -831,6 +866,7 @@ showExternalJobs={graph !== 'RunChart'} activeLabel={filters.val.label} {lastFetchWentToEnd} + loadingExtra={jobsLoader.loadingExtra} bind:selectedIds bind:selectedWorkspace onloadExtra={loadExtra} diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 7cc9608c3b..1fbaca71c4 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -1651,6 +1651,11 @@ customEmail={customOnBehalfOfEmail} isDeployment={false} /> + {:else if script.on_behalf_of_email && !canPreserve} + + Currently: {originalOnBehalfOfEmail ?? script.on_behalf_of_email}. + Will be set to {$userStore?.email} on deploy (requires admin or wm_deployers group to override) + {/if}
diff --git a/frontend/src/lib/components/Tooltip.svelte b/frontend/src/lib/components/Tooltip.svelte index d9ded1f249..ad4db757eb 100644 --- a/frontend/src/lib/components/Tooltip.svelte +++ b/frontend/src/lib/components/Tooltip.svelte @@ -3,11 +3,9 @@ * @deprecated Use `$lib/components/meltComponents/Tooltip.svelte` instead. * This legacy tooltip component will be removed in a future version. */ - import Markdown from 'svelte-exmarkdown' import type { PopoverPlacement } from './Popover.model' import Popover from './Popover.svelte' import { InfoIcon } from 'lucide-svelte' - import { gfmPlugin } from 'svelte-exmarkdown/gfm' import { getContext, hasContext } from 'svelte' import { twMerge } from 'tailwind-merge' interface Props { @@ -16,7 +14,6 @@ placement?: PopoverPlacement | undefined documentationLink?: string | undefined small?: boolean - markdownTooltip?: string | undefined customSize?: string class?: string Icon?: typeof InfoIcon @@ -29,13 +26,11 @@ placement = undefined, documentationLink = undefined, small = false, - markdownTooltip = undefined, customSize = '100%', class: classNames = '', Icon = InfoIcon, children }: Props = $props() - const plugins = [gfmPlugin()] const disableTooltips = hasContext('disableTooltips') ? getContext('disableTooltips') === true @@ -58,13 +53,7 @@ {#snippet text()} - {#if markdownTooltip} -
- -
- {:else} - {@render children?.()} - {/if} + {@render children?.()} {/snippet} {/if} diff --git a/frontend/src/lib/components/TooltipInner.svelte b/frontend/src/lib/components/TooltipInner.svelte index 215d58a499..c21204b2b0 100644 --- a/frontend/src/lib/components/TooltipInner.svelte +++ b/frontend/src/lib/components/TooltipInner.svelte @@ -1,23 +1,18 @@ diff --git a/frontend/src/lib/components/WorkspaceDependenciesEditor.svelte b/frontend/src/lib/components/WorkspaceDependenciesEditor.svelte index 6e8c0af72b..937620dd22 100644 --- a/frontend/src/lib/components/WorkspaceDependenciesEditor.svelte +++ b/frontend/src/lib/components/WorkspaceDependenciesEditor.svelte @@ -145,7 +145,7 @@ numpy>=1.24.0 // go: `module mymod - // go 1.25 + // go 1.26 // require ( // rsc.io/quote v1.5.2 diff --git a/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts b/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts index 840a7a71d9..a44cab88fa 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts @@ -13,7 +13,7 @@ import { import { type Preview } from '$lib/gen' import type { DBSchema, GraphqlSchema, SQLSchema } from '$lib/stores' -import { stringifySchema } from '$lib/components/copilot/lib' +import { stringifyGraphqlSchema, stringifySchema } from '$lib/components/copilot/lib' import type { DbType } from '$lib/components/dbTypes' import { getDatabaseArg } from '$lib/components/dbOps' @@ -416,7 +416,7 @@ export async function getDbSchemas( workspace, requestBody: { language: sqlScript.lang as Preview['language'], - content: sqlScript.code, + content: typeof sqlScript.code === 'function' ? await sqlScript.code() : sqlScript.code, tag: options.customTag, args: { [sqlScript.argName]: resourcePath.startsWith('datatable://') @@ -458,7 +458,10 @@ export async function getDbSchemas( lang: 'graphql' as GraphqlSchema['lang'], schema: result } - return { ...(dbSchema as any), stringified: stringifySchema(dbSchema as any) } + return { + ...(dbSchema as any), + stringified: await stringifyGraphqlSchema(result) + } } } } diff --git a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts index 961fac9b5b..ee30619366 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts @@ -1,11 +1,6 @@ import type { ScriptLang } from '$lib/gen' import type { SQLSchema } from '$lib/stores' -import { - buildClientSchema, - getIntrospectionQuery, - printSchema, - type IntrospectionQuery -} from 'graphql' +import type { IntrospectionQuery } from 'graphql' import type { DbType } from '$lib/components/dbTypes' @@ -64,7 +59,7 @@ export function resourceTypeToLang(rt: string) { const legacyScripts: Record< string, { - code: string + code: string | (() => Promise) lang: string processingFn?: (any: any) => SQLSchema['schema'] argName: string @@ -146,7 +141,7 @@ const legacyScripts: Record< argName: 'database' }, graphql: { - code: getIntrospectionQuery(), + code: () => import('graphql').then((m) => m.getIntrospectionQuery()), lang: 'graphql', argName: 'api' }, @@ -305,7 +300,8 @@ export function formatSchema(dbSchema: { } } -export function formatGraphqlSchema(schema: IntrospectionQuery): string { +export async function formatGraphqlSchema(schema: IntrospectionQuery): Promise { + const { buildClientSchema, printSchema } = await import('graphql') return printSchema(buildClientSchema(schema)) } diff --git a/frontend/src/lib/components/apps/editor/appPolicy.ts b/frontend/src/lib/components/apps/editor/appPolicy.ts index afa6962361..ee1f29babd 100644 --- a/frontend/src/lib/components/apps/editor/appPolicy.ts +++ b/frontend/src/lib/components/apps/editor/appPolicy.ts @@ -1,4 +1,3 @@ -import { Sha256 } from '@aws-crypto/sha256-js' import { getCountInput } from '../components/display/dbtable/queries/count' import { getDeleteInput } from '../components/display/dbtable/queries/delete' import { getInsertInput } from '../components/display/dbtable/queries/insert' @@ -236,6 +235,7 @@ async function hash(message) { return hashHex } catch { //subtle not available, trying pure js + const { Sha256 } = await import('@aws-crypto/sha256-js') const hash = new Sha256() hash.update(message ?? '') const result = Array.from(await hash.digest()) diff --git a/frontend/src/lib/components/apps/editor/commonAppUtils.ts b/frontend/src/lib/components/apps/editor/commonAppUtils.ts index ab68dc6080..3dc4139295 100644 --- a/frontend/src/lib/components/apps/editor/commonAppUtils.ts +++ b/frontend/src/lib/components/apps/editor/commonAppUtils.ts @@ -1,5 +1,4 @@ import type { StaticAppInput } from '../inputType' -import { Sha256 } from '@aws-crypto/sha256-js' export function collectStaticFields(fields: Record) { return Object.fromEntries( @@ -26,6 +25,7 @@ export async function hash(message) { return hashHex } catch { //subtle not available, trying pure js + const { Sha256 } = await import('@aws-crypto/sha256-js') const hash = new Sha256() hash.update(message ?? '') const result = Array.from(await hash.digest()) diff --git a/frontend/src/lib/components/apps/editor/component/components.ts b/frontend/src/lib/components/apps/editor/component/components.ts index 7e5b08c0fc..df6d1b2a0e 100644 --- a/frontend/src/lib/components/apps/editor/component/components.ts +++ b/frontend/src/lib/components/apps/editor/component/components.ts @@ -3069,18 +3069,7 @@ This is a paragraph. type: 'static', value: 'yyyy-MM-dd', fieldType: 'text', - markdownTooltip: `### Output format -See date-fns format for more information. By default, it is 'yyyy-MM-dd' - -| Format | Result | Description | -| ----------- | ----------- | ----------- | -| dd | 01, 02, ..., 31 | Day of the month | -| d | 1, 2, ..., 31 | Day of the month | -| MM | 01, 02, ..., 12 | Month | -| MMM | Jan, Feb, ..., Dec | Month | -| MMMM | January, February, ..., December | Month | -| yyyy | 2021, 2022, ... | Year | -`, + tooltip: `Output format
See date-fns format for more information. Default: yyyy-MM-dd
FormatResultDescription
dd01, 02, ..., 31Day of the month
d1, 2, ..., 31Day of the month
MM01, 02, ..., 12Month
MMMJan, Feb, ..., DecMonth
MMMMJanuary, ..., DecemberMonth
yyyy2021, 2022, ...Year
`, documentationLink: 'https://date-fns.org/v2.30.0/docs/format', placeholder: 'yyyy-MM-dd' @@ -3131,21 +3120,7 @@ See date-fns format for more information. By default, it is 'yyyy-MM-dd' fieldType: 'text', documentationLink: 'https://date-fns.org/v2.30.0/docs/format', placeholder: 'dd.MM.yyyy HH:mm', - markdownTooltip: `### Output format -See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm' - -| Format | Result | Description | -| ----------- | ----------- | ----------- | -| dd | 01, 02, ..., 31 | Day of the month | -| d | 1, 2, ..., 31 | Day of the month | -| MM | 01, 02, ..., 12 | Month | -| MMM | Jan, Feb, ..., Dec | Month | -| MMMM | January, February, ..., December | Month | -| yyyy | 2021, 2022, ... | Year | -| HH | 00, 01, ..., 23 | Hours | -| mm | 00, 01, ..., 59 | Minutes | -| ss | 00, 01, ..., 59 | Seconds | - ` + tooltip: `Output format
See date-fns format for more information. Default: dd.MM.yyyy HH:mm
FormatResultDescription
dd01, 02, ..., 31Day of the month
d1, 2, ..., 31Day of the month
MM01, 02, ..., 12Month
MMMJan, Feb, ..., DecMonth
MMMMJanuary, ..., DecemberMonth
yyyy2021, 2022, ...Year
HH00, 01, ..., 23Hours
mm00, 01, ..., 59Minutes
ss00, 01, ..., 59Seconds
` }, defaultValue: { type: 'static', diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte index 1fa6050b6f..3168e4c5f7 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte @@ -55,7 +55,6 @@ recomputeOnInputChanged?: boolean showOnDemandOnlyToggle?: boolean documentationLink?: string | undefined - markdownTooltip?: string | undefined securedContext?: boolean disabled?: boolean oncloseConnection?: (...args: any[]) => any @@ -88,7 +87,6 @@ recomputeOnInputChanged = true, showOnDemandOnlyToggle = true, documentationLink = undefined, - markdownTooltip = undefined, securedContext = false, oncloseConnection = undefined, onopenConnection = undefined @@ -180,9 +178,9 @@ {#if loading} {/if} - {#if tooltip || markdownTooltip} - - {tooltip} + {#if tooltip} + + {@html tooltip} {/if} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte index 8fc9f8f9b2..4a2a488d80 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte @@ -97,8 +97,7 @@ customTitle={meta?.['customTitle']} loading={meta?.['loading']} documentationLink={meta?.['documentationLink']} - markdownTooltip={meta?.['markdownTooltip']} - allowTypeChange={meta?.['allowTypeChange']} + allowTypeChange={meta?.['allowTypeChange']} {displayType} {recomputeOnInputChanged} {showOnDemandOnlyToggle} diff --git a/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte b/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte index 78de5bb5b5..5ef41b38e0 100644 --- a/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte +++ b/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte @@ -65,13 +65,10 @@ {:else if contextElement.type === 'db'}
{#if contextElement.schema && contextElement.schema.lang === 'graphql'} - {#await import('$lib/components/GraphqlSchemaViewer.svelte')} + {#await Promise.all([import('$lib/components/GraphqlSchemaViewer.svelte'), formatGraphqlSchema(contextElement.schema.schema)])} - {:then Module} - + {:then [Module, code]} + {/await} {:else if contextElement.schema} diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index 538ab2a103..4d77ebd01a 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -1,6 +1,6 @@ import type { AIProvider, AIProviderModel } from '$lib/gen' -import { workspaceStore, type DBSchema, type GraphqlSchema, type SQLSchema } from '$lib/stores' -import { buildClientSchema, printSchema } from 'graphql' +import { workspaceStore, type DBSchema, type SQLSchema } from '$lib/stores' +import type { IntrospectionQuery } from 'graphql' import OpenAI from 'openai' import type { ChatCompletionChunk, @@ -587,43 +587,40 @@ export function addThousandsSeparator(n: number) { return n.toFixed().replace(/\B(?=(\d{3})+(?!\d))/g, "'") } -export function stringifySchema( - dbSchema: Omit | Omit -) { +export function stringifySchema(dbSchema: Omit): string { const { schema, lang } = dbSchema - if (lang === 'graphql') { - let graphqlSchema = printSchema(buildClientSchema(schema)) - return graphqlSchema - } else { - let smallerSchema: { - [schemaKey: string]: { - [tableKey: string]: Array<[string, string, boolean, string?]> - } - } = {} - for (const schemaKey in schema) { - smallerSchema[schemaKey] = {} - for (const tableKey in schema[schemaKey]) { - smallerSchema[schemaKey][tableKey] = [] - for (const colKey in schema[schemaKey][tableKey]) { - const col = schema[schemaKey][tableKey][colKey] - const p: [string, string, boolean, string?] = [colKey, col.type, col.required] - if (col.default) { - p.push(col.default) - } - smallerSchema[schemaKey][tableKey].push(p) + let smallerSchema: { + [schemaKey: string]: { + [tableKey: string]: Array<[string, string, boolean, string?]> + } + } = {} + for (const schemaKey in schema) { + smallerSchema[schemaKey] = {} + for (const tableKey in schema[schemaKey]) { + smallerSchema[schemaKey][tableKey] = [] + for (const colKey in schema[schemaKey][tableKey]) { + const col = schema[schemaKey][tableKey][colKey] + const p: [string, string, boolean, string?] = [colKey, col.type, col.required] + if (col.default) { + p.push(col.default) } + smallerSchema[schemaKey][tableKey].push(p) } } - - let finalSchema: typeof smallerSchema | (typeof smallerSchema)['schemaKey'] = smallerSchema - if (dbSchema.publicOnly) { - finalSchema = - smallerSchema.public || smallerSchema.PUBLIC || smallerSchema.dbo || smallerSchema - } else if (lang === 'mysql' && Object.keys(smallerSchema).length === 1) { - finalSchema = smallerSchema[Object.keys(smallerSchema)[0]] - } - return JSON.stringify(finalSchema) } + + let finalSchema: typeof smallerSchema | (typeof smallerSchema)['schemaKey'] = smallerSchema + if (dbSchema.publicOnly) { + finalSchema = smallerSchema.public || smallerSchema.PUBLIC || smallerSchema.dbo || smallerSchema + } else if (lang === 'mysql' && Object.keys(smallerSchema).length === 1) { + finalSchema = smallerSchema[Object.keys(smallerSchema)[0]] + } + return JSON.stringify(finalSchema) +} + +export async function stringifyGraphqlSchema(schema: unknown): Promise { + const { buildClientSchema, printSchema } = await import('graphql') + return printSchema(buildClientSchema(schema as IntrospectionQuery)) } function addDBSChema(scriptOptions: CopilotOptions, prompt: string) { diff --git a/frontend/src/lib/components/flows/content/FlowSettings.svelte b/frontend/src/lib/components/flows/content/FlowSettings.svelte index c190781f63..9d52b1a180 100644 --- a/frontend/src/lib/components/flows/content/FlowSettings.svelte +++ b/frontend/src/lib/components/flows/content/FlowSettings.svelte @@ -80,7 +80,7 @@ { name: 'Early Return', active: Boolean(flowStore.val.value.early_return) }, { name: 'Dedicated Worker', active: Boolean(flowStore.val.dedicated_worker) }, { name: 'Concurrent Limit', active: Boolean(flowStore.val.value.concurrent_limit) }, - { name: 'Run on Behalf of Last Editor', active: Boolean(flowStore.val.on_behalf_of_email) }, + { name: `Run on Behalf of ${flowStore.val.on_behalf_of_email ?? 'Last Editor'}`, active: Boolean(flowStore.val.on_behalf_of_email) }, { name: 'Worker Tag', active: displayWorkerTagPicker } ]) @@ -426,6 +426,11 @@ customEmail={customOnBehalfOfEmail} isDeployment={false} /> + {:else if flowStore.val.on_behalf_of_email && !canPreserve} + + Currently: {$savedOnBehalfOfEmail ?? flowStore.val.on_behalf_of_email}. + Will be set to {$userStore?.email} on deploy (requires admin or wm_deployers group to override) + {/if} diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index 9e67ff585c..de4cd1528b 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -140,6 +140,21 @@ export const settings: Record = { !value?.endsWith('/') && !value?.endsWith(' ')) }, + { + label: 'Email domain', + description: 'Domain to display in webhooks for email triggers (should match the MX record)', + key: 'email_domain', + fieldType: 'text', + placeholder: 'mail.windmill.com', + storage: 'setting', + error: 'Must be a valid domain', + isValid: (value: string | undefined) => + value == undefined || + value === '' || + /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/.test( + value + ) + }, { label: 'Request size limit in MB', description: 'Maximum size of HTTP requests in MB.', @@ -216,7 +231,6 @@ export const settings: Record = { description: 'Maximum amount of time (measured in seconds) that a sync endpoint is allowed to run before it is forcibly stopped or timed out.', key: 'timeout_wait_result', - cloudonly: true, fieldType: 'seconds', placeholder: '60', storage: 'setting' diff --git a/frontend/src/lib/components/meltComponents/Tooltip.svelte b/frontend/src/lib/components/meltComponents/Tooltip.svelte index 54e8047352..fbe6ac3d57 100644 --- a/frontend/src/lib/components/meltComponents/Tooltip.svelte +++ b/frontend/src/lib/components/meltComponents/Tooltip.svelte @@ -14,7 +14,6 @@ placement?: Placement | undefined documentationLink?: string | undefined small?: boolean - markdownTooltip?: string | undefined disablePopup?: boolean /** When true, the trigger renders as a non-interactive span instead of a button */ notClickable?: boolean @@ -40,7 +39,6 @@ placement = 'bottom', documentationLink = undefined, small = false, - markdownTooltip = undefined, disablePopup = false, notClickable: _notClickable = false, openDelay = 300, @@ -90,7 +88,7 @@ {#if $open && !disablePopup}
- + {@render text?.()}
diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte index 0385eb9211..715132fc1f 100644 --- a/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte @@ -252,7 +252,6 @@ customTitle={meta?.['customTitle']} loading={meta?.['loading']} documentationLink={meta?.['documentationLink']} - markdownTooltip={meta?.['markdownTooltip']} allowTypeChange={meta?.['allowTypeChange']} displayType /> diff --git a/frontend/src/lib/components/raw_apps/RawAppInputsSpecEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppInputsSpecEditor.svelte index 87d8de2085..70c75c3898 100644 --- a/frontend/src/lib/components/raw_apps/RawAppInputsSpecEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppInputsSpecEditor.svelte @@ -45,7 +45,6 @@ allowTypeChange?: boolean loading?: boolean documentationLink?: string | undefined - markdownTooltip?: string | undefined } let { @@ -64,8 +63,7 @@ displayType = false, allowTypeChange = true, loading = false, - documentationLink = undefined, - markdownTooltip = undefined + documentationLink = undefined }: Props = $props() $effect(() => { @@ -97,9 +95,9 @@ {#if loading} {/if} - {#if tooltip || markdownTooltip} - - {tooltip} + {#if tooltip} + + {@html tooltip} {/if}
diff --git a/frontend/src/lib/components/runs/RunsTable.svelte b/frontend/src/lib/components/runs/RunsTable.svelte index e99ae589b4..84bebf5f4b 100644 --- a/frontend/src/lib/components/runs/RunsTable.svelte +++ b/frontend/src/lib/components/runs/RunsTable.svelte @@ -37,6 +37,7 @@ batchRerunOptionsIsOpen?: boolean manualSelectionMode: undefined | 'cancel' | 'rerun' onCancelJobs: (jobIds: string[]) => void + loadingExtra?: boolean onselect?: (...args: any[]) => any onloadExtra?: (...args: any[]) => any onfilterByLabel?: (...args: any[]) => any @@ -62,6 +63,7 @@ manualSelectionMode, onCancelJobs, batchRerunOptionsIsOpen = $bindable(), + loadingExtra = false, onselect = undefined, onloadExtra = undefined, onfilterByLabel = undefined, @@ -556,12 +558,17 @@ >{#if !lastFetchWentToEnd && jobs && jobs.length >= perPage} {/if} diff --git a/frontend/src/lib/components/runs/runsFilter.ts b/frontend/src/lib/components/runs/runsFilter.ts index 589c44a984..3aaa9a2842 100644 --- a/frontend/src/lib/components/runs/runsFilter.ts +++ b/frontend/src/lib/components/runs/runsFilter.ts @@ -238,6 +238,7 @@ export const buildRunsFilterPresets = ({ }) => [ { name: 'Hide schedules', value: 'job_trigger_kind:\\ !schedule' }, { name: 'Hide future jobs', value: 'show_future_jobs:\\ false' }, + { name: 'Show skipped', value: 'show_skipped:\\ true' }, ...(isSuperadmin && isAdminsWorkspace ? [{ name: 'All workspaces', value: 'all_workspaces:\\ true' }] : []) diff --git a/frontend/src/lib/components/runs/useJobsLoader.svelte.ts b/frontend/src/lib/components/runs/useJobsLoader.svelte.ts index e2bbeb3fe0..5e9aa29222 100644 --- a/frontend/src/lib/components/runs/useJobsLoader.svelte.ts +++ b/frontend/src/lib/components/runs/useJobsLoader.svelte.ts @@ -58,7 +58,6 @@ export interface UseJobLoaderArgs { skip?: boolean lookback?: number perPage?: number - onSetPerPage?: (perPage: number) => void } export function useJobsLoader(args: () => UseJobLoaderArgs) { @@ -73,7 +72,6 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { let refreshRate = $derived(_args.refreshRate ?? 5000) let syncQueuedRunsCount = $derived(_args.syncQueuedRunsCount ?? true) let lookback = $derived(_args.lookback ?? 0) - let onSetPerPage = $derived(_args.onSetPerPage) let timeframe = $derived(_args?.timeframe) let perPage = $derived(_args?.perPage ?? 1000) @@ -100,7 +98,9 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { let queue_count: Tweened | undefined = $state() let suspended_count: Tweened | undefined = $state() let loading = $state(false) + let loadingExtra = $state(false) let lastFetchWentToEnd = $state(true) + let batchProgress = $state<{ loaded: number; total: number } | null>(null) let completedJobs: CompletedJob[] | undefined = $state() let externalJobs: Job[] | undefined = $state() @@ -111,18 +111,33 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { let sync = true let paramChangeTimeout: ReturnType | undefined let paramChangePromise: CancelablePromise | undefined + let slowStreamIntervalId: ReturnType | undefined + let currentBatchSize = $state(null) function onParamChanges() { resetJobs() - let promise = loadJobsIntern(true) - promise = CancelablePromiseUtils.onTimeout(promise, 4000, () => { + slowStreamIntervalId = setInterval(() => { sendUserToast( - 'Loading jobs is taking longer than expected...', + 'Loading is taking a long time...', 'warning', - perPage > 25 && onSetPerPage - ? [{ label: 'Reduce to 25 items per page', callback: () => onSetPerPage(25) }] - : [] + [{ label: 'Stop loading', callback: () => stopBatchLoading() }], + undefined, + 8000 ) + }, 15000) + let promise = loadJobsIntern(true) + if (perPage > 25) { + promise = CancelablePromiseUtils.onTimeout(promise, 4000, () => { + sendUserToast('Loading jobs is taking longer than expected...', 'warning', [ + { label: 'Stream by batches of 25', callback: () => restreamWithSmallBatches() } + ]) + }) + } + promise = CancelablePromiseUtils.pipe(promise, () => { + if (slowStreamIntervalId) { + clearInterval(slowStreamIntervalId) + slowStreamIntervalId = undefined + } }) promise = CancelablePromiseUtils.catchErr(promise, (e) => { if (e instanceof CancelError) { @@ -133,50 +148,85 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { return promise } + function restreamWithBatchSize(size: number) { + paramChangePromise?.cancel() + resetJobs() + slowStreamIntervalId = setInterval(() => { + sendUserToast( + 'Loading is taking a long time...', + 'warning', + [{ label: 'Stop loading', callback: () => stopBatchLoading() }], + undefined, + 8000 + ) + }, 15000) + paramChangePromise = loadJobsIntern(false, size) + paramChangePromise = CancelablePromiseUtils.pipe(paramChangePromise, () => { + if (slowStreamIntervalId) { + clearInterval(slowStreamIntervalId) + slowStreamIntervalId = undefined + } + }) + paramChangePromise = CancelablePromiseUtils.catchErr(paramChangePromise, (e) => { + if (e instanceof CancelError) { + return CancelablePromiseUtils.pure(undefined) + } + return CancelablePromiseUtils.err(e) + }) + } + + function restreamWithSmallBatches() { + restreamWithBatchSize(25) + } + + function restreamWithSingleItems() { + restreamWithBatchSize(1) + } + let loadingFetch = false async function loadExtraJobs(): Promise { - if (jobs && jobs.length > 0) { - let minQueueTs: string | undefined = undefined - let minCompletedTs: string | undefined = undefined + loading = true + loadingExtra = true + const batchSize = Math.min(perPage, 1000) + await loadExtraJobsBatch(batchSize) + loadingExtra = false + } - let cursor = 0 - - while (jobs && cursor < jobs?.length) { - cursor++ - const job = jobs[jobs.length - 1 - cursor] - if (job.type == 'CompletedJob') { - minCompletedTs = job.completed_at - break - } else if (job.type == 'QueuedJob' && minQueueTs == undefined) { - minQueueTs = job.created_at - } - } - - const ts = minCompletedTs ?? minQueueTs - - if (!ts) { - sendUserToast('No jobs to load from') - lastFetchWentToEnd = false - return - } - // const minCreated = lastJob?.created_at - const minCreated = new Date(new Date(ts).getTime() - 1).toISOString() - - const minTs = timeframe?.computeMinMax().minTs ?? null - let olderJobs = await fetchJobs(minCreated, minTs, undefined) - jobs = updateWithNewJobs(olderJobs ?? [], jobs ?? []) - computeCompletedJobs() - lastFetchWentToEnd = olderJobs?.length < perPage - } else { - lastFetchWentToEnd = false + function loadExtraJobsBatch(batchSize: number): CancelablePromise { + if (!jobs || jobs.length === 0) { + lastFetchWentToEnd = true + return CancelablePromiseUtils.pure(undefined as void) } + const lastJob = jobs[jobs.length - 1] + const ts = lastJob.created_at + if (!ts) { + lastFetchWentToEnd = true + return CancelablePromiseUtils.pure(undefined as void) + } + const cursorTs = new Date(new Date(ts).getTime() - 1).toISOString() + const minTs = timeframe?.computeMinMax().minTs ?? null + return CancelablePromiseUtils.map( + fetchJobs(null, minTs, undefined, cursorTs, batchSize), + (olderJobs) => { + jobs = updateWithNewJobs(olderJobs ?? [], jobs ?? []) + if (extendedJobs) { + extendedJobs.jobs = jobs ?? [] + extendedJobs = extendedJobs + } + computeCompletedJobs() + lastFetchWentToEnd = (olderJobs?.length ?? 0) < batchSize + loading = false + } + ) } function fetchJobs( completedBefore: string | null, completedAfter: string | null, - createdAfterQueue: string | undefined + createdAfterQueue: string | undefined, + createdBefore?: string, + perPageOverride?: number ): CancelablePromise { if (_args.skip) return CancelablePromiseUtils.pure([]) loadingFetch = true @@ -186,6 +236,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { let isCompletedOnly = success == 'success' || success == 'failure' let promise = JobService.listJobs({ workspace: currentWorkspace, + createdBefore: createdBefore ?? undefined, completedBefore: isQueueOnly ? undefined : (completedBefore ?? undefined), completedAfter: isQueueOnly ? undefined : (completedAfter ?? undefined), createdBeforeQueue: isQueueOnly ? (completedBefore ?? undefined) : undefined, @@ -226,7 +277,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { : undefined, triggerKind: jobTriggerKind ?? undefined, allWorkspaces: allWorkspaces ? true : undefined, - perPage, + perPage: perPageOverride ?? perPage, allowWildcards: allowWildcards ? true : undefined, broadFilter }) @@ -299,16 +350,36 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { await loadJobsIntern(shouldGetCount) } + function stopBatchLoading(): void { + paramChangePromise?.cancel() + if (slowStreamIntervalId) { + clearInterval(slowStreamIntervalId) + slowStreamIntervalId = undefined + } + batchProgress = null + currentBatchSize = null + loading = false + } + function resetJobs() { jobs = undefined completedJobs = undefined externalJobs = undefined extendedJobs = undefined lastFetchWentToEnd = false + batchProgress = null + currentBatchSize = null + if (slowStreamIntervalId) { + clearInterval(slowStreamIntervalId) + slowStreamIntervalId = undefined + } intervalId && clearInterval(intervalId) intervalId = setInterval(syncer, refreshRate) } - function loadJobsIntern(shouldGetCount?: boolean): CancelablePromise { + function loadJobsIntern( + shouldGetCount?: boolean, + overrideBatchSize?: number + ): CancelablePromise { const { minTs, maxTs } = timeframe?.computeMinMax() ?? { minTs: null, maxTs: null } if (shouldGetCount) { getCount() @@ -320,20 +391,77 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { const extendedMinTs = subtractDaysFromDateString(minTs, lookback) if (concurrencyKey == null || concurrencyKey === '') { - return CancelablePromiseUtils.map( - fetchJobs(maxTs, extendedMinTs ?? null, extendedMinTs), - (newJobs) => { - extendedJobs = { jobs: newJobs, obscured_jobs: [] } as ExtendedJobs - - // Filter on minTs here and not in the backend - // to get enough data for the concurrency graph - jobs = sortMinDate(minTs, newJobs) - externalJobs = [] - computeCompletedJobs() - lastFetchWentToEnd = newJobs.length < perPage - loading = false - } + const batchSize = overrideBatchSize ?? Math.min(perPage, 1000) + const isBatched = perPage > batchSize + if (isBatched) { + batchProgress = { loaded: 0, total: perPage } + } + currentBatchSize = batchSize + let slowBatchToastShown = false + let firstFetchPromise = fetchJobs( + maxTs, + extendedMinTs ?? null, + extendedMinTs, + undefined, + batchSize ) + if (isBatched && batchSize > 1) { + firstFetchPromise = CancelablePromiseUtils.onTimeout(firstFetchPromise, 4000, () => { + if (!slowBatchToastShown) { + slowBatchToastShown = true + sendUserToast( + `Streaming by batches of ${batchSize} is slow, try loading one at a time`, + 'warning', + [{ label: 'Stream 1 by 1', callback: () => restreamWithSingleItems() }] + ) + } + }) + } + let p = CancelablePromiseUtils.map(firstFetchPromise, (newJobs) => { + extendedJobs = { jobs: newJobs, obscured_jobs: [] } as ExtendedJobs + + // Filter on minTs here and not in the backend + // to get enough data for the concurrency graph + jobs = sortMinDate(minTs, newJobs) + externalJobs = [] + computeCompletedJobs() + lastFetchWentToEnd = newJobs.length < batchSize + loading = false + if (isBatched) { + batchProgress = { loaded: jobs?.length ?? 0, total: perPage } + } + }) + if (isBatched) { + const numExtraBatches = Math.ceil(perPage / batchSize) - 1 + for (let i = 0; i < numExtraBatches; i++) { + p = CancelablePromiseUtils.then(p, (): CancelablePromise => { + if (lastFetchWentToEnd || !jobs) + return CancelablePromiseUtils.pure(undefined as void) + loading = true + let batchPromise = CancelablePromiseUtils.pipe(loadExtraJobsBatch(batchSize), () => { + batchProgress = { loaded: jobs?.length ?? 0, total: perPage } + }) + if (batchSize > 1 && !slowBatchToastShown) { + batchPromise = CancelablePromiseUtils.onTimeout(batchPromise, 4000, () => { + if (!slowBatchToastShown) { + slowBatchToastShown = true + sendUserToast( + `Streaming by batches of ${batchSize} is slow, try loading one at a time`, + 'warning', + [{ label: 'Stream 1 by 1', callback: () => restreamWithSingleItems() }] + ) + } + }) + } + return batchPromise + }) + } + } + p = CancelablePromiseUtils.pipe(p, () => { + batchProgress = null + currentBatchSize = null + }) + return p } else { return CancelablePromiseUtils.map( fetchExtendedJobs(concurrencyKey, maxTs, extendedMinTs ?? null), @@ -577,6 +705,14 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { return { loadExtraJobs, loadJobs, + stopBatchLoading, + restreamWithBatchSize, + get batchProgress() { + return batchProgress + }, + get currentBatchSize() { + return currentBatchSize + }, get queue_count() { return queue_count }, @@ -586,6 +722,9 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) { get loading() { return loading }, + get loadingExtra() { + return loadingExtra + }, get completedJobs() { return completedJobs }, diff --git a/frontend/src/lib/components/select/GenericDropdown.svelte b/frontend/src/lib/components/select/GenericDropdown.svelte index 9771e8f823..fc738d5f3f 100644 --- a/frontend/src/lib/components/select/GenericDropdown.svelte +++ b/frontend/src/lib/components/select/GenericDropdown.svelte @@ -44,7 +44,10 @@ if (!getInputRect || !listEl) return { width: 0, height: 0, x: 0, y: 0, isBelow: true } let inputR = getInputRect() const listR = listEl.getBoundingClientRect() - const isBelow = inputR.y + inputR.height + listR.height <= window.innerHeight + const fitsBelow = inputR.y + inputR.height + listR.height <= window.innerHeight + const spaceBelow = window.innerHeight - (inputR.y + inputR.height) + const spaceAbove = inputR.y + const isBelow = fitsBelow || spaceBelow >= spaceAbove let [x, y] = disablePortal ? [0, 0] : [inputR.x, inputR.y] if (isBelow) return { width: inputR.width, height: listR.height, x: x, y: y + inputR.height, isBelow } diff --git a/frontend/src/lib/components/sqlTypeService.ts b/frontend/src/lib/components/sqlTypeService.ts index d1f756c043..b37a17dc7a 100644 --- a/frontend/src/lib/components/sqlTypeService.ts +++ b/frontend/src/lib/components/sqlTypeService.ts @@ -36,13 +36,12 @@ async function getWorkerClient(): Promise<(...uris: Uri[]) => Promise { +export async function waitForWorkerInitialization(modelUri: string): Promise { const WORKER_INIT_TIMEOUT = 10000 const MAX_RETRIES = 10 const RETRY_DELAY = 300 - if (!fileUri.endsWith('.ts')) fileUri += '.ts' - const uri = Uri.parse(fileUri) + const uri = Uri.parse(modelUri) const startTime = Date.now() @@ -84,12 +83,11 @@ export async function waitForWorkerInitialization(fileUri: string): Promise { try { - if (!fileUri.endsWith('.ts')) fileUri += '.ts' - const uri = Uri.parse(fileUri) + const uri = Uri.parse(modelUri) const uriString = uri.toString() const workerClient = await getWorkerClient() diff --git a/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte b/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte index 4f51621f58..bb7086e06a 100644 --- a/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte +++ b/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte @@ -57,6 +57,7 @@ displayName: 'Google', description: 'Connect to Google for Drive and Calendar triggers', icon: GoogleIcon, + docsUrl: 'https://www.windmill.dev/docs/core_concepts/native_triggers#google-triggers', requiresBaseUrl: false, clientIdPlaceholder: 'xxxx.apps.googleusercontent.com', clientSecretPlaceholder: 'Google Cloud Console client secret', diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 3fa1b3256a..1cf01ba344 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -17,8 +17,6 @@ export { sendUserToast } import type { AnyMeltElement } from '@melt-ui/svelte' import type { TriggerKind } from './components/triggers' import { stateSnapshot } from './svelte5Utils.svelte' -import { validate, dereference } from '@scalar/openapi-parser' - export namespace OpenApi { export enum OpenApiVersion { V2, @@ -59,6 +57,7 @@ export namespace OpenApi { * @throws Will throw an error if the specification is invalid or cannot be parsed. */ export async function parse(api: string): Promise<[OpenAPI.Document, OpenApiVersion]> { + const { validate, dereference } = await import('@scalar/openapi-parser') const { valid, errors } = await validate(api) if (!valid) { diff --git a/lsp/Dockerfile b/lsp/Dockerfile index 075ad9dba0..7efa5e8cd9 100644 --- a/lsp/Dockerfile +++ b/lsp/Dockerfile @@ -21,13 +21,13 @@ RUN set -eux; \ url=; \ case "$arch" in \ 'amd64') \ - targz='go1.25.0.linux-amd64.tar.gz'; \ + targz='go1.26.0.linux-amd64.tar.gz'; \ ;; \ 'arm64') \ - targz='go1.25.0.linux-arm64.tar.gz'; \ + targz='go1.26.0.linux-arm64.tar.gz'; \ ;; \ 'armhf') \ - targz='go1.25.0.linux-armv6l.tar.gz'; \ + targz='go1.26.0.linux-armv6l.tar.gz'; \ ;; \ *) echo >&2 "error: unsupported architecture '$arch' (likely packaging update needed)"; exit 1 ;; \ esac; \ diff --git a/lsp/Pipfile b/lsp/Pipfile index 188a8d5685..876db2f672 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,7 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.646.0" -wmill_pg = ">=1.646.0" +wmill = ">=1.647.1" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/lsp/pyls_launcher.py b/lsp/pyls_launcher.py index e41c3a3bd3..c77a08c35c 100644 --- a/lsp/pyls_launcher.py +++ b/lsp/pyls_launcher.py @@ -97,7 +97,7 @@ if __name__ == "__main__": go_mod_path = os.path.join(monaco_path, "go.mod") if not os.path.exists(go_mod_path): f = open(go_mod_path, "w") - f.write("module mymod\ngo 1.25") + f.write("module mymod\ngo 1.26") f.close() port = int(os.environ.get("PORT", "3001")) app = web.Application( diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index a33bf589ea..725bc4a658 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.646.0 + version: 1.647.1 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index c2346ef452..b8e24f942e 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.646.0' + ModuleVersion = '1.647.1' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/build.sh b/python-client/build.sh index 7abcd92a09..825d0f3933 100755 --- a/python-client/build.sh +++ b/python-client/build.sh @@ -61,7 +61,6 @@ mv windmill-api/README.md.tmp windmill-api/README.md cd windmill-api && poetry build cd ../wmill && poetry build -cd ../wmill_pg && poetry build cd .. && echo "windmill-api/" >> .gitignore # Build documentation (similar to typescript-client/build_typedoc.sh) diff --git a/python-client/install.sh b/python-client/install.sh index ae463574fb..6201928d0f 100755 --- a/python-client/install.sh +++ b/python-client/install.sh @@ -3,14 +3,11 @@ set -e rm windmill-api/dist/* || true rm wmill/dist/* || true -rm wmill_pg/dist/* || true ./build.sh pip3 install windmill-api/dist/*.whl --force-reinstall --user --break-system-packages pip3 install wmill/dist/*.whl --force-reinstall --user --break-system-packages -pip3 install wmill_pg/dist/*.whl --force-reinstall --user --break-system-packages rm -rf windmill-api/ rm -rf wmill/dist/* -rm -rf install wmill_pg/dist/* diff --git a/python-client/publish.sh b/python-client/publish.sh index b6610ebf7f..6b1d1b3d5d 100755 --- a/python-client/publish.sh +++ b/python-client/publish.sh @@ -5,7 +5,5 @@ set -e cd windmill-api && poetry publish --username __token__ --password $PYPI_PASSWORD -n || true cd ../wmill && poetry publish --username __token__ --password $PYPI_PASSWORD -n || true -sleep 5 -cd ../wmill_pg && poetry publish --username __token__ --password $PYPI_PASSWORD -n || true rm -rf windmill-api/ diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index c7f4217d4b..0a44ff7453 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.646.0" +version = "1.647.1" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill/wmill/client.py b/python-client/wmill/wmill/client.py index 8c050f554c..ee5f13edf5 100644 --- a/python-client/wmill/wmill/client.py +++ b/python-client/wmill/wmill/client.py @@ -2228,7 +2228,7 @@ class DataTableClient: args_def = "" for i, arg in enumerate(args): args_dict[f"arg{i+1}"] = arg - args_def += f"-- ${i+1} arg{i+1}\n" + args_def += f"-- ${i+1} arg{i+1} ({infer_sql_type(arg)})\n" sql = args_def + sql return SqlQuery( sql, @@ -2338,7 +2338,7 @@ def infer_sql_type(value) -> str: elif isinstance(value, int): return "BIGINT" elif isinstance(value, float): - return "DOUBLE PRECISION" + return "FLOAT8" elif value is None: return "TEXT" elif isinstance(value, str): diff --git a/python-client/wmill_pg/README.md b/python-client/wmill_pg/README.md deleted file mode 100644 index 4e043806be..0000000000 --- a/python-client/wmill_pg/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# wmill - -The postgres extension client for the [Windmill](https://windmill.dev) platform. - -[windmill-api](https://pypi.org/project/windmill-api/). - -## Quickstart - -```python -import wmill_pg - - -def main(): - my_list = query("UPDATE demo SET value = 'value' RETURNING key, value") - for key, value in my_list: - ... -``` diff --git a/python-client/wmill_pg/dist/wmill_pg-1.245.0-py3-none-any.whl b/python-client/wmill_pg/dist/wmill_pg-1.245.0-py3-none-any.whl deleted file mode 100644 index 5e487f5b88..0000000000 Binary files a/python-client/wmill_pg/dist/wmill_pg-1.245.0-py3-none-any.whl and /dev/null differ diff --git a/python-client/wmill_pg/dist/wmill_pg-1.245.0.tar.gz b/python-client/wmill_pg/dist/wmill_pg-1.245.0.tar.gz deleted file mode 100644 index e539cc5f88..0000000000 Binary files a/python-client/wmill_pg/dist/wmill_pg-1.245.0.tar.gz and /dev/null differ diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml deleted file mode 100644 index 747396e026..0000000000 --- a/python-client/wmill_pg/pyproject.toml +++ /dev/null @@ -1,48 +0,0 @@ -[tool.poetry] -name = "wmill-pg" -version = "1.646.0" -description = "An extension client for the wmill client library focused on pg" -license = "Apache-2.0" -homepage = "https://windmill.dev" -documentation = "https://windmill.dev" - -authors = ["Ruben Fiszel "] - -readme = "README.md" -packages = [ - {include = "wmill_pg"}, -] -include = ["wmill_pg/py.typed"] - -[tool.poetry.dependencies] -python = "^3.7" -wmill = "^1.5.0" -psycopg2-binary = "*" - -[build-system] -requires = ["poetry>=1.0.2", "poetry-dynamic-versioning"] -build-backend = "poetry.masonry.api" - -[tool.poetry-dynamic-versioning] -enable = true -vcs = "git" -style = "semver" -format-jinja = "{% if distance == 0 %}{{ base }}{% else %}{{ base }}+{{ distance }}.{{ commit }}{% endif %}" - - -[tool.black] -line-length = 120 -target_version = ['py36', 'py37', 'py38'] -exclude = ''' -( - /( - | \.git - | \.venv - | \.mypy_cache - )/ -) -''' - -[tool.isort] -line_length = 120 -profile = "black" diff --git a/python-client/wmill_pg/wmill_pg/__init__.py b/python-client/wmill_pg/wmill_pg/__init__.py deleted file mode 100644 index 421945bc42..0000000000 --- a/python-client/wmill_pg/wmill_pg/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .client import * diff --git a/python-client/wmill_pg/wmill_pg/client.py b/python-client/wmill_pg/wmill_pg/client.py deleted file mode 100644 index 73b5efc3fe..0000000000 --- a/python-client/wmill_pg/wmill_pg/client.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Any - -import wmill - -import psycopg2 - - -def query(query: str, connection: "str | dict[str, Any]" = "g/all/postgres") -> "list[tuple[Any, ...]] | None": - """ - Query a postgres database using psycopg2 library underneath. See its documentation for more info. - - Args: - query: The query as string, without ending ';' - resource: The path of the resource of type 'postgres' containing the connection info. - The default value is 'g/all/postgres'. It is by convention the default postgres - db of any given workspace. - - Return: - Either None if it is a non returning statement or a list of tuple for statement with return values. - """ - if isinstance(connection, str): - pg_con = wmill.get_resource(connection) - if pg_con is None: - raise Exception(f"Resource {connection} not found") - else: - pg_con = connection - - - conn = psycopg2.connect(**pg_con) - cur = conn.cursor() - cur.execute(f"{query};") - if cur.description: - res = cur.fetchall() - else: - res = None - conn.commit() - cur.close() - conn.close() - return res - diff --git a/python-client/wmill_pg/wmill_pg/py.typed b/python-client/wmill_pg/wmill_pg/py.typed deleted file mode 100644 index 7632ecf775..0000000000 --- a/python-client/wmill_pg/wmill_pg/py.typed +++ /dev/null @@ -1 +0,0 @@ -# Marker file for PEP 561 diff --git a/sandbox-image/Dockerfile.sandbox b/sandbox-image/Dockerfile.sandbox index 072095c106..7c9ca181a4 100644 --- a/sandbox-image/Dockerfile.sandbox +++ b/sandbox-image/Dockerfile.sandbox @@ -1,68 +1,46 @@ FROM debian:bookworm-slim -# ── System packages ─────────────────────────────────────────────────────────── +# ── Minimal system deps ────────────────────────────────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ - curl ca-certificates git unzip openssh-client \ - # Rust native build deps - pkg-config cmake clang mold libtool \ - libssl-dev libxml2-dev libxmlsec1-dev libxslt1-dev \ - libffi-dev zlib1g-dev libcurl4-openssl-dev libclang-dev \ - libkrb5-dev libsasl2-dev \ - # PostgreSQL - postgresql postgresql-client \ + ca-certificates curl git openssh-client sudo xz-utils \ + # PostgreSQL client (server provided by Nix profile) + postgresql-client \ && rm -rf /var/lib/apt/lists/* -# ── Node.js 22 ──────────────────────────────────────────────────────────────── -RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && rm -rf /var/lib/apt/lists/* +# ── Nix (single-user, Determinate installer) ──────────────────────────────── +RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | \ + sh -s -- install linux --no-confirm --init none +ENV PATH="/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" -# ── GitHub CLI ──────────────────────────────────────────────────────────────── -RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ - > /etc/apt/sources.list.d/github-cli.list \ - && apt-get update && apt-get install -y --no-install-recommends gh \ - && rm -rf /var/lib/apt/lists/* +# ── Install default sandbox profile ───────────────────────────────────────── +COPY flake.nix flake.lock /tmp/flake/ +RUN cd /tmp/flake && nix profile install .#sandbox \ + && rm -rf /tmp/flake -# ── Rust toolchain ──────────────────────────────────────────────────────────── -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ - sh -s -- -y --default-toolchain stable --profile minimal -ENV PATH="/root/.cargo/bin:$PATH" +# ── Browser env (Puppeteer / Mermaid point to Nix Chromium) ───────────────── +ENV PUPPETEER_SKIP_DOWNLOAD="true" +RUN printf '{"args":["--no-sandbox","--disable-setuid-sandbox"],"executablePath":"%s"}\n' \ + "$(readlink -f "$(which chromium)")" > /root/.puppeteerrc.json -RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres \ - && cargo install cargo-watch - -# ── Bun ─────────────────────────────────────────────────────────────────────── -RUN curl -fsSL https://bun.sh/install | bash -ENV PATH="/root/.bun/bin:$PATH" - -# ── Playwright + Chromium ───────────────────────────────────────────────────── -RUN bun add -g @playwright/test \ - && bunx playwright install chromium --with-deps \ - && rm -rf /var/lib/apt/lists/* /tmp/bunx-* - -# ── AWS CLI ─────────────────────────────────────────────────────────────────── -RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \ - && unzip -q /tmp/awscliv2.zip -d /tmp \ - && /tmp/aws/install \ - && rm -rf /tmp/aws /tmp/awscliv2.zip -ENV AWS_DEFAULT_REGION=auto - -# ── Claude Code ─────────────────────────────────────────────────────────────── +# ── Claude Code ────────────────────────────────────────────────────────────── RUN curl -fsSL https://claude.ai/install.sh | bash ENV PATH="/root/.local/bin:$PATH" -# ── Codex ───────────────────────────────────────────────────────────────────── +# ── npm globals (install to /usr/local so bins land on PATH) ───────────────── +ENV NPM_CONFIG_PREFIX=/usr/local RUN npm i -g @openai/codex +RUN PUPPETEER_SKIP_DOWNLOAD=true npm i -g @mermaid-js/mermaid-cli -# ── Mermaid CLI ─────────────────────────────────────────────────────────────── -# Skip puppeteer's own Chromium download; reuse the one Playwright already installed above -RUN PUPPETEER_SKIP_DOWNLOAD=true npm i -g @mermaid-js/mermaid-cli \ - && CHROMIUM=$(find /root/.cache/ms-playwright -name 'chrome' -executable -type f | head -1) \ - && printf '{"args":["--no-sandbox","--disable-setuid-sandbox"],"executablePath":"%s"}\n' "$CHROMIUM" \ - > /root/.puppeteerrc.json +# ── Runtime env ────────────────────────────────────────────────────────────── +ENV CARGO_HOME=/tmp/.cargo -# ── Entrypoint (run explicitly via docker exec, not on container start) ────── -COPY entrypoint.sh /usr/local/bin/entrypoint.sh +# ── Allow non-root UID (--user) to access tools installed in /root ─────────── +# Give UID 1000 a proper passwd entry, passwordless sudo, and full access to /root +RUN chmod -R 777 /root \ + && useradd -u 1000 -g 100 -d /root -s /bin/bash -M agent \ + && echo "agent ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/agent \ + && chmod 0440 /etc/sudoers.d/agent + +# ── Entrypoint ─────────────────────────────────────────────────────────────── +COPY sandbox-image/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh diff --git a/sandbox-image/entrypoint.sh b/sandbox-image/entrypoint.sh index 24f384bcf5..b5a0e4b14e 100644 --- a/sandbox-image/entrypoint.sh +++ b/sandbox-image/entrypoint.sh @@ -1,21 +1,31 @@ #!/bin/bash set -euo pipefail -# ── Start PostgreSQL ────────────────────────────────────────────────────────── +# ── Nix profile ────────────────────────────────────────────────────────────── +export PATH="/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" +export CARGO_HOME=/tmp/.cargo + +# ── Browser env (from Nix sandbox profile) ─────────────────────────────────── +if command -v sandbox-env >/dev/null 2>&1; then + eval "$(sandbox-env)" +fi + +# ── Start PostgreSQL as current user (no root/su needed) ───────────────────── PGDATA=/tmp/pgdata mkdir -p "$PGDATA" -chown postgres:postgres "$PGDATA" if [ ! -f "$PGDATA/PG_VERSION" ]; then - su - postgres -c "/usr/lib/postgresql/15/bin/initdb -D $PGDATA --auth=trust" + initdb -D "$PGDATA" --auth=trust fi -su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D $PGDATA -l /tmp/pg.log start -o '-k /tmp'" -su - postgres -c "psql -h /tmp -c 'CREATE ROLE root SUPERUSER LOGIN'" 2>/dev/null || true -su - postgres -c "createdb -h /tmp windmill" 2>/dev/null || true +pg_ctl -D "$PGDATA" -l /tmp/pg.log start -o "-k /tmp" + +# Create postgres role and windmill database (idempotent) +psql -h /tmp -d postgres -c "CREATE ROLE postgres SUPERUSER LOGIN" 2>/dev/null || true +createdb -h /tmp windmill 2>/dev/null || true # ── Run migrations if present ───────────────────────────────────────────────── if [ -d "$PWD/backend/migrations" ]; then - DATABASE_URL="postgres://root@localhost/windmill?host=/tmp" \ + DATABASE_URL="postgres://postgres@localhost/windmill?host=/tmp" \ sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true fi diff --git a/scripts/wm-cursor b/scripts/wm-cursor index 596d89bd3c..d2c2300fb5 100755 --- a/scripts/wm-cursor +++ b/scripts/wm-cursor @@ -10,25 +10,30 @@ setopt err_exit no_unset pipe_fail local script_path=${0:A} -# --- Resolve Cursor remote CLI (most recently modified) --- +# --- Lazy Cursor CLI resolution (only when needed) --- -local -a cursor_bins=(~/.cursor-server/cli/servers/*/server/bin/remote-cli/cursor(NOm)) -if (( ${#cursor_bins} == 0 )); then - print -u2 "Error: Cursor remote CLI not found in ~/.cursor-server/cli/servers/" - exit 1 -fi -local cursor_bin=${cursor_bins[1]} +local cursor_bin= -# --- Refresh Cursor IPC socket (tmux may hold a stale one) --- -# Multiple stale sockets may exist; probe to find a live one +resolve_cursor_cli() { + [[ -n $cursor_bin ]] && return 0 -local sock -for sock in /tmp/vscode-ipc-*.sock(NOm); do - if VSCODE_IPC_HOOK_CLI=$sock $cursor_bin --status &>/dev/null; then - export VSCODE_IPC_HOOK_CLI=$sock - break + local -a cursor_bins=(~/.cursor-server/cli/servers/*/server/bin/remote-cli/cursor(NOm)) + if (( ${#cursor_bins} == 0 )); then + print -u2 "Error: Cursor remote CLI not found in ~/.cursor-server/cli/servers/" + exit 1 fi -done + cursor_bin=${cursor_bins[1]} + + # Refresh Cursor IPC socket (tmux may hold a stale one) + # Multiple stale sockets may exist; probe to find a live one + local sock + for sock in /tmp/vscode-ipc-*.sock(NOm); do + if timeout 2 env VSCODE_IPC_HOOK_CLI=$sock $cursor_bin --status &>/dev/null; then + export VSCODE_IPC_HOOK_CLI=$sock + break + fi + done +} # --- Helper functions --- @@ -133,6 +138,7 @@ parse_features_flag() { # --- Subcommands --- cmd_add() { + resolve_cursor_cli ensure_tmux check_dev_db @@ -166,6 +172,7 @@ cmd_open() { local name=${1:?Usage: wm-cursor open } shift + resolve_cursor_cli ensure_tmux check_dev_db @@ -202,6 +209,7 @@ cmd_close() { cmd_open_ee() { local name=${1:?Usage: wm-cursor open-ee } + resolve_cursor_cli local wt_path=$(workmux path $name) local main_repo_root="$(cd "$(git -C "$wt_path" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)" diff --git a/scripts/worktree-env b/scripts/worktree-env index 0c3c56dea3..c2252754fa 100755 --- a/scripts/worktree-env +++ b/scripts/worktree-env @@ -88,6 +88,15 @@ if [[ -n "$main_repo_root" && -d "$main_repo_root/frontend/node_modules" ]]; the echo "Copied frontend/node_modules (with symlinks preserved)" fi +# --- Install cli deps and generate client --- +if [[ -n "$main_repo_root" && -d "$main_repo_root/cli/node_modules" ]]; then + cp -a "$main_repo_root/cli/node_modules" cli/ + echo "Copied cli/node_modules (with symlinks preserved)" +fi +(cd cli && npm install && npm run gen-client) \ + && echo "CLI deps installed and client generated" \ + || echo "WARNING: CLI setup failed" >&2 + # --- Allow direnv so the nix devshell activates in pane commands --- if command -v direnv &>/dev/null && [ -f .envrc ]; then direnv allow diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 3307e3f9a9..e3289413bd 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.646.0", + "version": "1.647.1", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 44e77802ec..68658964d2 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.646.0", + "version": "1.647.1", "author": "Ruben Fiszel", "license": "Apache 2.0", "sideEffects": false, diff --git a/version.txt b/version.txt index 0cdcc52abb..dc81c1ee46 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.646.0 +1.647.1