Compare commits
9 Commits
rf/pg_embe
...
alp/path_f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9eca26316 | ||
|
|
f0fd1c5e1a | ||
|
|
ecb8015d6c | ||
|
|
35ddfc428d | ||
|
|
98c073bfaa | ||
|
|
57fd32b11d | ||
|
|
17d29cd8c7 | ||
|
|
7b19ca44a3 | ||
|
|
3939e96c71 |
30
.claude/hooks/resolve-symlinks.sh
Executable file
30
.claude/hooks/resolve-symlinks.sh
Executable 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
|
||||
36
.claude/hooks/restore-symlinks.sh
Executable file
36
.claude/hooks/restore-symlinks.sh
Executable 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
25
.github/workflows/claude-fast.yml
vendored
25
.github/workflows/claude-fast.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/claude.yml
vendored
9
.github/workflows/claude.yml
vendored
@@ -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": {
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -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
58
backend/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -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
|
||||
$$;
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
1092
backend/windmill-duckdb-ffi-internal/Cargo.lock
generated
1092
backend/windmill-duckdb-ffi-internal/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"] }
|
||||
|
||||
@@ -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<_>>(),
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {} +
|
||||
|
||||
@@ -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 {} +
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.603.0",
|
||||
"version": "1.603.2",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.603.0
|
||||
version: 1.603.2
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.603.0'
|
||||
ModuleVersion = '1.603.2'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.603.0
|
||||
1.603.2
|
||||
|
||||
Reference in New Issue
Block a user