diff --git a/backend/.sqlx/query-01c040b04b487e86b7f4ff38b0faacf6af2c284ae446860113c82bc4e1da08ab.json b/backend/.sqlx/query-01c040b04b487e86b7f4ff38b0faacf6af2c284ae446860113c82bc4e1da08ab.json deleted file mode 100644 index 0ad1fe4367..0000000000 --- a/backend/.sqlx/query-01c040b04b487e86b7f4ff38b0faacf6af2c284ae446860113c82bc4e1da08ab.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO app_version (id, app_id, value, created_by, created_at)\n VALUES (3001, 3001, '{\"grid\": []}', 'admin', NOW())", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "01c040b04b487e86b7f4ff38b0faacf6af2c284ae446860113c82bc4e1da08ab" -} diff --git a/backend/.sqlx/query-1ad384eeb3946ef7c492124c69fbc47caebc25215a430d6b301b35e265888159.json b/backend/.sqlx/query-1ad384eeb3946ef7c492124c69fbc47caebc25215a430d6b301b35e265888159.json deleted file mode 100644 index 24d3c8929a..0000000000 --- a/backend/.sqlx/query-1ad384eeb3946ef7c492124c69fbc47caebc25215a430d6b301b35e265888159.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE app SET versions = ARRAY[3001::bigint] WHERE id = 3001", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "1ad384eeb3946ef7c492124c69fbc47caebc25215a430d6b301b35e265888159" -} diff --git a/backend/.sqlx/query-1fa27bd47ab66b3f301ca43ef93fb7cf5f2b76f013c274245af13d7d727ebf1f.json b/backend/.sqlx/query-1fa27bd47ab66b3f301ca43ef93fb7cf5f2b76f013c274245af13d7d727ebf1f.json deleted file mode 100644 index 10cab9117a..0000000000 --- a/backend/.sqlx/query-1fa27bd47ab66b3f301ca43ef93fb7cf5f2b76f013c274245af13d7d727ebf1f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, extra_perms)\n VALUES ('test-workspace', 'u/operator/existing_flow', 'Existing flow', '', '{\"modules\": []}', 'admin', NOW(), '{}', '{}')", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "1fa27bd47ab66b3f301ca43ef93fb7cf5f2b76f013c274245af13d7d727ebf1f" -} diff --git a/backend/.sqlx/query-4e88aec662ebc70e0425a48a1b4e2e60e3183fa81a411622891caea6dc03fa90.json b/backend/.sqlx/query-4e88aec662ebc70e0425a48a1b4e2e60e3183fa81a411622891caea6dc03fa90.json deleted file mode 100644 index 27d46b27ed..0000000000 --- a/backend/.sqlx/query-4e88aec662ebc70e0425a48a1b4e2e60e3183fa81a411622891caea6dc03fa90.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO token (token_hash, token_prefix, email, label, super_admin, owner, workspace_id)\n VALUES ($1, $2, 'charlie@windmill.dev', 'Charlie new token', false, 'u/charlie', 'test-workspace')", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "4e88aec662ebc70e0425a48a1b4e2e60e3183fa81a411622891caea6dc03fa90" -} diff --git a/backend/.sqlx/query-5b140cb478aa32ac38fb831d6af960ffc33da5f31bf780e8fd6a66d5150b8027.json b/backend/.sqlx/query-5b140cb478aa32ac38fb831d6af960ffc33da5f31bf780e8fd6a66d5150b8027.json deleted file mode 100644 index 8e558fe67b..0000000000 --- a/backend/.sqlx/query-5b140cb478aa32ac38fb831d6af960ffc33da5f31bf780e8fd6a66d5150b8027.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)\n VALUES (3001, 'test-workspace', 'u/operator/existing_app', 'Existing app', '{}',\n '{\"on_behalf_of\": \"u/admin\", \"on_behalf_of_email\": \"admin@windmill.dev\", \"execution_mode\": \"viewer\"}', '{}')", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "5b140cb478aa32ac38fb831d6af960ffc33da5f31bf780e8fd6a66d5150b8027" -} diff --git a/backend/.sqlx/query-6c24b5c08341979e02b7968242369c87a9d29370ec985d2c8c28633cd078ffaf.json b/backend/.sqlx/query-6c24b5c08341979e02b7968242369c87a9d29370ec985d2c8c28633cd078ffaf.json deleted file mode 100644 index 6da123cbc4..0000000000 --- a/backend/.sqlx/query-6c24b5c08341979e02b7968242369c87a9d29370ec985d2c8c28633cd078ffaf.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO usr_to_group (workspace_id, group_, usr) VALUES ('test-workspace', 'editors', 'charlie')", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "6c24b5c08341979e02b7968242369c87a9d29370ec985d2c8c28633cd078ffaf" -} diff --git a/backend/.sqlx/query-be35e85e94641c71620e5402dcf74da8c73120b3e16194904575f79a4e055002.json b/backend/.sqlx/query-be35e85e94641c71620e5402dcf74da8c73120b3e16194904575f79a4e055002.json deleted file mode 100644 index d7cc49fe3f..0000000000 --- a/backend/.sqlx/query-be35e85e94641c71620e5402dcf74da8c73120b3e16194904575f79a4e055002.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)\n VALUES ('test-workspace', 3001, 'u/operator/existing_script', 'export function main() { return \"original\"; }', 'deno', 'script', 'admin', '{}', 'Existing script', '', '', '{}')", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "be35e85e94641c71620e5402dcf74da8c73120b3e16194904575f79a4e055002" -} diff --git a/backend/.sqlx/query-ccd76be88fa9c11b3dc2e6d7711437ab3e02d8c3c10c53decc664533b8d04bc0.json b/backend/.sqlx/query-ccd76be88fa9c11b3dc2e6d7711437ab3e02d8c3c10c53decc664533b8d04bc0.json deleted file mode 100644 index d9b7688eba..0000000000 --- a/backend/.sqlx/query-ccd76be88fa9c11b3dc2e6d7711437ab3e02d8c3c10c53decc664533b8d04bc0.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id,\n script_path, is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "VarcharArray", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "ccd76be88fa9c11b3dc2e6d7711437ab3e02d8c3c10c53decc664533b8d04bc0" -} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 40b16c0b62..7d86a6114e 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -414202845a45e2a7c6a2d3e154bd8dfd0273cc14 \ No newline at end of file +b5c8af4df9ba2c39fdd494d7a40f9a92fbff8abc diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 55ad777cbf..fb234a13a3 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -35,7 +35,6 @@ use windmill_common::variables::{ build_crypt, decrypt, encrypt, SECRET_SALT, WORKSPACE_CRYPT_CACHE, }; use windmill_common::worker::{to_raw_value, CLOUD_HOSTED}; -#[cfg(feature = "enterprise")] use windmill_common::workspaces::GitRepositorySettings; #[cfg(feature = "enterprise")] use windmill_common::workspaces::WorkspaceDeploymentUISettings; @@ -115,6 +114,7 @@ pub fn workspaced_service() -> Router { .route("/list_datatables", get(list_datatables)) .route("/list_datatable_schemas", get(list_datatable_schemas)) .route("/edit_datatable_config", post(edit_datatable_config)) + .route("/git_sync_enabled", get(get_git_sync_enabled)) .route("/edit_git_sync_config", post(edit_git_sync_config)) .route("/edit_git_sync_repository", post(edit_git_sync_repository)) .route( @@ -1595,24 +1595,20 @@ async fn edit_datatable_config( #[derive(Deserialize)] pub struct EditGitSyncConfig { - #[cfg(feature = "enterprise")] pub git_sync_settings: Option, } -#[cfg(feature = "enterprise")] #[derive(Deserialize, Debug)] pub struct EditGitSyncRepository { pub git_repo_resource_path: String, pub repository: GitRepositorySettings, } -#[cfg(feature = "enterprise")] #[derive(Deserialize, Debug)] pub struct DeleteGitSyncRepositoryRequest { pub git_repo_resource_path: String, } -#[cfg(feature = "enterprise")] fn validate_git_repo_resource_path(path: &str) -> Result<()> { // Resource paths should follow the pattern: $res:f// or $res:u// if path.is_empty() { @@ -1661,7 +1657,6 @@ fn validate_git_repo_resource_path(path: &str) -> Result<()> { Ok(()) } -#[cfg(feature = "enterprise")] fn cleanup_legacy_git_sync_settings_in_memory( git_sync_settings: &mut windmill_common::workspaces::WorkspaceGitSyncSettings, workspace_id: &str, @@ -1688,18 +1683,72 @@ fn cleanup_legacy_git_sync_settings_in_memory( } #[cfg(not(feature = "enterprise"))] -async fn edit_git_sync_config( - _authed: ApiAuthed, - Extension(_db): Extension, - Path(_w_id): Path, - Json(_new_config): Json, -) -> Result { - return Err(Error::BadRequest( - "Git sync is only available on Windmill Enterprise Edition".to_string(), - )); +const CE_GIT_SYNC_MAX_USERS: i64 = 2; + +#[cfg(feature = "enterprise")] +async fn check_git_sync_access(_db: &DB, _w_id: &str) -> Result<()> { + Ok(()) +} + +#[cfg(not(feature = "enterprise"))] +async fn check_git_sync_access(db: &DB, w_id: &str) -> Result<()> { + let user_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) FROM usr WHERE workspace_id = $1 AND disabled = false", + w_id + ) + .fetch_one(db) + .await? + .unwrap_or(0); + + if user_count > CE_GIT_SYNC_MAX_USERS { + return Err(Error::BadRequest(format!( + "Git sync is available for workspaces with up to {} members. \ + Upgrade to Windmill Enterprise Edition for unlimited workspace members.", + CE_GIT_SYNC_MAX_USERS + ))); + } + Ok(()) } #[cfg(feature = "enterprise")] +async fn get_git_sync_enabled( + _authed: ApiAuthed, + Extension(_db): Extension, + Path(_w_id): Path, +) -> JsonResult { + Ok(Json(serde_json::json!({ + "enabled": true, + "reason": "enterprise", + "max_repos": null, + "user_count": null, + "max_users": null, + }))) +} + +#[cfg(not(feature = "enterprise"))] +async fn get_git_sync_enabled( + _authed: ApiAuthed, + Extension(db): Extension, + Path(w_id): Path, +) -> JsonResult { + let user_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) FROM usr WHERE workspace_id = $1 AND disabled = false", + &w_id + ) + .fetch_one(&db) + .await? + .unwrap_or(0); + + let enabled = user_count <= CE_GIT_SYNC_MAX_USERS; + Ok(Json(serde_json::json!({ + "enabled": enabled, + "reason": if enabled { Some("free_tier") } else { None::<&str> }, + "max_repos": if enabled { Some(1) } else { None:: }, + "user_count": user_count, + "max_users": CE_GIT_SYNC_MAX_USERS, + }))) +} + async fn edit_git_sync_config( authed: ApiAuthed, Extension(db): Extension, @@ -1708,6 +1757,7 @@ async fn edit_git_sync_config( Json(new_config): Json, ) -> Result { require_admin(is_admin, &username)?; + check_git_sync_access(&db, &w_id).await?; let mut tx = db.begin().await?; @@ -1764,19 +1814,6 @@ async fn edit_git_sync_config( Ok(format!("Edit git sync config for workspace {}", &w_id)) } -#[cfg(not(feature = "enterprise"))] -async fn edit_git_sync_repository( - _authed: ApiAuthed, - Extension(_db): Extension, - Path(_w_id): Path, - Json(_new_config): Json, -) -> Result { - return Err(Error::BadRequest( - "Git sync is only available on Windmill Enterprise Edition".to_string(), - )); -} - -#[cfg(feature = "enterprise")] async fn edit_git_sync_repository( authed: ApiAuthed, Extension(db): Extension, @@ -1785,10 +1822,19 @@ async fn edit_git_sync_repository( Json(new_config): Json, ) -> Result { require_admin(is_admin, &username)?; + check_git_sync_access(&db, &w_id).await?; // Validate the resource path format validate_git_repo_resource_path(&new_config.git_repo_resource_path)?; + // Promotion mode: EE only + #[cfg(not(feature = "enterprise"))] + if new_config.repository.use_individual_branch.unwrap_or(false) { + return Err(Error::BadRequest( + "Promotion mode is an Enterprise Edition feature".to_string(), + )); + } + let mut tx = db.begin().await?; // First, get the current git sync settings @@ -1810,6 +1856,20 @@ async fn edit_git_sync_repository( WorkspaceGitSyncSettings::default() }; + // Multi-repo: EE only + #[cfg(not(feature = "enterprise"))] + { + let is_new = !git_sync_settings + .repositories + .iter() + .any(|r| r.git_repo_resource_path == new_config.git_repo_resource_path); + if is_new && !git_sync_settings.repositories.is_empty() { + return Err(Error::BadRequest( + "Multiple git sync repositories is an Enterprise Edition feature".to_string(), + )); + } + } + // Audit log before we move the repository audit_log( &mut *tx, @@ -1893,19 +1953,6 @@ async fn edit_git_sync_repository( )) } -#[cfg(not(feature = "enterprise"))] -async fn delete_git_sync_repository( - _authed: ApiAuthed, - Extension(_db): Extension, - Path(_w_id): Path, - Json(_request): Json, -) -> Result { - return Err(Error::BadRequest( - "Git sync is only available on Windmill Enterprise Edition".to_string(), - )); -} - -#[cfg(feature = "enterprise")] async fn delete_git_sync_repository( authed: ApiAuthed, Extension(db): Extension, @@ -1915,7 +1962,7 @@ async fn delete_git_sync_repository( ) -> Result { require_admin(is_admin, &username)?; - // For deletion, only validate that path is not empty to allow cleanup of malformed entries + // No check_git_sync_access here — admins should always be able to delete/clean up repos if request.git_repo_resource_path.is_empty() { return Err(Error::BadRequest( "Resource path cannot be empty".to_string(), diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 5f350f2ce5..a8f83c368a 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -3430,6 +3430,37 @@ paths: application/json: schema: {} + /w/{workspace}/workspaces/git_sync_enabled: + get: + summary: Check if git sync is available for this workspace + operationId: getGitSyncEnabled + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: Git sync availability status + content: + application/json: + schema: + type: object + properties: + enabled: + type: boolean + reason: + type: string + nullable: true + max_repos: + type: integer + nullable: true + user_count: + type: integer + nullable: true + max_users: + type: integer + nullable: true + /w/{workspace}/workspaces/edit_git_sync_config: post: summary: edit workspace git sync settings diff --git a/backend/windmill-common/src/ee_oss.rs b/backend/windmill-common/src/ee_oss.rs index 51b1efd2e2..93d0061ade 100644 --- a/backend/windmill-common/src/ee_oss.rs +++ b/backend/windmill-common/src/ee_oss.rs @@ -23,6 +23,7 @@ lazy_static::lazy_static! { } #[cfg(not(feature = "private"))] +#[derive(PartialEq, Eq)] pub enum LicensePlan { Community, Pro, diff --git a/backend/windmill-git-sync/Cargo.toml b/backend/windmill-git-sync/Cargo.toml index 063e1ce54a..148746dcca 100644 --- a/backend/windmill-git-sync/Cargo.toml +++ b/backend/windmill-git-sync/Cargo.toml @@ -9,7 +9,7 @@ name = "windmill_git_sync" path = "./src/lib.rs" [features] -private = [] +private = ["windmill-common/private"] enterprise = ["windmill-queue/enterprise", "windmill-common/enterprise"] all_sqlx_features = ["enterprise"] default = [] diff --git a/backend/windmill-git-sync/src/lib.rs b/backend/windmill-git-sync/src/lib.rs index f9cecd46ce..dcbcd5bcb2 100644 --- a/backend/windmill-git-sync/src/lib.rs +++ b/backend/windmill-git-sync/src/lib.rs @@ -272,7 +272,10 @@ mod tests { path: "f/folder/script".to_string(), parent_path: Some("f/folder/old_script".to_string()), }; - assert_eq!(obj.get_parent_path(), Some("f/folder/old_script".to_string())); + assert_eq!( + obj.get_parent_path(), + Some("f/folder/old_script".to_string()) + ); } #[test] @@ -313,21 +316,13 @@ mod tests { #[test] fn test_get_kind_flow() { - let obj = DeployedObject::Flow { - path: "test".to_string(), - parent_path: None, - version: 1, - }; + let obj = DeployedObject::Flow { path: "test".to_string(), parent_path: None, version: 1 }; assert_eq!(obj.get_kind(), "flow"); } #[test] fn test_get_kind_app() { - let obj = DeployedObject::App { - path: "test".to_string(), - version: 1, - parent_path: None, - }; + let obj = DeployedObject::App { path: "test".to_string(), version: 1, parent_path: None }; assert_eq!(obj.get_kind(), "app"); } @@ -346,7 +341,8 @@ mod tests { "http_trigger" ); assert_eq!( - DeployedObject::WebsocketTrigger { path: "t".to_string(), parent_path: None }.get_kind(), + DeployedObject::WebsocketTrigger { path: "t".to_string(), parent_path: None } + .get_kind(), "websocket_trigger" ); assert_eq!( diff --git a/cli/package-lock.json b/cli/package-lock.json index 0e86b9d2b7..ae46c240c9 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -25,10 +25,11 @@ "windmill-parser-wasm-nu": "*", "windmill-parser-wasm-php": "*", "windmill-parser-wasm-py": "*", + "windmill-parser-wasm-py-imports": "*", "windmill-parser-wasm-regex": "*", "windmill-parser-wasm-ruby": "*", "windmill-parser-wasm-rust": "*", - "windmill-parser-wasm-ts": "*", + "windmill-parser-wasm-ts": "^1.659.1", "windmill-parser-wasm-yaml": "*", "windmill-yaml-validator": "1.1.1", "ws": "8.18.0", @@ -1414,6 +1415,11 @@ "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.628.3.tgz", "integrity": "sha512-TlluqknZpg8cZ+A3m6JFLPseY2PpKtDsxdj26fAnCUzKPtse8TxQR+n0dwC80rfW5TwdWSulvNGRDgcNuf7CTw==" }, + "node_modules/windmill-parser-wasm-py-imports": { + "version": "1.659.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py-imports/-/windmill-parser-wasm-py-imports-1.659.1.tgz", + "integrity": "sha512-nfnf04WBRf8f/mNIwdvggYOgz3erxrFGjKqULYBH+bKFMlKA6V7eB19m6CXOBkq9rjTp0ZFG+rgsR+Us7JEkyQ==" + }, "node_modules/windmill-parser-wasm-regex": { "version": "1.639.0", "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.639.0.tgz", @@ -1430,9 +1436,9 @@ "integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ==" }, "node_modules/windmill-parser-wasm-ts": { - "version": "1.647.1", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.647.1.tgz", - "integrity": "sha512-64iSAUMU5W/WtePqE1vtDvglDqtkiZVndyieYBVDX0nl7UuovS+wPgH/P3TEoKbR+FwAPacki0CX3DsEzZ/Yxw==" + "version": "1.659.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.659.1.tgz", + "integrity": "sha512-EmXMzOmazC5r29UZh+1TVF9g/N2X51pqK11qDL6xWGeWTIIonhfOZ5nWdGvKQMDUR650fGxehImZzW2v9hNy+w==" }, "node_modules/windmill-parser-wasm-yaml": { "version": "1.593.0", diff --git a/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts b/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts index 834982c3f3..ba919fa16b 100644 --- a/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts +++ b/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts @@ -1,4 +1,7 @@ import { getContext, setContext } from 'svelte' +import { enterpriseLicense } from '$lib/stores' +import { get } from 'svelte/store' +import { sendUserToast } from '$lib/toast' import { JobService, WorkspaceService, ResourceService } from '$lib/gen' import type { GitRepositorySettings as BackendGitRepositorySettings, @@ -646,6 +649,10 @@ export function createGitSyncContext(workspace: string) { } function addSyncRepository() { + if (!get(enterpriseLicense) && repositories && repositories.length >= 1) { + sendUserToast('Multiple repositories requires Enterprise Edition', true) + return + } repositories.push({ git_repo_resource_path: '', script_path: undefined, @@ -669,6 +676,10 @@ export function createGitSyncContext(workspace: string) { } function addPromotionRepository() { + if (!get(enterpriseLicense)) { + sendUserToast('Promotion mode requires Enterprise Edition', true) + return + } repositories.push({ git_repo_resource_path: '', script_path: undefined, diff --git a/frontend/src/lib/components/git_sync/GitSyncSection.svelte b/frontend/src/lib/components/git_sync/GitSyncSection.svelte index 893e782b19..9a03e43b27 100644 --- a/frontend/src/lib/components/git_sync/GitSyncSection.svelte +++ b/frontend/src/lib/components/git_sync/GitSyncSection.svelte @@ -6,12 +6,46 @@ import GitSyncRepositoryCard from './GitSyncRepositoryCard.svelte' import GitSyncModalManager from './GitSyncModalManager.svelte' import { enterpriseLicense, workspaceStore } from '$lib/stores' + import { WorkspaceService } from '$lib/gen' import { sendUserToast } from '$lib/toast' import { untrack } from 'svelte' // Create context reactively based on workspaceStore const gitSyncContext = $derived($workspaceStore ? setGitSyncContext($workspaceStore) : null) + // Fetch git sync eligibility + let gitSyncStatus = $state<{ + enabled: boolean + reason: string | null + max_repos: number | null + user_count: number | null + max_users: number | null + }>({ enabled: false, reason: null, max_repos: null, user_count: null, max_users: null }) + + $effect(() => { + if ($workspaceStore) { + WorkspaceService.getGitSyncEnabled({ workspace: $workspaceStore }) + .then((status) => { + gitSyncStatus = status as typeof gitSyncStatus + }) + .catch(() => { + gitSyncStatus = { + enabled: false, + reason: null, + max_repos: null, + user_count: null, + max_users: null + } + }) + } + }) + + const gitSyncAllowed = $derived(gitSyncStatus.enabled) + const isFreeTier = $derived(gitSyncAllowed && !$enterpriseLicense) + const hasConfiguredRepos = $derived( + gitSyncContext?.repositories?.some((r) => r.git_repo_resource_path) ?? false + ) + // Load settings when workspace context changes $effect(() => { if (gitSyncContext) { @@ -58,7 +92,7 @@ link="https://www.windmill.dev/docs/advanced/git_sync" > {#snippet actions()} - {#if $enterpriseLicense && gitSyncContext.repositories != undefined} + {#if (gitSyncAllowed || gitSyncStatus.user_count != null) && gitSyncContext?.repositories != undefined} - - {#if secondarySyncExpanded} -
- {#if secondarySync.length === 0} -
- No secondary sync repositories configured -
- {:else} - {#each secondarySync as { repo, idx } (repo.git_repo_resource_path)} -
- -
- {/each} - {/if} - - {#if !hasUnsavedSecondary} -
- -
- {/if} -
- {/if} - - {:else} - - {#if !hasUnsavedSecondary} -
- -
- {/if} - {/if} - {/if} - - -
- gitSyncContext.addPromotionRepository()} - isCollapsible={false} - showEmptyState={primaryPromotion?.repo === null} - /> - - - {#if primaryPromotion && !primaryPromotion.repo?.isUnsavedConnection} - {#if secondaryPromotion.length > 0 || secondaryPromotionExpanded} + {#if $enterpriseLicense} + + {#if primarySync && !primarySync.repo?.isUnsavedConnection} + {#if secondarySync.length > 0 || secondarySyncExpanded}
- {#if secondaryPromotionExpanded} + {#if secondarySyncExpanded}
- {#if secondaryPromotion.length === 0} + {#if secondarySync.length === 0}
- No secondary promotion repositories configured + No secondary sync repositories configured
{:else} - {#each secondaryPromotion as { repo, idx } (repo.git_repo_resource_path)} + {#each secondarySync as { repo, idx } (repo.git_repo_resource_path)}
{/each} {/if} - {#if !hasUnsavedSecondaryPromotion} + {#if !hasUnsavedSecondary}
{/if} @@ -216,23 +187,99 @@ {/if}
{:else} - - {#if !hasUnsavedSecondaryPromotion} + + {#if !hasUnsavedSecondary}
{/if} {/if} {/if} -
+ + +
+ gitSyncContext.addPromotionRepository()} + isCollapsible={false} + showEmptyState={primaryPromotion?.repo === null} + /> + + + {#if primaryPromotion && !primaryPromotion.repo?.isUnsavedConnection} + {#if secondaryPromotion.length > 0 || secondaryPromotionExpanded} +
+ + + {#if secondaryPromotionExpanded} +
+ {#if secondaryPromotion.length === 0} +
+ No secondary promotion repositories configured +
+ {:else} + {#each secondaryPromotion as { repo, idx } (repo.git_repo_resource_path)} +
+ +
+ {/each} + {/if} + + {#if !hasUnsavedSecondaryPromotion} +
+ +
+ {/if} +
+ {/if} +
+ {:else} + + {#if !hasUnsavedSecondaryPromotion} +
+ +
+ {/if} + {/if} + {/if} +
+ {/if}