diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs index 99f77a3706..4563d58a0e 100644 --- a/crates/defguard_core/src/handlers/user.rs +++ b/crates/defguard_core/src/handlers/user.rs @@ -43,7 +43,8 @@ use crate::{ ldap_handle_user_modify, ldap_remove_user_from_groups, ldap_update_user_state, }, }, - limits::update_counts, + license::get_cached_license, + limits::{get_counts, update_counts}, }, error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, @@ -315,6 +316,18 @@ pub async fn add_user( let username = user_data.username.clone(); debug!("User {} adding user {username}", session.user.username); + // check if adding new user will go over limits + let user_count = get_counts().user(); + + if get_cached_license() + .as_ref() + .and_then(|l| l.limits.as_ref()) + .is_some_and(|l| l.users == user_count) + { + error!("Adding user {username} blocked! License limit reached."); + return Ok(WebError::Forbidden("License limit reached.".into()).into()); + } + // check username if let Err(err) = check_username(&username) { debug!("Username {username} rejected: {err}"); diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index eb4accf317..e05c805031 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -40,7 +40,8 @@ use crate::{ firewall::try_get_location_firewall_config, handlers::CanManageDevices, is_business_license_active, - limits::update_counts, + license::get_cached_license, + limits::{get_counts, update_counts}, }, events::{ApiEvent, ApiEventType, ApiRequestContext}, grpc::gateway::events::GatewayEvent, @@ -223,6 +224,18 @@ pub(crate) async fn create_network( session.user.username ); + // check if adding new network will go over license limits + let location_count = get_counts().location(); + + if get_cached_license() + .as_ref() + .and_then(|l| l.limits.as_ref()) + .is_some_and(|l| l.locations == location_count) + { + error!("Adding location {network_name} blocked! License limit reached."); + return Ok(WebError::Forbidden("License limit reached.".into()).into()); + } + data.validate_location_mfa_mode(&appstate.pool).await?; let allowed_ips = data.parse_allowed_ips(); diff --git a/web/biome.json b/web/biome.json index 9d89795086..17c6a056f7 100644 --- a/web/biome.json +++ b/web/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.15/schema.json", "vcs": { "enabled": false, "clientKind": "git", diff --git a/web/package.json b/web/package.json index 15fd640bce..9e62f3f39c 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,7 @@ "@shortercode/webzip": "1.1.1-0", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", - "@tanstack/react-form": "^1.28.0", + "@tanstack/react-form": "^1.28.1", "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.159.5", "@tanstack/react-table": "^8.21.3", @@ -51,7 +51,7 @@ "zustand": "^5.0.11" }, "devDependencies": { - "@biomejs/biome": "2.3.14", + "@biomejs/biome": "2.3.15", "@inlang/paraglide-js": "2.11.0", "@tanstack/devtools-vite": "^0.5.1", "@tanstack/react-devtools": "^0.9.5", @@ -71,7 +71,7 @@ "prettier": "^3.8.1", "sass": "^1.97.3", "sharp": "^0.34.5", - "stylelint": "^17.2.0", + "stylelint": "^17.3.0", "stylelint-config-standard-scss": "^17.0.0", "stylelint-scss": "^7.0.0", "typescript": "~5.9.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index ab6e025c5d..0e43aeb51e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^2.0.1 version: 2.0.1 '@tanstack/react-form': - specifier: ^1.28.0 - version: 1.28.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^1.28.1 + version: 1.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21(react@19.2.4) @@ -118,8 +118,8 @@ importers: version: 5.0.11(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@biomejs/biome': - specifier: 2.3.14 - version: 2.3.14 + specifier: 2.3.15 + version: 2.3.15 '@tanstack/devtools-vite': specifier: ^0.5.1 version: 0.5.1(vite@7.3.1(@types/node@25.2.3)(sass@1.97.3)(tsx@4.21.0)) @@ -175,14 +175,14 @@ importers: specifier: ^0.34.5 version: 0.34.5 stylelint: - specifier: ^17.2.0 - version: 17.2.0(typescript@5.9.3) + specifier: ^17.3.0 + version: 17.3.0(typescript@5.9.3) stylelint-config-standard-scss: specifier: ^17.0.0 - version: 17.0.0(postcss@8.5.6)(stylelint@17.2.0(typescript@5.9.3)) + version: 17.0.0(postcss@8.5.6)(stylelint@17.3.0(typescript@5.9.3)) stylelint-scss: specifier: ^7.0.0 - version: 7.0.0(stylelint@17.2.0(typescript@5.9.3)) + version: 7.0.0(stylelint@17.3.0(typescript@5.9.3)) typescript: specifier: ~5.9.3 version: 5.9.3 @@ -284,59 +284,59 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.3.14': - resolution: {integrity: sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==} + '@biomejs/biome@2.3.15': + resolution: {integrity: sha512-u+jlPBAU2B45LDkjjNNYpc1PvqrM/co4loNommS9/sl9oSxsAQKsNZejYuUztvToB5oXi1tN/e62iNd6ESiY3g==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.3.14': - resolution: {integrity: sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==} + '@biomejs/cli-darwin-arm64@2.3.15': + resolution: {integrity: sha512-SDCdrJ4COim1r8SNHg19oqT50JfkI/xGZHSyC6mGzMfKrpNe/217Eq6y98XhNTc0vGWDjznSDNXdUc6Kg24jbw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.3.14': - resolution: {integrity: sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==} + '@biomejs/cli-darwin-x64@2.3.15': + resolution: {integrity: sha512-RkyeSosBtn3C3Un8zQnl9upX0Qbq4E3QmBa0qjpOh1MebRbHhNlRC16jk8HdTe/9ym5zlfnpbb8cKXzW+vlTxw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.3.14': - resolution: {integrity: sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==} + '@biomejs/cli-linux-arm64-musl@2.3.15': + resolution: {integrity: sha512-SSSIj2yMkFdSkXqASzIBdjySBXOe65RJlhKEDlri7MN19RC4cpez+C0kEwPrhXOTgJbwQR9QH1F4+VnHkC35pg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.3.14': - resolution: {integrity: sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==} + '@biomejs/cli-linux-arm64@2.3.15': + resolution: {integrity: sha512-FN83KxrdVWANOn5tDmW6UBC0grojchbGmcEz6JkRs2YY6DY63sTZhwkQ56x6YtKhDVV1Unz7FJexy8o7KwuIhg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.3.14': - resolution: {integrity: sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==} + '@biomejs/cli-linux-x64-musl@2.3.15': + resolution: {integrity: sha512-dbjPzTh+ijmmNwojFYbQNMFp332019ZDioBYAMMJj5Ux9d8MkM+u+J68SBJGVwVeSHMYj+T9504CoxEzQxrdNw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.3.14': - resolution: {integrity: sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==} + '@biomejs/cli-linux-x64@2.3.15': + resolution: {integrity: sha512-T8n9p8aiIKOrAD7SwC7opiBM1LYGrE5G3OQRXWgbeo/merBk8m+uxJ1nOXMPzfYyFLfPlKF92QS06KN1UW+Zbg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.3.14': - resolution: {integrity: sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==} + '@biomejs/cli-win32-arm64@2.3.15': + resolution: {integrity: sha512-puMuenu/2brQdgqtQ7geNwQlNVxiABKEZJhMRX6AGWcmrMO8EObMXniFQywy2b81qmC+q+SDvlOpspNwz0WiOA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.3.14': - resolution: {integrity: sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==} + '@biomejs/cli-win32-x64@2.3.15': + resolution: {integrity: sha512-kDZr/hgg+igo5Emi0LcjlgfkoGZtgIpJKhnvKTRmMBv6FF/3SDyEV4khBwqNebZIyMZTzvpca9sQNSXJ39pI2A==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -347,8 +347,8 @@ packages: '@cacheable/utils@2.3.4': resolution: {integrity: sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==} - '@csstools/css-calc@3.0.1': - resolution: {integrity: sha512-bsDKIP6f4ta2DO9t+rAbSSwv4EMESXy5ZIvzQl1afmD6Z1XHkVu9ijcG9QR/qSgQS1dVa+RaQ/MfQ7FIB/Dn1Q==} + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -1212,8 +1212,8 @@ packages: peerDependencies: solid-js: '>=1.9.7' - '@tanstack/form-core@1.28.0': - resolution: {integrity: sha512-MX3YveB6SKHAJ2yUwp+Ca/PCguub8bVEnLcLUbFLwdkSRMkP0lMGdaZl+F0JuEgZw56c6iFoRyfILhS7OQpydA==} + '@tanstack/form-core@1.28.1': + resolution: {integrity: sha512-+7rOzEOoQjuGFBV9/QoW2XjYjB5hbRYDAJxhooSMAQSkBQrIyoXqV3pqyDS/q4I5kgtDJmfWMnOZU5ieCkhsCQ==} '@tanstack/history@1.154.14': resolution: {integrity: sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA==} @@ -1238,8 +1238,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-form@1.28.0': - resolution: {integrity: sha512-ibLcf5QkTogV0Ly944CuqGxWTpHyreNA4Cy8Wtky7zE9wtE3HVapQt4/hUuXo51zihfTkv5URiXpoTSKF5Xosg==} + '@tanstack/react-form@1.28.1': + resolution: {integrity: sha512-JN5J8p7aAybTzqgomi4rApr9/dDH1y+nFqYErv+dM+lufW30srYqey2PsSqBe7i8eqsZG70E2JjaDCk1fBon8w==} peerDependencies: '@tanstack/react-start': '*' react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2672,8 +2672,8 @@ packages: peerDependencies: stylelint: ^16.8.2 || ^17.0.0 - stylelint@17.2.0: - resolution: {integrity: sha512-602jhMkRt6P1dSh9kEzbFIaOKY//h4D0E7u/w2WHKxmi5VAjjMqe6P8rQPJuCWdbB3apOkjOFN5kcg6qWPIZWQ==} + stylelint@17.3.0: + resolution: {integrity: sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA==} engines: {node: '>=20.19.0'} hasBin: true @@ -3085,39 +3085,39 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.3.14': + '@biomejs/biome@2.3.15': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.14 - '@biomejs/cli-darwin-x64': 2.3.14 - '@biomejs/cli-linux-arm64': 2.3.14 - '@biomejs/cli-linux-arm64-musl': 2.3.14 - '@biomejs/cli-linux-x64': 2.3.14 - '@biomejs/cli-linux-x64-musl': 2.3.14 - '@biomejs/cli-win32-arm64': 2.3.14 - '@biomejs/cli-win32-x64': 2.3.14 - - '@biomejs/cli-darwin-arm64@2.3.14': + '@biomejs/cli-darwin-arm64': 2.3.15 + '@biomejs/cli-darwin-x64': 2.3.15 + '@biomejs/cli-linux-arm64': 2.3.15 + '@biomejs/cli-linux-arm64-musl': 2.3.15 + '@biomejs/cli-linux-x64': 2.3.15 + '@biomejs/cli-linux-x64-musl': 2.3.15 + '@biomejs/cli-win32-arm64': 2.3.15 + '@biomejs/cli-win32-x64': 2.3.15 + + '@biomejs/cli-darwin-arm64@2.3.15': optional: true - '@biomejs/cli-darwin-x64@2.3.14': + '@biomejs/cli-darwin-x64@2.3.15': optional: true - '@biomejs/cli-linux-arm64-musl@2.3.14': + '@biomejs/cli-linux-arm64-musl@2.3.15': optional: true - '@biomejs/cli-linux-arm64@2.3.14': + '@biomejs/cli-linux-arm64@2.3.15': optional: true - '@biomejs/cli-linux-x64-musl@2.3.14': + '@biomejs/cli-linux-x64-musl@2.3.15': optional: true - '@biomejs/cli-linux-x64@2.3.14': + '@biomejs/cli-linux-x64@2.3.15': optional: true - '@biomejs/cli-win32-arm64@2.3.14': + '@biomejs/cli-win32-arm64@2.3.15': optional: true - '@biomejs/cli-win32-x64@2.3.14': + '@biomejs/cli-win32-x64@2.3.15': optional: true '@cacheable/memory@2.0.7': @@ -3132,7 +3132,7 @@ snapshots: hashery: 1.4.0 keyv: 5.6.0 - '@csstools/css-calc@3.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -3785,7 +3785,7 @@ snapshots: - csstype - utf-8-validate - '@tanstack/form-core@1.28.0': + '@tanstack/form-core@1.28.1': dependencies: '@tanstack/devtools-event-client': 0.4.0 '@tanstack/pacer-lite': 0.1.1 @@ -3812,9 +3812,9 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-form@1.28.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-form@1.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/form-core': 1.28.0 + '@tanstack/form-core': 1.28.1 '@tanstack/react-store': 0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 transitivePeerDependencies: @@ -5353,33 +5353,33 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylelint-config-recommended-scss@17.0.0(postcss@8.5.6)(stylelint@17.2.0(typescript@5.9.3)): + stylelint-config-recommended-scss@17.0.0(postcss@8.5.6)(stylelint@17.3.0(typescript@5.9.3)): dependencies: postcss-scss: 4.0.9(postcss@8.5.6) - stylelint: 17.2.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.2.0(typescript@5.9.3)) - stylelint-scss: 7.0.0(stylelint@17.2.0(typescript@5.9.3)) + stylelint: 17.3.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.3.0(typescript@5.9.3)) + stylelint-scss: 7.0.0(stylelint@17.3.0(typescript@5.9.3)) optionalDependencies: postcss: 8.5.6 - stylelint-config-recommended@18.0.0(stylelint@17.2.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.3.0(typescript@5.9.3)): dependencies: - stylelint: 17.2.0(typescript@5.9.3) + stylelint: 17.3.0(typescript@5.9.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.6)(stylelint@17.2.0(typescript@5.9.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.6)(stylelint@17.3.0(typescript@5.9.3)): dependencies: - stylelint: 17.2.0(typescript@5.9.3) - stylelint-config-recommended-scss: 17.0.0(postcss@8.5.6)(stylelint@17.2.0(typescript@5.9.3)) - stylelint-config-standard: 40.0.0(stylelint@17.2.0(typescript@5.9.3)) + stylelint: 17.3.0(typescript@5.9.3) + stylelint-config-recommended-scss: 17.0.0(postcss@8.5.6)(stylelint@17.3.0(typescript@5.9.3)) + stylelint-config-standard: 40.0.0(stylelint@17.3.0(typescript@5.9.3)) optionalDependencies: postcss: 8.5.6 - stylelint-config-standard@40.0.0(stylelint@17.2.0(typescript@5.9.3)): + stylelint-config-standard@40.0.0(stylelint@17.3.0(typescript@5.9.3)): dependencies: - stylelint: 17.2.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.2.0(typescript@5.9.3)) + stylelint: 17.3.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.3.0(typescript@5.9.3)) - stylelint-scss@7.0.0(stylelint@17.2.0(typescript@5.9.3)): + stylelint-scss@7.0.0(stylelint@17.3.0(typescript@5.9.3)): dependencies: css-tree: 3.1.0 is-plain-object: 5.0.0 @@ -5389,11 +5389,11 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - stylelint: 17.2.0(typescript@5.9.3) + stylelint: 17.3.0(typescript@5.9.3) - stylelint@17.2.0(typescript@5.9.3): + stylelint@17.3.0(typescript@5.9.3): dependencies: - '@csstools/css-calc': 3.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-syntax-patches-for-csstree': 1.0.27 '@csstools/css-tokenizer': 4.0.0 diff --git a/web/src/pages/LocationsPage/LocationsPage.tsx b/web/src/pages/LocationsPage/LocationsPage.tsx index 7e2d269fe3..57ec0294d4 100644 --- a/web/src/pages/LocationsPage/LocationsPage.tsx +++ b/web/src/pages/LocationsPage/LocationsPage.tsx @@ -1,17 +1,20 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; +import { Suspense } from 'react'; import { Page } from '../../shared/components/Page/Page'; -import { getLocationsQueryOptions } from '../../shared/query'; import { LocationsTable } from './components/LocationsTable'; import { AddLocationModal } from './modals/AddLocationModal/AddLocationModal'; import './style.scss'; +import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton'; +import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout'; export const LocationsPage = () => { - const { data: locations } = useSuspenseQuery(getLocationsQueryOptions); - return ( <> - + }> + + + + diff --git a/web/src/pages/LocationsPage/components/LocationsTable.tsx b/web/src/pages/LocationsPage/components/LocationsTable.tsx index a399daea23..fb50206c71 100644 --- a/web/src/pages/LocationsPage/components/LocationsTable.tsx +++ b/web/src/pages/LocationsPage/components/LocationsTable.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { createColumnHelper, @@ -27,18 +27,20 @@ import { TableTop } from '../../../shared/defguard-ui/components/table/TableTop/ import { ThemeSpacing, ThemeVariable } from '../../../shared/defguard-ui/types'; import { openModal } from '../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; +import { + getLicenseInfoQueryOptions, + getLocationsQueryOptions, +} from '../../../shared/query'; import { tableSortingFns } from '../../../shared/utils/dateSortingFn'; import { useGatewayWizardStore } from '../../GatewaySetupPage/useGatewayWizardStore'; -type Props = { - locations: NetworkLocation[]; -}; - type RowData = NetworkLocation; const columnHelper = createColumnHelper(); -export const LocationsTable = ({ locations }: Props) => { +export const LocationsTable = () => { + const { data: locations } = useSuspenseQuery(getLocationsQueryOptions); + const { data: license } = useSuspenseQuery(getLicenseInfoQueryOptions); const navigate = useNavigate(); const [search, setSearch] = useState(''); @@ -66,10 +68,17 @@ export const LocationsTable = ({ locations }: Props) => { iconLeft: 'add-location', testId: 'add-location', onClick: () => { - openModal(ModalName.AddLocation); + if ( + license?.limits && + license.limits.locations.current === license.limits.locations.limit + ) { + openModal(ModalName.LimitReached); + } else { + openModal(ModalName.AddLocation); + } }, }), - [], + [license], ); const columns = useMemo( diff --git a/web/src/pages/PlaygroundPage/PlaygroundPage.tsx b/web/src/pages/PlaygroundPage/PlaygroundPage.tsx index 148f02633c..5fcb2e586c 100644 --- a/web/src/pages/PlaygroundPage/PlaygroundPage.tsx +++ b/web/src/pages/PlaygroundPage/PlaygroundPage.tsx @@ -4,6 +4,7 @@ import { CodeCard } from '../../shared/defguard-ui/components/CodeCard/CodeCard' import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { ThemeSpacing } from '../../shared/defguard-ui/types'; import './style.scss'; +import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import { useMemo, useState } from 'react'; import z from 'zod'; @@ -13,7 +14,6 @@ import { DestinationLabel } from '../../shared/components/DestinationLabel/Desti import { IpAssignmentCard } from '../../shared/components/IpAssignmentCard/IpAssignmentCard'; import { IpAssignmentDeviceSection } from '../../shared/components/IpAssignmentDeviceSection/IpAssignmentDeviceSection'; import { LoadingStep } from '../../shared/components/LoadingStep/LoadingStep'; -import { UpgradePlanModalManager } from '../../shared/components/modals/UpgradePlanModalManager/UpgradePlanModalManager'; import { SelectionSection } from '../../shared/components/SelectionSection/SelectionSection'; import type { SelectionOption, @@ -43,13 +43,15 @@ import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; import { openModal } from '../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../shared/hooks/modalControls/modalTypes'; +import { getLicenseInfoQueryOptions } from '../../shared/query'; import { FoldableRadioSection } from '../FoldableRadioSection/FoldableRadioSection'; import testIconSrc from './assets/actionable-test1.png'; export const PlaygroundPage = () => { return (
- + + @@ -175,27 +177,6 @@ export const PlaygroundPage = () => {
- - -