Compare commits

...

9 Commits

Author SHA1 Message Date
Alexander Petric
c9eca26316 path fix for log endpoint 2026-01-11 02:55:12 +00:00
Diego Imbert
f0fd1c5e1a Improve alter table query speed + duckdb nits (#7538)
* Fetch alter table metadata much faster

* Upgrade duckdb to 1.4.3

* Disable transactional DDL for Ducklake (bug on their side)
2026-01-10 01:24:02 +01:00
Ruben Fiszel
ecb8015d6c chore(main): release 1.603.2 (#7537)
* chore(main): release 1.603.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-01-09 20:10:47 +01:00
Alexander Petric
35ddfc428d fix: windmill ee full cache permission issues for non root users (#7536) 2026-01-09 20:03:46 +01:00
Ruben Fiszel
98c073bfaa chore(main): release 1.603.1 (#7535)
* chore(main): release 1.603.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-01-09 17:38:24 +00:00
Ruben Fiszel
57fd32b11d fix improve tags sensitivity behavior for certain backend apis 2026-01-09 17:32:44 +00:00
Diego Imbert
17d29cd8c7 fix: Better workspace storage settings (#7533)
* Better Workspace Storage settings

* nit

* super nit

* Permission settings in modal

* badge indicator

* nit width
2026-01-09 15:16:56 +00:00
Diego Imbert
7b19ca44a3 fix: Fix custom instance user migration (#7534) 2026-01-09 14:57:15 +00:00
centdix
3939e96c71 internal: better claude (#7530)
* better claude

* symlink hooks
2026-01-09 11:48:38 +00:00
42 changed files with 1759 additions and 412 deletions

View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Resolve _ee.rs symlinks to actual files so Claude can read them
# This script runs before each user prompt is processed
set -e
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
# Find all _ee.rs symlinks and store their targets
find "$PROJECT_DIR" -name "*_ee.rs" -type l 2>/dev/null | while read -r symlink; do
target=$(readlink -f "$symlink" 2>/dev/null) || continue
# Only process if target file exists
if [[ -f "$target" ]]; then
# Store symlink path and target in manifest
echo "$symlink|$target" >> "$MANIFEST_FILE.tmp"
# Replace symlink with actual file content
rm "$symlink"
cp "$target" "$symlink"
fi
done
# Atomically replace manifest
if [[ -f "$MANIFEST_FILE.tmp" ]]; then
mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
fi
exit 0

View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Restore _ee.rs symlinks after Claude finishes processing
# This script runs when Claude stops
# IMPORTANT: Copies any modifications back to the target before restoring symlinks
set -e
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
# Check if manifest exists
if [[ ! -f "$MANIFEST_FILE" ]]; then
exit 0
fi
# Read manifest and restore symlinks
while IFS='|' read -r symlink target; do
if [[ -n "$symlink" && -n "$target" ]]; then
# If the file exists (not a symlink) and target exists, copy changes back
if [[ -f "$symlink" && ! -L "$symlink" && -e "$target" ]]; then
# Copy the potentially modified file back to the target
cp "$symlink" "$target"
fi
# Remove the regular file (which was a copy)
rm -f "$symlink" 2>/dev/null || true
# Recreate the symlink
ln -s "$target" "$symlink" 2>/dev/null || true
fi
done < "$MANIFEST_FILE"
# Clean up manifest
rm -f "$MANIFEST_FILE"
exit 0

View File

@@ -1,7 +1,41 @@
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/resolve-symlinks.sh",
"timeout": 30
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
"timeout": 30
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
"timeout": 30
}
]
}
]
},
"permissions": {
"allow": [
"Read(**/*.rs)",
"Bash(ls:*)",
"Bash(grep:*)",
"Bash(cat:*)",
@@ -56,10 +90,11 @@
"Bash(git checkout:*)",
"Bash(git merge:*)",
"Bash(git rebase:*)"
],
"additionalDirectories": [
"../windmill-ee-private/"
]
]
},
"enableAllProjectMcpServers": true
"enableAllProjectMcpServers": true,
"enabledPlugins": {
"rust-analyzer-lsp@claude-plugins-official": true,
"typescript-lsp@claude-plugins-official": true
}
}

View File

@@ -49,9 +49,9 @@ jobs:
needs.check-membership.outputs.is_member == 'true'
runs-on: ubicloud-standard-8
permissions:
contents: read
contents: write
pull-requests: write
issues: read
issues: write
id-token: write
steps:
- name: Checkout repository
@@ -63,16 +63,15 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
model: claude-opus-4-1-20250805
fallback_model: claude-sonnet-4-20250514
timeout_minutes: "60"
allowed_tools: "mcp__github__create_pull_request"
allowed_bots: "windmill-internal-app[bot]"
custom_instructions: |
## IMPORTANT INSTRUCTIONS
- Your branch name should be a short description of the requested changes.
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.
## Available Tools
- mcp__github__create_pull_request: Create PRs from branches
trigger_phrase: "/ai-fast"
plugins: "rust-analyzer-lsp@claude-plugins-official,typescript-lsp@claude-plugins-official"
settings: |
{
"env": {
"SQLX_OFFLINE": "true"
}
}
claude_args: |
--allowedTools "Bash,WebFetch,WebSearch"
--model opus

View File

@@ -50,9 +50,9 @@ jobs:
runs-on: ubicloud-standard-8
timeout-minutes: 60
permissions:
contents: read
contents: write
pull-requests: write
issues: read
issues: write
id-token: write
steps:
- name: Checkout repository
@@ -95,8 +95,9 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: 'windmill-internal-app[bot]'
trigger_phrase: '/ai'
allowed_bots: "windmill-internal-app[bot]"
trigger_phrase: "/ai"
plugins: "rust-analyzer-lsp@claude-plugins-official,typescript-lsp@claude-plugins-official"
settings: |
{
"env": {

View File

@@ -1,5 +1,20 @@
# Changelog
## [1.603.2](https://github.com/windmill-labs/windmill/compare/v1.603.1...v1.603.2) (2026-01-09)
### Bug Fixes
* windmill ee full cache permission issues for non root users ([#7536](https://github.com/windmill-labs/windmill/issues/7536)) ([35ddfc4](https://github.com/windmill-labs/windmill/commit/35ddfc428dc98e492012731f60feda64ff5ebc2c))
## [1.603.1](https://github.com/windmill-labs/windmill/compare/v1.603.0...v1.603.1) (2026-01-09)
### Bug Fixes
* Better workspace storage settings ([#7533](https://github.com/windmill-labs/windmill/issues/7533)) ([17d29cd](https://github.com/windmill-labs/windmill/commit/17d29cd8c770fbe1f7503367474951e5eb6991b1))
* Fix custom instance user migration ([#7534](https://github.com/windmill-labs/windmill/issues/7534)) ([7b19ca4](https://github.com/windmill-labs/windmill/commit/7b19ca44a3ff7e2e87d5a358873370aeb40dc7a3))
## [1.603.0](https://github.com/windmill-labs/windmill/compare/v1.602.0...v1.603.0) (2026-01-09)

58
backend/Cargo.lock generated
View File

@@ -15189,7 +15189,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15252,7 +15252,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"argon2",
@@ -15374,7 +15374,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -15384,7 +15384,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"chrono",
"lazy_static",
@@ -15398,7 +15398,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"axum",
@@ -15417,7 +15417,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -15511,7 +15511,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"regex",
"serde",
@@ -15526,7 +15526,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -15550,7 +15550,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15566,7 +15566,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"reqwest 0.12.28",
@@ -15579,7 +15579,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"async-oauth2",
@@ -15603,7 +15603,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15612,7 +15612,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15624,7 +15624,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"serde_json",
@@ -15636,7 +15636,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"gosyn",
@@ -15648,7 +15648,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15660,7 +15660,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"serde_json",
@@ -15672,7 +15672,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"nu-parser",
@@ -15683,7 +15683,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15694,7 +15694,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15707,7 +15707,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -15731,7 +15731,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15745,7 +15745,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15762,7 +15762,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15776,7 +15776,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15795,7 +15795,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"serde",
@@ -15806,7 +15806,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -15843,7 +15843,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15853,7 +15853,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.603.0"
version = "1.603.2"
dependencies = [
"anyhow",
"async-once-cell",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.603.0"
version = "1.603.2"
authors.workspace = true
edition.workspace = true
@@ -35,7 +35,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.603.0"
version = "1.603.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,17 @@
DO $$
DECLARE
dbname text := current_database();
BEGIN
-- Revoke default privileges first
ALTER DEFAULT PRIVILEGES IN SCHEMA public
REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM custom_instance_user;
REVOKE CREATE ON SCHEMA public FROM custom_instance_user;
REVOKE USAGE ON SCHEMA public FROM custom_instance_user;
EXECUTE format('REVOKE CREATE ON DATABASE %I FROM custom_instance_user', dbname);
EXECUTE format('REVOKE CONNECT ON DATABASE %I FROM custom_instance_user', dbname);
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Error in custom_instance_user migration: %', SQLERRM;
-- Continue without failing the migration
END
$$;

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.603.0
version: 1.603.2
title: Windmill API
contact:
@@ -13532,6 +13532,12 @@ paths:
description: comma separated list of tags
schema:
type: string
- name: workspace
in: query
required: false
description: workspace to filter tags visibility (required when TAGS_ARE_SENSITIVE is enabled for non-superadmins)
schema:
type: string
responses:
"200":
description: map of tags to whether at least one worker with the tag exists

View File

@@ -56,6 +56,7 @@ lazy_static::lazy_static! {
(20251105100125, include_str!(
"../../migrations/20251105100125_legacy_sql_result_flag.up.sql"
).replace("", "")),
(20260107133344, "".to_string()),
].into_iter().collect();
}

View File

@@ -45,7 +45,7 @@ use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat};
use windmill_common::scripts::ScriptRunnableSettingsInline;
use windmill_common::triggers::TriggerMetadata;
use windmill_common::utils::{RunnableKind, WarnAfterExt};
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
use windmill_common::worker::{is_allowed_file_location, Connection, CLOUD_HOSTED, TMP_DIR};
use windmill_common::workspace_dependencies::{
RawWorkspaceDependencies, MIN_VERSION_WORKSPACE_DEPENDENCIES,
};
@@ -7235,7 +7235,10 @@ impl Hash for JobUpdate {
}
async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::Result<Response> {
let local_file = format!("{TMP_DIR}/logs/{file_p}");
// Validate path to prevent directory traversal attacks (CVE pending)
let logs_dir = format!("{TMP_DIR}/logs");
let local_file = is_allowed_file_location(&logs_dir, &file_p)
.map_err(|_| error::Error::BadRequest("Invalid file path".to_string()))?;
if tokio::fs::metadata(&local_file).await.is_ok() {
let mut file = tokio::fs::File::open(local_file).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
@@ -7274,7 +7277,8 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
}
} else {
return Err(error::Error::internal_err(format!(
"Object store client not present and file not found on server logs volume at {local_file}"
"Object store client not present and file not found on server logs volume at {}",
local_file.display()
)));
}

View File

@@ -18,6 +18,7 @@ use uuid::Uuid;
use windmill_common::{
db::UserDB,
error::JsonResult,
jobs::TAGS_ARE_SENSITIVE,
utils::{paginate, Pagination},
worker::{ALL_TAGS, CUSTOM_TAGS_PER_WORKSPACE, DEFAULT_TAGS, DEFAULT_TAGS_PER_WORKSPACE},
DB,
@@ -112,28 +113,64 @@ async fn list_worker_pings(
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
let rows = if *TAGS_ARE_SENSITIVE && !is_super_admin {
rows.into_iter()
.map(|mut w| {
w.custom_tags = None;
w
})
.collect()
} else {
rows
};
Ok(Json(rows))
}
#[derive(Serialize, Deserialize)]
struct TagsQuery {
tags: String,
workspace: Option<String>,
}
async fn exists_workers_with_tags(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Query(tags_query): Query<TagsQuery>,
) -> JsonResult<std::collections::HashMap<String, bool>> {
// Create a list of requested tags
let mut tags: Vec<String> = tags_query
.tags
.split(',')
.map(|s| s.to_string())
.collect();
// When TAGS_ARE_SENSITIVE is enabled, filter tags based on workspace visibility
if *TAGS_ARE_SENSITIVE {
let is_super_admin = require_super_admin(&db, &authed.email).await.is_ok();
if !is_super_admin {
if let Some(ref workspace) = tags_query.workspace {
// Filter to only tags visible in this workspace
let custom_tags = CUSTOM_TAGS_PER_WORKSPACE.read().await;
let allowed_tags = custom_tags.to_string_vec(Some(workspace.clone()));
tags.retain(|t| allowed_tags.contains(t));
} else {
// No workspace provided and not superadmin - return empty
return Ok(Json(std::collections::HashMap::new()));
}
}
}
if tags.is_empty() {
return Ok(Json(std::collections::HashMap::new()));
}
let mut tx = user_db.begin(&authed).await?;
let mut result = std::collections::HashMap::new();
// Create a query that checks all tags at once using unnest
let tags = tags_query
.tags
.split(',')
.map(|s| s.to_string())
.collect::<Vec<String>>();
let rows = sqlx::query!(
"SELECT tag::text, EXISTS(SELECT 1 FROM worker_ping WHERE custom_tags @> ARRAY[tag] AND ping_at > now() - interval '1 minute') as exists
FROM unnest($1::text[]) as tag",
@@ -155,7 +192,11 @@ struct CustomTagQuery {
workspace: Option<String>,
show_workspace_restriction: Option<bool>,
}
async fn get_custom_tags(Query(query): Query<CustomTagQuery>) -> JsonResult<Vec<String>> {
async fn get_custom_tags(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Query(query): Query<CustomTagQuery>,
) -> JsonResult<Vec<String>> {
if query.show_workspace_restriction.is_some_and(|x| x) && query.workspace.is_some() {
return Err(windmill_common::error::Error::BadRequest(
"Cannot use both workspace and show_workspace_restriction".to_string(),
@@ -170,6 +211,12 @@ async fn get_custom_tags(Query(query): Query<CustomTagQuery>) -> JsonResult<Vec<
let all_tags = tags_o.to_string_vec(None);
return Ok(Json(all_tags));
}
if *TAGS_ARE_SENSITIVE {
let is_super_admin = require_super_admin(&db, &authed.email).await.is_ok();
if !is_super_admin {
return Ok(Json(vec![]));
}
}
Ok(Json(ALL_TAGS.read().await.clone().into()))
}

View File

@@ -822,7 +822,7 @@ pub async fn get_logs_from_store(
}
lazy_static::lazy_static! {
static ref TAGS_ARE_SENSITIVE: bool = std::env::var("TAGS_ARE_SENSITIVE").map(
pub static ref TAGS_ARE_SENSITIVE: bool = std::env::var("TAGS_ARE_SENSITIVE").map(
|v| v.parse().unwrap()
).unwrap_or(false);
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ edition = "2024"
[dependencies]
chrono = "0.4.41"
duckdb = { rev = "fe0702529de6ec5a568337726bba9355503157d2", git = "https://github.com/windmill-labs/duckdb-rs.git", features = ["bundled"] }
duckdb = { version = "1.4.3", features = ["bundled"] }
rust_decimal = "1.37.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }

View File

@@ -267,11 +267,7 @@ fn do_duckdb_inner(
(0..stmt.column_count())
.map(|i| {
let logical_type = stmt.column_logical_type(i);
if logical_type.is_invalid() {
None
} else {
logical_type.get_alias()
}
logical_type.get_alias()
})
.collect::<Vec<_>>(),
);

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.603.0";
export const VERSION = "v1.603.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -70,7 +70,7 @@ export {
// }
// });
export const VERSION = "1.603.0";
export const VERSION = "1.603.2";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -26,3 +26,8 @@ RUN /usr/bin/java -jar /usr/bin/coursier about
# Ruby
RUN apt-get install -y ruby ruby-bundler
# Fix UV cache permissions for non-root user support (uid 1000, etc.)
# The uv tool install ansible command populates the UV cache with root-owned files
RUN chmod -R a+rw /tmp/windmill/cache/uv && \
find /tmp/windmill/cache/uv -type d -exec chmod 777 {} +

View File

@@ -50,3 +50,8 @@ RUN /usr/bin/java -jar /usr/bin/coursier about
# Ruby
RUN apt-get install -y ruby ruby-bundler
# Fix UV cache permissions for non-root user support (uid 1000, etc.)
# The uv tool install ansible command populates the UV cache with root-owned files
RUN chmod -R a+rw /tmp/windmill/cache/uv && \
find /tmp/windmill/cache/uv -type d -exec chmod 777 {} +

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.603.0",
"version": "1.603.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.603.0",
"version": "1.603.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.603.0",
"version": "1.603.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -75,7 +75,10 @@
clearTimeout(timeout)
}
if (open) {
tagsToWorkerExists = await WorkerService.existsWorkersWithTags({ tags: tags.join(',') })
tagsToWorkerExists = await WorkerService.existsWorkersWithTags({
tags: tags.join(','),
workspace: $workspaceStore
})
lastCheck = Date.now()
if (visible) {
timeout = setTimeout(() => {

View File

@@ -476,5 +476,9 @@ function normalizeNewFkToOldColNames(
}
export function dbSupportsTransactionalDdl(dbType: DbType): boolean {
return dbType === 'postgresql' || dbType === 'ms_sql_server' || dbType === 'duckdb'
return dbType === 'postgresql' || dbType === 'ms_sql_server'
// Commented out temporarily because Ducklake transactional DDL sometimes put the
// ducklake in an unusable state.
// See : https://github.com/duckdb/ducklake/issues/683
// || dbType === 'duckdb'
}

View File

@@ -3,6 +3,7 @@ import { wrapDucklakeQuery } from '$lib/components/ducklake'
import { runScriptAndPollResult } from '$lib/components/jobs/utils'
import type { ScriptLang } from '$lib/gen'
import type { TableEditorForeignKey } from '../tableEditor'
import type { TableMetadata } from '../utils'
/**
* Raw foreign key result from database queries
@@ -402,57 +403,74 @@ export async function fetchTableRelationalKeys(
schema: string | undefined,
workspace: string,
dbArg: Record<string, any>,
language: ScriptLang
): Promise<{ foreignKeys: TableEditorForeignKey[]; pk_constraint_name?: string }> {
let foreignKeys: TableEditorForeignKey[] = []
let pk_constraint_name: string | undefined = undefined
try {
if (dbType !== 'bigquery') {
let fkQuery = makeForeignKeysQuery(dbType, table, schema)
if (input.type === 'ducklake') fkQuery = wrapDucklakeQuery(fkQuery, input.ducklake)
language: ScriptLang,
getColDefs: () => Promise<TableMetadata>
): Promise<{
foreignKeys: TableEditorForeignKey[]
pk_constraint_name?: string
colDefs: TableMetadata
}> {
let fkPromise = async () => {
try {
if (dbType !== 'bigquery') {
let fkQuery = makeForeignKeysQuery(dbType, table, schema)
if (input.type === 'ducklake') fkQuery = wrapDucklakeQuery(fkQuery, input.ducklake)
const fkResult = await runScriptAndPollResult({
workspace,
requestBody: { args: dbArg, content: fkQuery, language }
})
let rawForeignKeys = fkResult as RawForeignKey[]
if (rawForeignKeys && Array.isArray(rawForeignKeys)) {
// Lowercase keys for consistency
rawForeignKeys = rawForeignKeys.map((fk) => {
const lowerFk: any = {}
Object.keys(fk).forEach((key) => {
lowerFk[key.toLowerCase()] = fk[key]
})
return lowerFk
const fkResult = await runScriptAndPollResult({
workspace,
requestBody: { args: dbArg, content: fkQuery, language }
})
foreignKeys = transformForeignKeys(rawForeignKeys)
let rawForeignKeys = fkResult as RawForeignKey[]
if (rawForeignKeys && Array.isArray(rawForeignKeys)) {
// Lowercase keys for consistency
rawForeignKeys = rawForeignKeys.map((fk) => {
const lowerFk: any = {}
Object.keys(fk).forEach((key) => {
lowerFk[key.toLowerCase()] = fk[key]
})
return lowerFk
})
return transformForeignKeys(rawForeignKeys)
}
}
} catch (e) {
console.warn('Failed to fetch foreign keys:', e)
}
} catch (e) {
console.warn('Failed to fetch foreign keys:', e)
return []
}
try {
if (dbType !== 'bigquery' && dbType !== 'mysql') {
let pkQuery = makePrimaryKeyConstraintQuery(dbType, table, schema)
if (input.type === 'ducklake') pkQuery = wrapDucklakeQuery(pkQuery, input.ducklake)
let pkPromise = async () => {
try {
if (dbType !== 'bigquery' && dbType !== 'mysql') {
let pkQuery = makePrimaryKeyConstraintQuery(dbType, table, schema)
if (input.type === 'ducklake') pkQuery = wrapDucklakeQuery(pkQuery, input.ducklake)
const pkResult = await runScriptAndPollResult({
workspace,
requestBody: { args: dbArg, content: pkQuery, language }
})
const pkResult = await runScriptAndPollResult({
workspace,
requestBody: { args: dbArg, content: pkQuery, language }
})
let rawPkResult = pkResult as RawPrimaryKeyConstraint[]
let rawPkResult = pkResult as RawPrimaryKeyConstraint[]
if (rawPkResult && Array.isArray(rawPkResult) && rawPkResult.length > 0) {
const pkRecord: any = rawPkResult[0]
pk_constraint_name = pkRecord?.constraint_name || pkRecord?.CONSTRAINT_NAME || ''
if (rawPkResult && Array.isArray(rawPkResult) && rawPkResult.length > 0) {
const pkRecord: any = rawPkResult[0]
const pk_constraint_name: string =
pkRecord?.constraint_name || pkRecord?.CONSTRAINT_NAME || ''
return pk_constraint_name
}
}
} catch (e) {
console.warn('Failed to fetch primary key constraint:', e)
}
} catch (e) {
console.warn('Failed to fetch primary key constraint:', e)
}
return { foreignKeys, pk_constraint_name }
const [foreignKeys, pk_constraint_name, colDefs] = await Promise.all([
fkPromise(),
pkPromise(),
getColDefs()
])
return { foreignKeys, pk_constraint_name, colDefs }
}

View File

@@ -191,15 +191,15 @@ export function dbSchemaOpsWithPreviewScripts({
})
},
onFetchTableEditorDefinition: async ({ table, schema, getColDefs }) => {
let colDefs = await getColDefs()
let { foreignKeys, pk_constraint_name } = await fetchTableRelationalKeys(
let { foreignKeys, pk_constraint_name, colDefs } = await fetchTableRelationalKeys(
input,
dbType,
table,
schema,
workspace,
dbArg,
language
language,
getColDefs
)
return buildTableEditorValues({

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { WorkerService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { AlertTriangle } from 'lucide-svelte'
import Popover from '../Popover.svelte'
import { onDestroy, untrack } from 'svelte'
@@ -28,7 +29,10 @@
async function lookForTag(): Promise<void> {
try {
if (!customTag) return
const existsWorkerWithTag = await WorkerService.existsWorkersWithTags({ tags: customTag })
const existsWorkerWithTag = await WorkerService.existsWorkersWithTags({
tags: customTag,
workspace: $workspaceStore
})
noWorkerWithTag = !existsWorkerWithTag[customTag]
if (noWorkerWithTag) {
timeout = setTimeout(() => {

View File

@@ -35,6 +35,7 @@
neverShowLoader?: boolean
loading?: boolean
loadingMore?: boolean
containerClass?: string
children?: import('svelte').Snippet
emptyMessage?: import('svelte').Snippet
}
@@ -59,6 +60,7 @@
neverShowLoader = false,
loading = false,
loadingMore = false,
containerClass = '',
children,
emptyMessage
}: Props = $props()
@@ -119,7 +121,8 @@
class={twMerge(
'h-full',
rounded ? 'rounded-md overflow-hidden' : '',
noBorder ? 'border-0' : 'border'
noBorder ? 'border-0' : 'border',
containerClass
)}
bind:clientHeight={tableHeight}
>

View File

@@ -352,18 +352,12 @@
class="cursor-not-allowed"
>
<svelte:fragment slot="trigger">
<ExploreAssetButton
class="h-9"
asset={{ kind: 'ducklake', path: ducklake.name }}
{dbManagerDrawer}
disabled
/>
<ExploreAssetButton asset={{ kind: 'ducklake', path: '' }} disabled />
</svelte:fragment>
<svelte:fragment slot="content">Please save settings first</svelte:fragment>
</Popover>
{:else}
<ExploreAssetButton
class="h-9"
asset={{ kind: 'ducklake', path: ducklake.name }}
{dbManagerDrawer}
/>

View File

@@ -1,11 +1,9 @@
<script lang="ts">
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { emptyString, sendUserToast } from '$lib/utils'
import { emptyString, pick, sendUserToast } from '$lib/utils'
import { ChevronDown, Plus, Shield } from 'lucide-svelte'
import Alert from '../common/alert/Alert.svelte'
import Button from '../common/button/Button.svelte'
import Tab from '../common/tabs/Tab.svelte'
import Tabs from '../common/tabs/Tabs.svelte'
import Description from '../Description.svelte'
import ResourcePicker from '../ResourcePicker.svelte'
import Toggle from '../Toggle.svelte'
@@ -25,11 +23,27 @@
import CloseButton from '../common/CloseButton.svelte'
import TextInput from '../text_input/TextInput.svelte'
import Select from '../select/Select.svelte'
import DataTable from '../table/DataTable.svelte'
import Head from '../table/Head.svelte'
import Cell from '../table/Cell.svelte'
import Row from '../table/Row.svelte'
import { deepEqual } from 'fast-equals'
import ExploreAssetButton from '../ExploreAssetButton.svelte'
import Modal2 from '../common/modal/Modal2.svelte'
let {
s3ResourceSettings = $bindable(),
s3ResourceSavedSettings,
onSave = undefined
}: { s3ResourceSettings: S3ResourceSettings; onSave?: () => void } = $props()
}: {
s3ResourceSettings: S3ResourceSettings
s3ResourceSavedSettings: S3ResourceSettings
onSave?: () => void
} = $props()
let advancedPermissionModalState:
| { open: false }
| { open: true; storage: S3ResourceSettingsItem } = $state({ open: false })
let s3FileViewer: S3FilePicker | undefined = $state()
@@ -45,6 +59,42 @@
sendUserToast(`Large file storage settings changed`)
onSave?.()
}
let tableHeadNames = ['Name', 'Storage resource', '', ''] as const
let tableHeadTooltips: Partial<Record<(typeof tableHeadNames)[number], string | undefined>> = {
'Storage resource':
'Which resource the workspace storage will point to. Note that all users of the workspace will be able to access the workspace storage regardless of the resource visibility.'
}
let tableRows: [string | null, S3ResourceSettingsItem][] = $derived([
[null, s3ResourceSettings],
...(s3ResourceSettings.secondaryStorage ?? [])
])
let secondaryStorageIsDirty: Record<string, boolean> = $derived(
Object.fromEntries(
s3ResourceSettings.secondaryStorage?.map((d) => {
const saved = s3ResourceSavedSettings.secondaryStorage?.find((saved) => saved[0] === d[0])
return [d[0], !deepEqual(saved?.[1], d[1])] as const
}) ?? []
)
)
let primaryStorageIsDirty: boolean = $derived.by(() => {
const fields = [
'resourcePath',
'resourceType',
'publicResource',
'advancedPermissions'
] as const
return !deepEqual(pick(s3ResourceSavedSettings, fields), pick(s3ResourceSettings, fields))
})
function isDirty(name: string | null): boolean {
return name === null ? primaryStorageIsDirty : secondaryStorageIsDirty[name]
}
function isPermissionsNonDefault(storage: S3ResourceSettingsItem): boolean {
const defaultPerms = defaultS3AdvancedPermissions(!!$enterpriseLicense)
return !deepEqual(storage.advancedPermissions, defaultPerms)
}
</script>
<Portal name="workspace-settings">
@@ -82,140 +132,145 @@
</Alert>
{/if}
{#if s3ResourceSettings}
<div class="mt-5">
<div class="w-full">
<!-- this can be removed once parent moves to runes -->
<!-- svelte-ignore binding_property_non_reactive -->
<Tabs bind:selected={s3ResourceSettings.resourceType}>
<Tab exact label="S3" value="s3" />
<Tab value="azure_blob" label="Azure Blob" />
<Tab exact value="s3_aws_oidc" label="AWS OIDC" />
<Tab value="azure_workload_identity" label="Azure Workload Identity" />
<Tab exact value="gcloud_storage" label="Google Cloud Storage" />
</Tabs>
</div>
<div class="w-full flex gap-1 mt-4 whitespace-nowrap">
<!-- this can be removed once parent moves to runes -->
<!-- svelte-ignore binding_property_non_reactive -->
<ResourcePicker
resourceType={s3ResourceSettings.resourceType}
bind:value={s3ResourceSettings.resourcePath}
/>
{@render permissionBtn(s3ResourceSettings)}
<Button
size="sm"
variant="accent"
disabled={emptyString(s3ResourceSettings.resourcePath)}
on:click={async () => {
if ($workspaceStore) {
s3FileViewer?.open?.(undefined)
}
}}>Browse content (save first)</Button
>
</div>
</div>
<DataTable containerClass="mt-4">
<Head>
<tr>
{#each tableHeadNames as name, i}
<Cell head first={i == 0} last={i == tableHeadNames.length - 1}>
{name}
{#if tableHeadTooltips[name]}
<Tooltip>{@html tableHeadTooltips[name]}</Tooltip>
{/if}
</Cell>
{/each}
</tr>
</Head>
<tbody class="divide-y bg-surface">
{#each tableRows as tableRow, idx}
<Row>
<Cell first class="w-48 relative">
{#if tableRow[0] === null}
<TextInput inputProps={{ placeholder: 'Primary storage', disabled: true }} />
{:else}
<TextInput bind:value={tableRow[0]} inputProps={{ placeholder: 'Name' }} />
{/if}
</Cell>
<Cell>
<div class="flex gap-2">
<div class="relative">
<Select
items={[
{ value: 's3', label: 'S3' },
{ value: 'azure_blob', label: 'Azure Blob' },
{ value: 's3_aws_oidc', label: 'AWS OIDC' },
{ value: 'azure_workload_identity', label: 'Azure Workload Identity' },
{ value: 'gcloud_storage', label: 'Google Cloud Storage' }
]}
bind:value={tableRow[1].resourceType}
class="w-40"
/>
</div>
<div class="flex flex-1">
<ResourcePicker
class="flex-1"
bind:value={tableRow[1].resourcePath}
resourceType={tableRow[1].resourceType}
/>
</div>
</div>
</Cell>
<div class="mt-6">
<div class="flex mt-2 flex-col gap-y-4 max-w-5xl">
{#each s3ResourceSettings.secondaryStorage ?? [] as _, idx}
<div class="flex gap-1 relative whitespace-nowrap">
<TextInput
class="max-w-[200px]"
inputProps={{ type: 'text', placeholder: 'Storage name' }}
bind:value={
() => s3ResourceSettings.secondaryStorage?.[idx]?.[0] || '',
(v) => {
if (s3ResourceSettings.secondaryStorage?.[idx]) {
s3ResourceSettings.secondaryStorage[idx][0] = v
}
}
}
/>
<Select
class="max-w-[125px]"
inputClass="h-full"
bind:value={
() => s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourceType || 's3',
(v) => {
if (s3ResourceSettings.secondaryStorage?.[idx]) {
s3ResourceSettings.secondaryStorage[idx][1].resourceType = v
}
}
}
items={[
{ value: 's3', label: 'S3' },
{ value: 'azure_blob', label: 'Azure Blob' },
{ value: 's3_aws_oidc', label: 'AWS OIDC' },
{ value: 'azure_workload_identity', label: 'Azure Workload Identity' },
{ value: 'gcloud_storage', label: 'Google Cloud Storage' }
]}
/>
<ResourcePicker
resourceType={s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourceType || 's3'}
bind:value={
() => s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourcePath || undefined,
(v) => {
if (s3ResourceSettings.secondaryStorage?.[idx]) {
s3ResourceSettings.secondaryStorage[idx][1].resourcePath = v
}
}
}
/>
{@render permissionBtn(s3ResourceSettings.secondaryStorage![idx][1])}
<Button
size="sm"
variant="accent"
disabled={emptyString(s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourcePath)}
on:click={async () => {
if ($workspaceStore) {
s3FileViewer?.open?.({
s3: '',
storage: s3ResourceSettings.secondaryStorage?.[idx]?.[0] || ''
})
}
}}>Browse content (save first)</Button
>
<CloseButton
class="my-auto"
small
on:close={() => {
if (s3ResourceSettings.secondaryStorage) {
s3ResourceSettings.secondaryStorage.splice(idx, 1)
s3ResourceSettings.secondaryStorage = [...s3ResourceSettings.secondaryStorage]
}
}}
/>
</div>
<Cell class="w-12">
<div class="flex gap-2">
<Button
variant="default"
btnClasses="px-2.5 relative"
size="sm"
onClick={() =>
(advancedPermissionModalState = { open: true, storage: tableRow[1] })}
>
<Shield size={16} /> Permissions <ChevronDown size={14} />
{#if isPermissionsNonDefault(tableRow[1])}
<span class="absolute -top-0.5 -right-0.5 h-1.5 w-1.5 rounded-full bg-accent"
></span>
{/if}
</Button>
{#if emptyString(tableRow[1].resourcePath) || isDirty(tableRow[0])}
<Popover
openOnHover
contentClasses="p-2 text-sm text-secondary italic"
class="cursor-not-allowed"
>
<svelte:fragment slot="trigger">
<ExploreAssetButton asset={{ kind: 's3object', path: '' }} disabled />
</svelte:fragment>
<svelte:fragment slot="content">
{#if emptyString(tableRow[1].resourcePath)}
Please select a storage resource
{:else if isDirty(tableRow[0])}
Please save your changes
{/if}
</svelte:fragment>
</Popover>
{:else}
<ExploreAssetButton
asset={{ kind: 's3object', path: (tableRow[0] ?? '') + '/' }}
s3FilePicker={s3FileViewer}
/>
{/if}
</div>
</Cell>
<Cell class="w-12">
{#if tableRow[0] !== null}
<CloseButton
small
on:close={() => {
if (s3ResourceSettings.secondaryStorage) {
s3ResourceSettings.secondaryStorage.splice(idx - 1, 1)
s3ResourceSettings.secondaryStorage = [...s3ResourceSettings.secondaryStorage]
}
}}
/>
{/if}
</Cell>
</Row>
{/each}
<div class="flex gap-1">
<Button
size="xs"
variant="default"
on:click={() => {
if (s3ResourceSettings.secondaryStorage === undefined) {
s3ResourceSettings.secondaryStorage = []
}
s3ResourceSettings.secondaryStorage.push([
`storage_${s3ResourceSettings.secondaryStorage.length + 1}`,
{
resourcePath: '',
resourceType: 's3',
publicResource: false,
advancedPermissions: defaultS3AdvancedPermissions(!!$enterpriseLicense)
}
])
s3ResourceSettings.secondaryStorage = s3ResourceSettings.secondaryStorage
}}><Plus size={14} />Add secondary storage</Button
>
<Tooltip>
Secondary storage is a feature that allows you to read and write from storage that isn't
your main storage by specifying it in the s3 object as "secondary_storage" with the name
of it
</Tooltip>
</div>
</div>
</div>
<Row class="!border-0">
<Cell colspan={tableHeadNames.length} class="pt-0 pb-2">
<div class="flex justify-center">
<Button
size="sm"
btnClasses="max-w-fit"
variant="default"
on:click={() => {
if (s3ResourceSettings.secondaryStorage === undefined) {
s3ResourceSettings.secondaryStorage = []
}
s3ResourceSettings.secondaryStorage.push([
`storage_${s3ResourceSettings.secondaryStorage.length + 1}`,
{
resourcePath: '',
resourceType: 's3',
publicResource: false,
advancedPermissions: defaultS3AdvancedPermissions(!!$enterpriseLicense)
}
])
s3ResourceSettings.secondaryStorage = s3ResourceSettings.secondaryStorage
}}
>
<Plus /> Add secondary storage
<Tooltip>
Secondary storage is a feature that allows you to read and write from storage that
isn't your main storage by specifying it in the s3 object as "secondary_storage"
with the name of it
</Tooltip>
</Button>
</div>
</Cell>
</Row>
</tbody>
</DataTable>
<div class="flex mt-5 mb-5 gap-1">
<Button
variant="accent"
@@ -228,100 +283,98 @@
</div>
{/if}
{#snippet permissionBtn(storage: NonNullable<S3ResourceSettings['secondaryStorage']>[number][1])}
<Popover closeOnOtherPopoverOpen placement="left">
<svelte:fragment slot="trigger">
<Button variant="default" wrapperClasses="h-full" btnClasses="px-2.5" size="sm">
<Shield size={16} /> Permissions <ChevronDown size={14} />
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
<div class="flex flex-col gap-3 mx-4 pb-4 pt-5 w-[48rem]">
{#if !$enterpriseLicense}
<Alert
type={storage.advancedPermissions ? 'error' : 'info'}
title="Advanced permission rules are an Enterprise feature"
>
Consider upgrading to Windmill EE to use advanced permission rules to control access to
your object storage at a more granular level.</Alert
>
{/if}
<Toggle
bind:checked={
() => !!storage.advancedPermissions,
(v) => {
storage.advancedPermissions = v
? defaultS3AdvancedPermissions(!!$enterpriseLicense)
: undefined
if (v) storage.publicResource = false
}
}
options={{
right: 'Enable advanced permission rules',
rightTooltip: 'Control precisely which paths are allowed to your users.'
}}
disabled={!storage.advancedPermissions && !$enterpriseLicense}
/>
{#if storage.advancedPermissions}
{@render advancedPermissionsEditor(storage.advancedPermissions)}
{/if}
{#if !storage.advancedPermissions}
{#if storage.resourceType == 's3'}
<div class="flex flex-col mt-2 mb-1 gap-1">
<Toggle
disabled={emptyString(storage.resourcePath)}
bind:checked={storage.publicResource}
options={{
right:
'S3 resource details and content can be accessed by all users of this workspace',
rightTooltip:
'If set, all users of this workspace will have access the to entire content of the S3 bucket, as well as the resource details and the "open preview" button. This effectively by-pass the permissions set on the resource and makes it public to everyone.'
}}
/>
{#if storage.publicResource === true}
<div class="pt-2"></div>
<Modal2
target="#content"
title={'Permission settings'}
contentClasses="flex flex-col gap-3"
fixedWidth="md"
fixedHeight="lg"
isOpen={advancedPermissionModalState.open}
>
{#if advancedPermissionModalState.open}
{@const storage = advancedPermissionModalState.storage}
{#if !$enterpriseLicense}
<Alert
type={storage.advancedPermissions ? 'error' : 'info'}
title="Advanced permission rules are an Enterprise feature"
>
Consider upgrading to Windmill EE to use advanced permission rules to control access to your
object storage at a more granular level.</Alert
>
{/if}
<Toggle
bind:checked={
() => !!storage.advancedPermissions,
(v) => {
storage.advancedPermissions = v
? defaultS3AdvancedPermissions(!!$enterpriseLicense)
: undefined
if (v) storage.publicResource = false
}
}
options={{
right: 'Enable advanced permission rules',
rightTooltip: 'Control precisely which paths are allowed to your users.'
}}
disabled={!storage.advancedPermissions && !$enterpriseLicense}
/>
{#if storage.advancedPermissions}
{@render advancedPermissionsEditor(storage.advancedPermissions)}
{/if}
{#if !storage.advancedPermissions}
{#if storage.resourceType == 's3'}
<div class="flex flex-col mt-2 mb-1 gap-1">
<Toggle
disabled={emptyString(storage.resourcePath)}
bind:checked={storage.publicResource}
options={{
right:
'S3 resource details and content can be accessed by all users of this workspace',
rightTooltip:
'If set, all users of this workspace will have access the to entire content of the S3 bucket, as well as the resource details and the "open preview" button. This effectively by-pass the permissions set on the resource and makes it public to everyone.'
}}
/>
{#if storage.publicResource === true}
<div class="pt-2"></div>
<Alert
type="warning"
title="(Legacy) S3 bucket content and resource details are shared"
>
S3 resource public access is ON, which means that the entire content of the S3
bucket will be accessible to all the users of this workspace regardless of whether
they have access the resource or not. Similarly, certain Windmill SDK endpoints
can be used in scripts to access the resource details, including public and
private keys.
</Alert>
{/if}
</div>
{:else}
<div class="flex flex-col mt-5 mb-1 gap-1 max-w-[40rem]">
<Toggle
disabled={emptyString(storage.resourcePath)}
bind:checked={storage.publicResource}
options={{
right: 'object storage content can be accessed by all users of this workspace',
rightTooltip:
'If set, all users of this workspace will have access the to entire content of the object storage.'
}}
/>
{#if storage.publicResource === true}
<div class="pt-2"></div>
<Alert
type="warning"
title="(Legacy) Object storage content and resource details are shared"
>
object public access is ON, which means that the entire content of the object
store will be accessible to all the users of this workspace regardless of whether
they have access the resource or not.
</Alert>
{/if}
</div>
<Alert
type="warning"
title="(Legacy) S3 bucket content and resource details are shared"
>
S3 resource public access is ON, which means that the entire content of the S3 bucket
will be accessible to all the users of this workspace regardless of whether they have
access the resource or not. Similarly, certain Windmill SDK endpoints can be used in
scripts to access the resource details, including public and private keys.
</Alert>
{/if}
{/if}
</div>
</svelte:fragment>
</Popover>
{/snippet}
</div>
{:else}
<div class="flex flex-col mt-5 mb-1 gap-1 max-w-[40rem]">
<Toggle
disabled={emptyString(storage.resourcePath)}
bind:checked={storage.publicResource}
options={{
right: 'object storage content can be accessed by all users of this workspace',
rightTooltip:
'If set, all users of this workspace will have access the to entire content of the object storage.'
}}
/>
{#if storage.publicResource === true}
<div class="pt-2"></div>
<Alert
type="warning"
title="(Legacy) Object storage content and resource details are shared"
>
object public access is ON, which means that the entire content of the object store
will be accessible to all the users of this workspace regardless of whether they have
access the resource or not.
</Alert>
{/if}
</div>
{/if}
{/if}
{/if}
</Modal2>
{#snippet advancedPermissionsEditor(rules: S3ResourceSettingsItem['advancedPermissions'])}
<Alert title="Standard Unix-style glob syntax is supported">
@@ -335,20 +388,22 @@
<br />
Note that changes may take up to 1 minute to propagate due to cache invalidation
</Alert>
{#each rules ?? [] as item, idx}
<div class="flex gap-2">
<ClearableInput bind:value={item.pattern} placeholder="Pattern" />
<MultiSelect
items={[{ value: 'read' }, { value: 'write' }, { value: 'delete' }, { value: 'list' }]}
bind:value={item.allow}
disablePortal
class="w-[20rem]"
placeholder="Deny all access"
hideMainClearBtn
/>
<CloseButton onClick={() => rules?.splice(idx, 1)} />
</div>
{/each}
<div class="flex-1 overflow-y-auto gap-3 flex flex-col">
{#each rules ?? [] as item, idx}
<div class="flex gap-2">
<ClearableInput bind:value={item.pattern} placeholder="Pattern" />
<MultiSelect
items={[{ value: 'read' }, { value: 'write' }, { value: 'delete' }, { value: 'list' }]}
bind:value={item.allow}
class="w-[20rem]"
placeholder="Deny all access"
hideMainClearBtn
/>
<CloseButton onClick={() => rules?.splice(idx, 1)} />
</div>
{/each}
</div>
<Button size="xs" variant="default" on:click={() => rules?.push({ pattern: '', allow: [] })}>
<Plus size={14} />
Add permission rule

View File

@@ -1956,3 +1956,28 @@ export function countChars(str: string, char: string): number {
}
return count
}
export function buildReactiveObj<T extends object>(fields: {
[name in keyof T]: [() => T[name], (v: T[name]) => void]
}): T {
const obj = {} as T
for (const key in fields) {
Object.defineProperty(obj, key, {
get: fields[key][0],
set: fields[key][1],
enumerable: true,
configurable: true
})
}
return obj
}
export function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
const result = {} as Pick<T, K>
for (const key of keys) {
if (key in obj) {
result[key] = obj[key]
}
}
return result
}

View File

@@ -120,7 +120,7 @@
publicResource: undefined,
secondaryStorage: undefined
})
let initialS3ResourceSettings: S3ResourceSettings = $state({
let s3ResourceSavedSettings: S3ResourceSettings = $state({
resourceType: 's3',
resourcePath: undefined,
publicResource: undefined,
@@ -353,7 +353,7 @@
settings.large_file_storage,
!!$enterpriseLicense
)
initialS3ResourceSettings = clone(s3ResourceSettings)
s3ResourceSavedSettings = clone(s3ResourceSettings)
dataTableSettings = convertDataTableSettingsFromBackend(settings.datatable)
ducklakeSettings = convertDucklakeSettingsFromBackend(settings.ducklake)
ducklakeSavedSettings = clone(ducklakeSettings)
@@ -580,7 +580,7 @@
}
const savedValue = {
s3ResourceSettings: initialS3ResourceSettings,
s3ResourceSettings: s3ResourceSavedSettings,
ducklakeSettings: ducklakeSavedSettings
}
@@ -594,7 +594,7 @@
// Function to discard unsaved storage settings changes
function discardStorageSettingsChanges() {
s3ResourceSettings = clone(initialS3ResourceSettings)
s3ResourceSettings = clone(s3ResourceSavedSettings)
ducklakeSettings = clone(ducklakeSavedSettings)
}
@@ -1203,8 +1203,9 @@
{:else if tab == 'windmill_lfs'}
<StorageSettings
bind:s3ResourceSettings
{s3ResourceSavedSettings}
onSave={() => {
initialS3ResourceSettings = clone(s3ResourceSettings)
s3ResourceSavedSettings = clone(s3ResourceSettings)
}}
/>
<DucklakeSettings

View File

@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.603.0"
wmill_pg = ">=1.603.0"
wmill = ">=1.603.2"
wmill_pg = ">=1.603.2"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.603.0
version: 1.603.2
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.603.0'
ModuleVersion = '1.603.2'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.603.0"
version = "1.603.2"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.603.0"
version = "1.603.2"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.603.0",
"version": "1.603.2",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.603.0",
"version": "1.603.2",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {

View File

@@ -1 +1 @@
1.603.0
1.603.2