From e623251fa0701e596370981f6772c6ba04c181de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 7 May 2025 13:38:40 +0200 Subject: [PATCH 01/10] setup overview-index --- web/src/components/App/App.tsx | 11 +++- .../overview-index/OverviewIndexPage.tsx | 20 +++++++ web/src/pages/overview-index/style.scss | 36 ++++++++++++ .../overview/OverviewExpandable/style.scss | 6 +- .../ExpandableSection/ExpandableSection.tsx | 45 +++++++++++++++ .../Layout/ExpandableSection/style.scss | 57 +++++++++++++++++++ 6 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 web/src/pages/overview-index/OverviewIndexPage.tsx create mode 100644 web/src/pages/overview-index/style.scss create mode 100644 web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx create mode 100644 web/src/shared/components/Layout/ExpandableSection/style.scss diff --git a/web/src/components/App/App.tsx b/web/src/components/App/App.tsx index 2ea79c89a8..7c6ba65ad8 100644 --- a/web/src/components/App/App.tsx +++ b/web/src/components/App/App.tsx @@ -28,6 +28,7 @@ import { ProtectedRoute } from '../../shared/components/Router/Guards/ProtectedR import { ToastManager } from '../../shared/defguard-ui/components/Layout/ToastManager/ToastManager'; import { useAuthStore } from '../../shared/hooks/store/useAuthStore'; import { Navigation } from '../Navigation/Navigation'; +import { OverviewIndexPage } from '../../pages/overview-index/OverviewIndexPage'; const App = () => { const currentUser = useAuthStore((state) => state.user); @@ -59,7 +60,7 @@ const App = () => { + } @@ -96,6 +97,14 @@ const App = () => { } /> + + + + } + /> { + return ( + +
+
+

All locations overview

+
+
+ +

Summary indeed

+
+
+
+ ); +}; diff --git a/web/src/pages/overview-index/style.scss b/web/src/pages/overview-index/style.scss new file mode 100644 index 0000000000..e78f8a3cb9 --- /dev/null +++ b/web/src/pages/overview-index/style.scss @@ -0,0 +1,36 @@ +#overview-index { + &>.page-content { + --nav-width: 88px; + --page-spacing: var(--spacing-s); + --page-content-spacing: var(--spacing-s); + + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 100%; + + @include media-breakpoint-up(lg) { + --page-spacing: var(--spacing-xl); + } + + padding: var(--page-spacing) var(--page-spacing) var(--spacing-l); + + + &.nav-open { + --nav-width: 230px; + } + + &>div { + width: 100%; + max-width: calc(1920px - 2 * var(--page-spacing) - var(--nav-width)); + } + } +} + +#overview-index>.page-content>div { + &>header { + padding-bottom: var(--page-content-spacing); + } +} \ No newline at end of file diff --git a/web/src/pages/overview/OverviewExpandable/style.scss b/web/src/pages/overview/OverviewExpandable/style.scss index 652b3fcda8..245010c97a 100644 --- a/web/src/pages/overview/OverviewExpandable/style.scss +++ b/web/src/pages/overview/OverviewExpandable/style.scss @@ -1,7 +1,7 @@ .overview-expandable { width: 100%; - & > .header { + &>.header { display: flex; flex-flow: row; align-items: center; @@ -42,7 +42,7 @@ transition-duration: 100ms; transition-timing-function: ease-in-out; - & > div { + &>div { box-sizing: border-box; padding-top: 20px; overflow: hidden; @@ -53,4 +53,4 @@ grid-template-rows: 1fr; } } -} +} \ No newline at end of file diff --git a/web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx b/web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx new file mode 100644 index 0000000000..e671c952ec --- /dev/null +++ b/web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx @@ -0,0 +1,45 @@ +import './style.scss'; + +import clsx from 'clsx'; +import { PropsWithChildren, useState } from 'react'; + +import { ArrowSingle } from '../../../defguard-ui/components/icons/ArrowSingle/ArrowSingle'; +import { ArrowSingleDirection } from '../../../defguard-ui/components/icons/ArrowSingle/types'; + +type Props = { + text: string; + initOpen?: boolean; + textAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'; +} & PropsWithChildren; + +export const ExpandableSection = ({ + children, + text, + textAs: Tag = 'p', + initOpen = true, +}: Props) => { + const [expanded, setExpanded] = useState(initOpen); + + return ( +
+
{ + setExpanded((s) => !s); + }} + > + {text} + +
+
+
{children}
+
+
+ ); +}; diff --git a/web/src/shared/components/Layout/ExpandableSection/style.scss b/web/src/shared/components/Layout/ExpandableSection/style.scss new file mode 100644 index 0000000000..8075d0f68a --- /dev/null +++ b/web/src/shared/components/Layout/ExpandableSection/style.scss @@ -0,0 +1,57 @@ +.expandable-section { + &>.track { + width: 100%; + user-select: none; + cursor: pointer; + display: grid; + grid-template-columns: 1fr auto; + column-gap: var(--spacing-xs); + align-items: center; + justify-items: start; + + p { + width: 100%; + user-select: none; + } + + .arrow-single { + align-self: end; + width: 22px; + height: 22px; + + svg { + transition-property: transform; + + @include animate-standard; + } + } + } + + &>.expandable { + display: grid; + width: 100%; + transition-property: grid-template-rows; + + @include animate-standard(); + + &:not(.open) { + grid-template-rows: 0fr; + } + + &.open { + grid-template-rows: 1fr; + } + + &>div { + overflow: hidden + } + } +} + +.expandable-section .track p { + @include typography(app-side-bar); +} + +.expandable-section .track h2 { + @include typography(app-body-1); +} \ No newline at end of file From 1a3a0db356f66e2571abd118486a5b01daaf12dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Thu, 8 May 2025 10:41:59 +0200 Subject: [PATCH 02/10] index up --- .../overview-index/OverviewIndexPage.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/web/src/pages/overview-index/OverviewIndexPage.tsx b/web/src/pages/overview-index/OverviewIndexPage.tsx index 634844876e..18e2a9297e 100644 --- a/web/src/pages/overview-index/OverviewIndexPage.tsx +++ b/web/src/pages/overview-index/OverviewIndexPage.tsx @@ -1,9 +1,58 @@ import './style.scss'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + import { ExpandableSection } from '../../shared/components/Layout/ExpandableSection/ExpandableSection'; import { PageContainer } from '../../shared/components/Layout/PageContainer/PageContainer'; +import useApi from '../../shared/hooks/useApi'; +import { Network, WireguardNetworkStats } from '../../shared/types'; +import { sumBy } from 'lodash-es'; + +type OverviewIndexNetworkStats = Network & { + stats: WireguardNetworkStats; +}; export const OverviewIndexPage = () => { + const { + network: { getNetworks, getNetworkStats }, + } = useApi(); + + const query = useCallback(async () => { + const res: OverviewIndexNetworkStats[] = []; + const networks = await getNetworks(); + const stats: WireguardNetworkStats[] = []; + for (const network of networks) { + const stats = await getNetworkStats({ + id: network.id, + }); + res.push({ + ...network, + stats, + }); + } + return stats; + }, [getNetworkStats, getNetworks]); + + const { data } = useQuery({ + queryKey: ['overview-index'], + queryFn: query, + }); + + const summaryData = useMemo(() => { + if (!data) return undefined; + const res: WireguardNetworkStats = { + active_devices: sumBy(data, 'active_devices'), + active_users: sumBy(data, 'active_users'), + current_active_devices: sumBy(data, 'current_active_devices'), + current_active_users: sumBy(data, 'current_active_users'), + download: sumBy(data, 'download'), + upload: sumBy(data, 'upload'), + transfer_series: [], + }; + return res; + }, [data]); + return (
From cda9889bce61d0b72a3da4d399cbf7cddbe76615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Thu, 8 May 2025 12:13:35 +0200 Subject: [PATCH 03/10] overview index working wip update --- web/package.json | 46 +- web/pnpm-lock.yaml | 1331 +++++++++++------ web/src/components/App/App.tsx | 2 +- web/src/components/Navigation/Navigation.tsx | 7 + .../overview-index/OverviewIndexPage.tsx | 98 +- web/src/pages/overview-index/style.scss | 25 +- .../overview/OverviewExpandable/style.scss | 6 +- .../pages/overview/OverviewStats/style.scss | 439 +++--- .../ExpandableSection/ExpandableSection.tsx | 6 +- .../Layout/ExpandableSection/style.scss | 12 +- web/src/shared/defguard-ui | 2 +- 11 files changed, 1262 insertions(+), 712 deletions(-) diff --git a/web/package.json b/web/package.json index 95c91c34c6..98a3a9f7ef 100644 --- a/web/package.json +++ b/web/package.json @@ -47,10 +47,10 @@ "@react-rxjs/core": "^0.10.8", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", - "@tanstack/query-core": "^5.74.4", - "@tanstack/react-query": "^5.74.4", - "@tanstack/react-virtual": "3.13.6", - "@tanstack/virtual-core": "3.13.6", + "@tanstack/query-core": "^5.75.5", + "@tanstack/react-query": "^5.75.5", + "@tanstack/react-virtual": "3.13.8", + "@tanstack/virtual-core": "3.13.8", "@use-gesture/react": "^10.3.1", "axios": "^1.9.0", "byte-size": "^9.0.1", @@ -64,7 +64,7 @@ "events": "^3.3.0", "fast-deep-equal": "^3.1.3", "file-saver": "^2.0.5", - "framer-motion": "^12.9.1", + "framer-motion": "^12.10.2", "fuse.js": "^7.1.0", "get-text-width": "^1.0.3", "hex-rgb": "^5.0.0", @@ -81,15 +81,15 @@ "react-click-away-listener": "^2.4.0", "react-datepicker": "^8.3.0", "react-dom": "^18.2.0", - "react-hook-form": "^7.56.1", + "react-hook-form": "^7.56.3", "react-idle-timer": "^5.7.2", "react-is": "^19.1.0", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "react-qr-code": "^2.0.15", "react-resize-detector": "^12.0.2", - "react-router": "^6.21.3", - "react-router-dom": "^6.21.3", + "react-router": "6", + "react-router-dom": "6", "react-tracked": "^2.0.1", "react-virtualized-auto-sizer": "^1.0.26", "react-window": "^1.8.11", @@ -102,51 +102,51 @@ "text-case": "^1.0.9", "typesafe-i18n": "^5.26.2", "use-breakpoint": "^4.0.6", - "zod": "^3.24.3", - "zustand": "^5.0.3" + "zod": "^3.24.4", + "zustand": "^5.0.4" }, "devDependencies": { - "@babel/core": "^7.26.10", + "@babel/core": "^7.27.1", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", - "@eslint/js": "^9.25.1", + "@eslint/js": "^9.26.0", "@hookform/devtools": "^4.4.0", "@stylistic/eslint-plugin-ts": "^4.2.0", - "@tanstack/react-query-devtools": "^5.74.6", + "@tanstack/react-query-devtools": "^5.75.5", "@types/byte-size": "^8.1.2", "@types/file-saver": "^2.0.7", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", - "@types/node": "^22.15.2", + "@types/node": "^22.15.16", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@types/react-router-dom": "^5.3.3", "@types/react-window": "^1.8.8", - "@typescript-eslint/eslint-plugin": "^8.31.0", - "@typescript-eslint/parser": "^8.31.0", + "@typescript-eslint/eslint-plugin": "^8.32.0", + "@typescript-eslint/parser": "^8.32.0", "@vitejs/plugin-react-swc": "^3.9.0", "autoprefixer": "^10.4.21", "concurrently": "^9.1.2", "dotenv": "^16.5.0", - "esbuild": "^0.25.3", - "eslint": "^9.25.1", - "eslint-config-prettier": "^10.1.2", + "esbuild": "^0.25.4", + "eslint": "^9.26.0", + "eslint-config-prettier": "^10.1.3", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "^5.2.6", + "eslint-plugin-prettier": "^5.4.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-simple-import-sort": "^12.1.1", - "globals": "^16.0.0", + "globals": "^16.1.0", "postcss": "^8.5.3", "prettier": "^3.5.3", "sass": "1.70", "standard-version": "^9.5.0", "typescript": "~5.8.3", - "typescript-eslint": "^8.31.0", + "typescript-eslint": "^8.32.0", "typescript-eslint-language-service": "^5.0.5", - "vite": "^6.3.3", + "vite": "^6.3.5", "vite-plugin-eslint": "^1.8.1", "vite-plugin-package-version": "^1.1.0" } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a8c122dae2..4ac88043d2 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 2.1.1 '@hookform/resolvers': specifier: ^5.0.1 - version: 5.0.1(react-hook-form@7.56.1(react@18.2.0)) + version: 5.0.1(react-hook-form@7.56.3(react@18.2.0)) '@react-hook/resize-observer': specifier: ^2.0.2 version: 2.0.2(react@18.2.0) @@ -30,17 +30,17 @@ importers: specifier: ^2.0.1 version: 2.0.1 '@tanstack/query-core': - specifier: ^5.74.4 - version: 5.74.4 + specifier: ^5.75.5 + version: 5.75.5 '@tanstack/react-query': - specifier: ^5.74.4 - version: 5.74.4(react@18.2.0) + specifier: ^5.75.5 + version: 5.75.5(react@18.2.0) '@tanstack/react-virtual': - specifier: 3.13.6 - version: 3.13.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: 3.13.8 + version: 3.13.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tanstack/virtual-core': - specifier: 3.13.6 - version: 3.13.6 + specifier: 3.13.8 + version: 3.13.8 '@use-gesture/react': specifier: ^10.3.1 version: 10.3.1(react@18.2.0) @@ -81,8 +81,8 @@ importers: specifier: ^2.0.5 version: 2.0.5 framer-motion: - specifier: ^12.9.1 - version: 12.9.1(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^12.10.2 + version: 12.10.2(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -132,8 +132,8 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) react-hook-form: - specifier: ^7.56.1 - version: 7.56.1(react@18.2.0) + specifier: ^7.56.3 + version: 7.56.3(react@18.2.0) react-idle-timer: specifier: ^5.7.2 version: 5.7.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -153,10 +153,10 @@ importers: specifier: ^12.0.2 version: 12.0.2(react@18.2.0) react-router: - specifier: ^6.21.3 + specifier: '6' version: 6.21.3(react@18.2.0) react-router-dom: - specifier: ^6.21.3 + specifier: '6' version: 6.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-tracked: specifier: ^2.0.1 @@ -195,15 +195,15 @@ importers: specifier: ^4.0.6 version: 4.0.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) zod: - specifier: ^3.24.3 - version: 3.24.3 + specifier: ^3.24.4 + version: 3.24.4 zustand: - specifier: ^5.0.3 - version: 5.0.3(@types/react@18.2.48)(react@18.2.0)(use-sync-external-store@1.4.0(react@18.2.0)) + specifier: ^5.0.4 + version: 5.0.4(@types/react@18.2.48)(react@18.2.0)(use-sync-external-store@1.4.0(react@18.2.0)) devDependencies: '@babel/core': - specifier: ^7.26.10 - version: 7.26.10 + specifier: ^7.27.1 + version: 7.27.1 '@csstools/css-parser-algorithms': specifier: ^3.0.4 version: 3.0.4(@csstools/css-tokenizer@3.0.3) @@ -211,17 +211,17 @@ importers: specifier: ^3.0.3 version: 3.0.3 '@eslint/js': - specifier: ^9.25.1 - version: 9.25.1 + specifier: ^9.26.0 + version: 9.26.0 '@hookform/devtools': specifier: ^4.4.0 version: 4.4.0(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@stylistic/eslint-plugin-ts': specifier: ^4.2.0 - version: 4.2.0(eslint@9.25.1)(typescript@5.8.3) + version: 4.2.0(eslint@9.26.0)(typescript@5.8.3) '@tanstack/react-query-devtools': - specifier: ^5.74.6 - version: 5.74.6(@tanstack/react-query@5.74.4(react@18.2.0))(react@18.2.0) + specifier: ^5.75.5 + version: 5.75.5(@tanstack/react-query@5.75.5(react@18.2.0))(react@18.2.0) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -235,8 +235,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^22.15.2 - version: 22.15.2 + specifier: ^22.15.16 + version: 22.15.16 '@types/react': specifier: ^18.2.48 version: 18.2.48 @@ -250,14 +250,14 @@ importers: specifier: ^1.8.8 version: 1.8.8 '@typescript-eslint/eslint-plugin': - specifier: ^8.31.0 - version: 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) + specifier: ^8.32.0 + version: 8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3) '@typescript-eslint/parser': - specifier: ^8.31.0 - version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) + specifier: ^8.32.0 + version: 8.32.0(eslint@9.26.0)(typescript@5.8.3) '@vitejs/plugin-react-swc': specifier: ^3.9.0 - version: 3.9.0(vite@6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + version: 3.9.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) @@ -268,38 +268,38 @@ importers: specifier: ^16.5.0 version: 16.5.0 esbuild: - specifier: ^0.25.3 - version: 0.25.3 + specifier: ^0.25.4 + version: 0.25.4 eslint: - specifier: ^9.25.1 - version: 9.25.1 + specifier: ^9.26.0 + version: 9.26.0 eslint-config-prettier: - specifier: ^10.1.2 - version: 10.1.2(eslint@9.25.1) + specifier: ^10.1.3 + version: 10.1.3(eslint@9.26.0) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1) + version: 2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.25.1) + version: 6.10.2(eslint@9.26.0) eslint-plugin-prettier: - specifier: ^5.2.6 - version: 5.2.6(@types/eslint@8.56.2)(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.3) + specifier: ^5.4.0 + version: 5.4.0(@types/eslint@8.56.2)(eslint-config-prettier@10.1.3(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.25.1) + version: 7.37.5(eslint@9.26.0) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.25.1) + version: 5.2.0(eslint@9.26.0) eslint-plugin-react-refresh: specifier: ^0.4.20 - version: 0.4.20(eslint@9.25.1) + version: 0.4.20(eslint@9.26.0) eslint-plugin-simple-import-sort: specifier: ^12.1.1 - version: 12.1.1(eslint@9.25.1) + version: 12.1.1(eslint@9.26.0) globals: - specifier: ^16.0.0 - version: 16.0.0 + specifier: ^16.1.0 + version: 16.1.0 postcss: specifier: ^8.5.3 version: 8.5.3 @@ -316,20 +316,20 @@ importers: specifier: ~5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.31.0 - version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) + specifier: ^8.32.0 + version: 8.32.0(eslint@9.26.0)(typescript@5.8.3) typescript-eslint-language-service: specifier: ^5.0.5 - version: 5.0.5(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) + version: 5.0.5(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3) vite: - specifier: ^6.3.3 - version: 6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + specifier: ^6.3.5 + version: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) vite-plugin-eslint: specifier: ^1.8.1 - version: 1.8.1(eslint@9.25.1)(vite@6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + version: 1.8.1(eslint@9.26.0)(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) vite-plugin-package-version: specifier: ^1.1.0 - version: 1.1.0(vite@6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + version: 1.1.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) packages: @@ -345,32 +345,40 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.26.8': - resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/core@7.26.10': - resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + '@babel/compat-data@7.27.2': + resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} engines: {node: '>=6.9.0'} - '@babel/generator@7.26.9': - resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} + '@babel/core@7.27.1': + resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} engines: {node: '>=6.9.0'} '@babel/generator@7.27.0': resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.26.5': - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + '@babel/generator@7.27.1': + resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.25.9': resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.1': + resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -379,56 +387,64 @@ packages: resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.0': - resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.9': - resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} - engines: {node: '>=6.0.0'} - hasBin: true + '@babel/helpers@7.27.1': + resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} + engines: {node: '>=6.9.0'} '@babel/parser@7.27.0': resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.27.2': + resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.23.8': resolution: {integrity: sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==} engines: {node: '>=6.9.0'} - '@babel/template@7.26.9': - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} - engines: {node: '>=6.9.0'} - '@babel/template@7.27.0': resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.26.9': - resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} '@babel/traverse@7.27.0': resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} engines: {node: '>=6.9.0'} - '@babel/types@7.26.9': - resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} + '@babel/traverse@7.27.1': + resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} engines: {node: '>=6.9.0'} '@babel/types@7.27.0': resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + engines: {node: '>=6.9.0'} + '@csstools/css-parser-algorithms@3.0.4': resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} engines: {node: '>=18'} @@ -493,152 +509,152 @@ packages: '@emotion/weak-memoize@0.3.1': resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} - '@esbuild/aix-ppc64@0.25.3': - resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.3': - resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.3': - resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.3': - resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.3': - resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.3': - resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.3': - resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.3': - resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.3': - resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.3': - resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.3': - resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.3': - resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.3': - resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.3': - resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.3': - resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.3': - resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.3': - resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.3': - resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.3': - resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.3': - resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.3': - resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.3': - resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.3': - resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.3': - resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.3': - resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -649,6 +665,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -669,8 +691,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.25.1': - resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} + '@eslint/js@9.26.0': + resolution: {integrity: sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -762,6 +784,10 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@modelcontextprotocol/sdk@1.11.0': + resolution: {integrity: sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==} + engines: {node: '>=18'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1018,31 +1044,31 @@ packages: '@swc/types@0.1.21': resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==} - '@tanstack/query-core@5.74.4': - resolution: {integrity: sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A==} + '@tanstack/query-core@5.75.5': + resolution: {integrity: sha512-kPDOxtoMn2Ycycb76Givx2fi+2pzo98F9ifHL/NFiahEDpDwSVW6o12PRuQ0lQnBOunhRG5etatAhQij91M3MQ==} - '@tanstack/query-devtools@5.74.6': - resolution: {integrity: sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw==} + '@tanstack/query-devtools@5.74.7': + resolution: {integrity: sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==} - '@tanstack/react-query-devtools@5.74.6': - resolution: {integrity: sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q==} + '@tanstack/react-query-devtools@5.75.5': + resolution: {integrity: sha512-S31U00nJOQIbxydRH1kOwdLRaLBrda8O5QjzmgkRg60UZzPGdbI6+873Qa0YGUfPeILDbR2ukgWyg7CJQPy4iA==} peerDependencies: - '@tanstack/react-query': ^5.74.4 + '@tanstack/react-query': ^5.75.5 react: ^18 || ^19 - '@tanstack/react-query@5.74.4': - resolution: {integrity: sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q==} + '@tanstack/react-query@5.75.5': + resolution: {integrity: sha512-QrLCJe40BgBVlWdAdf2ZEVJ0cISOuEy/HKupId1aTKU6gPJZVhSvZpH+Si7csRflCJphzlQ77Yx6gUxGW9o0XQ==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-virtual@3.13.6': - resolution: {integrity: sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==} + '@tanstack/react-virtual@3.13.8': + resolution: {integrity: sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/virtual-core@3.13.6': - resolution: {integrity: sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==} + '@tanstack/virtual-core@3.13.8': + resolution: {integrity: sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==} '@types/byte-size@8.1.2': resolution: {integrity: sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==} @@ -1125,8 +1151,8 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - '@types/node@22.15.2': - resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} + '@types/node@22.15.16': + resolution: {integrity: sha512-3pr+KjwpVujqWqOKT8mNR+rd09FqhBLwg+5L/4t0cNYBzm/yEiYGCxWttjaPBsLtAo+WFNoXzGJfolM1JuRXoA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1161,16 +1187,16 @@ packages: '@types/unist@3.0.2': resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} - '@typescript-eslint/eslint-plugin@8.31.0': - resolution: {integrity: sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==} + '@typescript-eslint/eslint-plugin@8.32.0': + resolution: {integrity: sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.31.0': - resolution: {integrity: sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==} + '@typescript-eslint/parser@8.32.0': + resolution: {integrity: sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1180,12 +1206,12 @@ packages: resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.31.0': - resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==} + '@typescript-eslint/scope-manager@8.32.0': + resolution: {integrity: sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.31.0': - resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==} + '@typescript-eslint/type-utils@8.32.0': + resolution: {integrity: sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1195,8 +1221,8 @@ packages: resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.31.0': - resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==} + '@typescript-eslint/types@8.32.0': + resolution: {integrity: sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.26.0': @@ -1205,8 +1231,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.31.0': - resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==} + '@typescript-eslint/typescript-estree@8.32.0': + resolution: {integrity: sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' @@ -1218,8 +1244,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.31.0': - resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==} + '@typescript-eslint/utils@8.32.0': + resolution: {integrity: sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1229,8 +1255,8 @@ packages: resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.31.0': - resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==} + '@typescript-eslint/visitor-keys@8.32.0': + resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.2.0': @@ -1253,6 +1279,10 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1380,6 +1410,10 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1390,11 +1424,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.24.2: - resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.24.4: resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1412,6 +1441,10 @@ packages: '@75lb/nature': optional: true + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -1447,9 +1480,6 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniuse-lite@1.0.30001687: - resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==} - caniuse-lite@1.0.30001707: resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} @@ -1535,6 +1565,14 @@ packages: engines: {node: '>=18'} hasBin: true + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + conventional-changelog-angular@5.0.13: resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} engines: {node: '>=10'} @@ -1611,9 +1649,21 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -1747,6 +1797,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1812,18 +1866,22 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.128: resolution: {integrity: sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==} - electron-to-chromium@1.5.72: - resolution: {integrity: sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1878,8 +1936,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.3: - resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} engines: {node: '>=18'} hasBin: true @@ -1891,6 +1949,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1899,8 +1960,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-prettier@10.1.2: - resolution: {integrity: sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==} + eslint-config-prettier@10.1.3: + resolution: {integrity: sha512-vDo4d9yQE+cS2tdIT4J02H/16veRvkHgiLDRpej+WL67oCfbOb97itZXn8wMPJ/GsiEBVjrjs//AVNw2Cp1EcA==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -1945,8 +2006,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-prettier@5.2.6: - resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==} + eslint-plugin-prettier@5.4.0: + resolution: {integrity: sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -1993,8 +2054,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.25.1: - resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} + eslint@9.26.0: + resolution: {integrity: sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2029,6 +2090,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -2036,6 +2101,24 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.1: + resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.6: + resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@7.5.0: + resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} + engines: {node: '>= 16'} + peerDependencies: + express: ^4.11 || 5 || ^5.0.0-beta.1 + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2085,6 +2168,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -2127,11 +2214,15 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - framer-motion@12.9.1: - resolution: {integrity: sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==} + framer-motion@12.10.2: + resolution: {integrity: sha512-1Y1xDu6LD2geGlpmBAEdkO7SVsFtum6r1eUsKSAClOEX3acDb8dovOvf8TRduY54z1x0FiKFPxCrUAc3u84row==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2144,6 +2235,10 @@ packages: react-dom: optional: true + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2232,8 +2327,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.0.0: - resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} + globals@16.1.0: + resolution: {integrity: sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==} engines: {node: '>=18'} globalthis@1.0.4: @@ -2362,9 +2457,17 @@ packages: htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + humanize-duration@3.32.1: resolution: {integrity: sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2401,6 +2504,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipaddr.js@2.2.0: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} @@ -2512,6 +2619,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -2727,6 +2837,10 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -2734,6 +2848,10 @@ packages: resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} engines: {node: '>=10'} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-refs@1.3.0: resolution: {integrity: sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==} peerDependencies: @@ -2817,10 +2935,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2843,11 +2969,11 @@ packages: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} - motion-dom@12.9.1: - resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==} + motion-dom@12.10.2: + resolution: {integrity: sha512-482quPO/wVLghG9AHq5kEWBhqv0a5UN+Ap7LzgkpURM9SqTxOuuSQJsVWDqa/0WMWIDClXN3f71Reg7V21HYqA==} - motion-utils@12.8.3: - resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==} + motion-utils@12.9.4: + resolution: {integrity: sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2863,6 +2989,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -2931,6 +3061,13 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -2993,6 +3130,10 @@ packages: parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -3008,6 +3149,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -3035,6 +3180,10 @@ packages: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -3072,6 +3221,10 @@ packages: property-information@6.4.1: resolution: {integrity: sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-compare@3.0.1: resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} @@ -3098,6 +3251,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3109,6 +3266,14 @@ packages: resolution: {integrity: sha512-b0Zcf09AhqKS83btmUeYBS8tFK7XL2e3RvLmZcm0sTdF1/UUlHSsjXdCcWNxe7yfmAlPve5ym0DmKGtTzP6kVQ==} engines: {node: '>=14.18.0'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + react-click-away-listener@2.4.0: resolution: {integrity: sha512-jDkXY8Q9qM8e197K7c7AoVhhk2meQO5POyjRJrKN2vUQUvIef49h/paM3JA6q+lf+JygDy9ENOBOsZalARUIeg==} peerDependencies: @@ -3126,8 +3291,8 @@ packages: peerDependencies: react: ^18.2.0 - react-hook-form@7.56.1: - resolution: {integrity: sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==} + react-hook-form@7.56.3: + resolution: {integrity: sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -3324,6 +3489,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3352,6 +3521,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass@1.70.0: resolution: {integrity: sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==} engines: {node: '>=14.0.0'} @@ -3381,6 +3553,14 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -3400,6 +3580,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3472,6 +3655,10 @@ packages: engines: {node: '>=10'} hasBin: true + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3652,6 +3839,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3672,6 +3863,12 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -3697,6 +3894,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3729,8 +3930,8 @@ packages: eslint: '>= 8.0.0' typescript: '>= 4.0.0' - typescript-eslint@8.31.0: - resolution: {integrity: sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==} + typescript-eslint@8.32.0: + resolution: {integrity: sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3774,6 +3975,10 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -3816,6 +4021,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -3839,8 +4048,8 @@ packages: peerDependencies: vite: '>=2.0.0-beta.69' - vite@6.3.3: - resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -3917,6 +4126,9 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -3971,11 +4183,16 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.24.3: - resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + + zod@3.24.4: + resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} - zustand@5.0.3: - resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + zustand@5.0.4: + resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -4010,20 +4227,26 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.26.8': {} + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.2': {} - '@babel/core@7.26.10': + '@babel/core@7.27.1': dependencies: '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.26.9 - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helpers': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 convert-source-map: 2.0.0 debug: 4.4.0 gensync: 1.0.0-beta.2 @@ -4032,74 +4255,79 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.26.9': + '@babel/generator@7.27.0': dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/generator@7.27.0': + '@babel/generator@7.27.1': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.26.5': + '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.26.8 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.2 + '@babel/compat-data': 7.27.2 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.4 lru-cache: 5.1.1 semver: 6.3.1 '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} - '@babel/helpers@7.27.0': - dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/helper-validator-option@7.27.1': {} - '@babel/parser@7.26.9': + '@babel/helpers@7.27.1': dependencies: - '@babel/types': 7.26.9 + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 '@babel/parser@7.27.0': dependencies: '@babel/types': 7.27.0 - '@babel/runtime@7.23.8': + '@babel/parser@7.27.2': dependencies: - regenerator-runtime: 0.14.1 + '@babel/types': 7.27.1 - '@babel/template@7.26.9': + '@babel/runtime@7.23.8': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + regenerator-runtime: 0.14.1 '@babel/template@7.27.0': dependencies: @@ -4107,17 +4335,11 @@ snapshots: '@babel/parser': 7.27.0 '@babel/types': 7.27.0 - '@babel/traverse@7.26.9': + '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 - debug: 4.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 '@babel/traverse@7.27.0': dependencies: @@ -4131,16 +4353,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.26.9': + '@babel/traverse@7.27.1': dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color '@babel/types@7.27.0': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.27.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-tokenizer': 3.0.3 @@ -4230,84 +4464,89 @@ snapshots: '@emotion/weak-memoize@0.3.1': {} - '@esbuild/aix-ppc64@0.25.3': + '@esbuild/aix-ppc64@0.25.4': optional: true - '@esbuild/android-arm64@0.25.3': + '@esbuild/android-arm64@0.25.4': optional: true - '@esbuild/android-arm@0.25.3': + '@esbuild/android-arm@0.25.4': optional: true - '@esbuild/android-x64@0.25.3': + '@esbuild/android-x64@0.25.4': optional: true - '@esbuild/darwin-arm64@0.25.3': + '@esbuild/darwin-arm64@0.25.4': optional: true - '@esbuild/darwin-x64@0.25.3': + '@esbuild/darwin-x64@0.25.4': optional: true - '@esbuild/freebsd-arm64@0.25.3': + '@esbuild/freebsd-arm64@0.25.4': optional: true - '@esbuild/freebsd-x64@0.25.3': + '@esbuild/freebsd-x64@0.25.4': optional: true - '@esbuild/linux-arm64@0.25.3': + '@esbuild/linux-arm64@0.25.4': optional: true - '@esbuild/linux-arm@0.25.3': + '@esbuild/linux-arm@0.25.4': optional: true - '@esbuild/linux-ia32@0.25.3': + '@esbuild/linux-ia32@0.25.4': optional: true - '@esbuild/linux-loong64@0.25.3': + '@esbuild/linux-loong64@0.25.4': optional: true - '@esbuild/linux-mips64el@0.25.3': + '@esbuild/linux-mips64el@0.25.4': optional: true - '@esbuild/linux-ppc64@0.25.3': + '@esbuild/linux-ppc64@0.25.4': optional: true - '@esbuild/linux-riscv64@0.25.3': + '@esbuild/linux-riscv64@0.25.4': optional: true - '@esbuild/linux-s390x@0.25.3': + '@esbuild/linux-s390x@0.25.4': optional: true - '@esbuild/linux-x64@0.25.3': + '@esbuild/linux-x64@0.25.4': optional: true - '@esbuild/netbsd-arm64@0.25.3': + '@esbuild/netbsd-arm64@0.25.4': optional: true - '@esbuild/netbsd-x64@0.25.3': + '@esbuild/netbsd-x64@0.25.4': optional: true - '@esbuild/openbsd-arm64@0.25.3': + '@esbuild/openbsd-arm64@0.25.4': optional: true - '@esbuild/openbsd-x64@0.25.3': + '@esbuild/openbsd-x64@0.25.4': optional: true - '@esbuild/sunos-x64@0.25.3': + '@esbuild/sunos-x64@0.25.4': optional: true - '@esbuild/win32-arm64@0.25.3': + '@esbuild/win32-arm64@0.25.4': optional: true - '@esbuild/win32-ia32@0.25.3': + '@esbuild/win32-ia32@0.25.4': optional: true - '@esbuild/win32-x64@0.25.3': + '@esbuild/win32-x64@0.25.4': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.25.1)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.26.0)': + dependencies: + eslint: 9.26.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.7.0(eslint@9.26.0)': dependencies: - eslint: 9.25.1 + eslint: 9.26.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -4340,7 +4579,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.25.1': {} + '@eslint/js@9.26.0': {} '@eslint/object-schema@2.1.6': {} @@ -4392,10 +4631,10 @@ snapshots: - '@types/react' - supports-color - '@hookform/resolvers@5.0.1(react-hook-form@7.56.1(react@18.2.0))': + '@hookform/resolvers@5.0.1(react-hook-form@7.56.3(react@18.2.0))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.56.1(react@18.2.0) + react-hook-form: 7.56.3(react@18.2.0) '@humanfs/core@0.19.1': {} @@ -4435,6 +4674,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + '@modelcontextprotocol/sdk@1.11.0': + dependencies: + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.6 + express: 5.1.0 + express-rate-limit: 7.5.0(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.24.4 + zod-to-json-schema: 3.24.5(zod@3.24.4) + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4569,10 +4823,10 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@stylistic/eslint-plugin-ts@4.2.0(eslint@9.25.1)(typescript@5.8.3)': + '@stylistic/eslint-plugin-ts@4.2.0(eslint@9.26.0)(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.26.0(eslint@9.25.1)(typescript@5.8.3) - eslint: 9.25.1 + '@typescript-eslint/utils': 8.26.0(eslint@9.26.0)(typescript@5.8.3) + eslint: 9.26.0 eslint-visitor-keys: 4.2.0 espree: 10.3.0 transitivePeerDependencies: @@ -4631,28 +4885,28 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.74.4': {} + '@tanstack/query-core@5.75.5': {} - '@tanstack/query-devtools@5.74.6': {} + '@tanstack/query-devtools@5.74.7': {} - '@tanstack/react-query-devtools@5.74.6(@tanstack/react-query@5.74.4(react@18.2.0))(react@18.2.0)': + '@tanstack/react-query-devtools@5.75.5(@tanstack/react-query@5.75.5(react@18.2.0))(react@18.2.0)': dependencies: - '@tanstack/query-devtools': 5.74.6 - '@tanstack/react-query': 5.74.4(react@18.2.0) + '@tanstack/query-devtools': 5.74.7 + '@tanstack/react-query': 5.75.5(react@18.2.0) react: 18.2.0 - '@tanstack/react-query@5.74.4(react@18.2.0)': + '@tanstack/react-query@5.75.5(react@18.2.0)': dependencies: - '@tanstack/query-core': 5.74.4 + '@tanstack/query-core': 5.75.5 react: 18.2.0 - '@tanstack/react-virtual@3.13.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@tanstack/react-virtual@3.13.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@tanstack/virtual-core': 3.13.6 + '@tanstack/virtual-core': 3.13.8 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@tanstack/virtual-core@3.13.6': {} + '@tanstack/virtual-core@3.13.8': {} '@types/byte-size@8.1.2': {} @@ -4729,7 +4983,7 @@ snapshots: '@types/ms@0.7.34': {} - '@types/node@22.15.2': + '@types/node@22.15.16': dependencies: undici-types: 6.21.0 @@ -4770,31 +5024,31 @@ snapshots: '@types/unist@3.0.2': {} - '@typescript-eslint/eslint-plugin@8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.31.0 - '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.31.0 - eslint: 9.25.1 + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.32.0 + '@typescript-eslint/type-utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.32.0 + eslint: 9.26.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.8.3) + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3)': + '@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.31.0 - '@typescript-eslint/types': 8.31.0 - '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.31.0 + '@typescript-eslint/scope-manager': 8.32.0 + '@typescript-eslint/types': 8.32.0 + '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.32.0 debug: 4.4.0 - eslint: 9.25.1 + eslint: 9.26.0 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -4804,25 +5058,25 @@ snapshots: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/scope-manager@8.31.0': + '@typescript-eslint/scope-manager@8.32.0': dependencies: - '@typescript-eslint/types': 8.31.0 - '@typescript-eslint/visitor-keys': 8.31.0 + '@typescript-eslint/types': 8.32.0 + '@typescript-eslint/visitor-keys': 8.32.0 - '@typescript-eslint/type-utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.32.0(eslint@9.26.0)(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) debug: 4.4.0 - eslint: 9.25.1 - ts-api-utils: 2.0.1(typescript@5.8.3) + eslint: 9.26.0 + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/types@8.31.0': {} + '@typescript-eslint/types@8.32.0': {} '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.3)': dependencies: @@ -4838,38 +5092,38 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.31.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.32.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.31.0 - '@typescript-eslint/visitor-keys': 8.31.0 + '@typescript-eslint/types': 8.32.0 + '@typescript-eslint/visitor-keys': 8.32.0 debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 2.0.1(typescript@5.8.3) + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.26.0(eslint@9.25.1)(typescript@5.8.3)': + '@typescript-eslint/utils@8.26.0(eslint@9.26.0)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.25.1) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.26.0) '@typescript-eslint/scope-manager': 8.26.0 '@typescript-eslint/types': 8.26.0 '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.3) - eslint: 9.25.1 + eslint: 9.26.0 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': + '@typescript-eslint/utils@8.32.0(eslint@9.26.0)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.25.1) - '@typescript-eslint/scope-manager': 8.31.0 - '@typescript-eslint/types': 8.31.0 - '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) - eslint: 9.25.1 + '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0) + '@typescript-eslint/scope-manager': 8.32.0 + '@typescript-eslint/types': 8.32.0 + '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.8.3) + eslint: 9.26.0 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -4879,9 +5133,9 @@ snapshots: '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.31.0': + '@typescript-eslint/visitor-keys@8.32.0': dependencies: - '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/types': 8.32.0 eslint-visitor-keys: 4.2.0 '@ungap/structured-clone@1.2.0': {} @@ -4893,10 +5147,10 @@ snapshots: '@use-gesture/core': 10.3.1 react: 18.2.0 - '@vitejs/plugin-react-swc@3.9.0(vite@6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': + '@vitejs/plugin-react-swc@3.9.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': dependencies: '@swc/core': 1.11.21 - vite: 6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + vite: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) transitivePeerDependencies: - '@swc/helpers' @@ -4905,6 +5159,11 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -5058,6 +5317,20 @@ snapshots: binary-extensions@2.2.0: {} + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -5071,13 +5344,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.24.2: - dependencies: - caniuse-lite: 1.0.30001687 - electron-to-chromium: 1.5.72 - node-releases: 2.0.19 - update-browserslist-db: 1.1.1(browserslist@4.24.2) - browserslist@4.24.4: dependencies: caniuse-lite: 1.0.30001707 @@ -5089,6 +5355,8 @@ snapshots: byte-size@9.0.1: {} + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0 @@ -5132,8 +5400,6 @@ snapshots: camelcase@5.3.1: {} - caniuse-lite@1.0.30001687: {} - caniuse-lite@1.0.30001707: {} ccount@2.0.1: {} @@ -5236,6 +5502,12 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + conventional-changelog-angular@5.0.13: dependencies: compare-func: 2.0.0 @@ -5352,8 +5624,17 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -5475,6 +5756,8 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} detect-browser@5.3.0: {} @@ -5543,14 +5826,16 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.128: {} + ee-first@1.1.1: {} - electron-to-chromium@1.5.72: {} + electron-to-chromium@1.5.128: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + entities@4.5.0: {} entities@6.0.0: {} @@ -5752,45 +6037,47 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.3: + esbuild@0.25.4: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.3 - '@esbuild/android-arm': 0.25.3 - '@esbuild/android-arm64': 0.25.3 - '@esbuild/android-x64': 0.25.3 - '@esbuild/darwin-arm64': 0.25.3 - '@esbuild/darwin-x64': 0.25.3 - '@esbuild/freebsd-arm64': 0.25.3 - '@esbuild/freebsd-x64': 0.25.3 - '@esbuild/linux-arm': 0.25.3 - '@esbuild/linux-arm64': 0.25.3 - '@esbuild/linux-ia32': 0.25.3 - '@esbuild/linux-loong64': 0.25.3 - '@esbuild/linux-mips64el': 0.25.3 - '@esbuild/linux-ppc64': 0.25.3 - '@esbuild/linux-riscv64': 0.25.3 - '@esbuild/linux-s390x': 0.25.3 - '@esbuild/linux-x64': 0.25.3 - '@esbuild/netbsd-arm64': 0.25.3 - '@esbuild/netbsd-x64': 0.25.3 - '@esbuild/openbsd-arm64': 0.25.3 - '@esbuild/openbsd-x64': 0.25.3 - '@esbuild/sunos-x64': 0.25.3 - '@esbuild/win32-arm64': 0.25.3 - '@esbuild/win32-ia32': 0.25.3 - '@esbuild/win32-x64': 0.25.3 + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 escalade@3.1.1: {} escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.2(eslint@9.25.1): + eslint-config-prettier@10.1.3(eslint@9.26.0): dependencies: - eslint: 9.25.1 + eslint: 9.26.0 eslint-import-resolver-node@0.3.9: dependencies: @@ -5800,17 +6087,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.25.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.26.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - eslint: 9.25.1 + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + eslint: 9.26.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5819,9 +6106,9 @@ snapshots: array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.25.1 + eslint: 9.26.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.25.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.26.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -5833,13 +6120,13 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.1): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.26.0): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 @@ -5849,7 +6136,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.25.1 + eslint: 9.26.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -5858,25 +6145,25 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@5.2.6(@types/eslint@8.56.2)(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.3): + eslint-plugin-prettier@5.4.0(@types/eslint@8.56.2)(eslint-config-prettier@10.1.3(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3): dependencies: - eslint: 9.25.1 + eslint: 9.26.0 prettier: 3.5.3 prettier-linter-helpers: 1.0.0 synckit: 0.11.2 optionalDependencies: '@types/eslint': 8.56.2 - eslint-config-prettier: 10.1.2(eslint@9.25.1) + eslint-config-prettier: 10.1.3(eslint@9.26.0) - eslint-plugin-react-hooks@5.2.0(eslint@9.25.1): + eslint-plugin-react-hooks@5.2.0(eslint@9.26.0): dependencies: - eslint: 9.25.1 + eslint: 9.26.0 - eslint-plugin-react-refresh@0.4.20(eslint@9.25.1): + eslint-plugin-react-refresh@0.4.20(eslint@9.26.0): dependencies: - eslint: 9.25.1 + eslint: 9.26.0 - eslint-plugin-react@7.37.5(eslint@9.25.1): + eslint-plugin-react@7.37.5(eslint@9.26.0): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -5884,7 +6171,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.25.1 + eslint: 9.26.0 estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -5898,9 +6185,9 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-simple-import-sort@12.1.1(eslint@9.25.1): + eslint-plugin-simple-import-sort@12.1.1(eslint@9.26.0): dependencies: - eslint: 9.25.1 + eslint: 9.26.0 eslint-scope@8.3.0: dependencies: @@ -5911,19 +6198,20 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.25.1: + eslint@9.26.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.25.1) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.26.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.0 '@eslint/config-helpers': 0.2.1 '@eslint/core': 0.13.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.25.1 + '@eslint/js': 9.26.0 '@eslint/plugin-kit': 0.2.8 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.2 + '@modelcontextprotocol/sdk': 1.11.0 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 @@ -5948,6 +6236,7 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.3 + zod: 3.24.4 transitivePeerDependencies: - supports-color @@ -5973,10 +6262,54 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@4.0.7: {} events@3.3.0: {} + eventsource-parser@3.0.1: {} + + eventsource@3.0.6: + dependencies: + eventsource-parser: 3.0.1 + + express-rate-limit@7.5.0(express@5.1.0): + dependencies: + express: 5.1.0 + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -6019,6 +6352,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-root@1.1.0: {} find-up@2.1.0: @@ -6058,18 +6402,22 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + forwarded@0.2.0: {} + fraction.js@4.3.7: {} - framer-motion@12.9.1(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + framer-motion@12.10.2(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - motion-dom: 12.9.1 - motion-utils: 12.8.3 + motion-dom: 12.10.2 + motion-utils: 12.9.4 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.2.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + fresh@2.0.0: {} + fsevents@2.3.3: optional: true @@ -6183,7 +6531,7 @@ snapshots: globals@14.0.0: {} - globals@16.0.0: {} + globals@16.1.0: {} globalthis@1.0.4: dependencies: @@ -6362,8 +6710,20 @@ snapshots: domutils: 3.2.2 entities: 6.0.0 + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + humanize-duration@3.32.1: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} immutable@4.3.4: {} @@ -6391,6 +6751,8 @@ snapshots: internmap@2.0.3: {} + ipaddr.js@1.9.1: {} + ipaddr.js@2.2.0: {} is-absolute-url@4.0.1: {} @@ -6483,6 +6845,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-regex@1.1.4: dependencies: call-bind: 1.0.8 @@ -6748,6 +7112,8 @@ snapshots: dependencies: '@types/mdast': 4.0.3 + media-typer@1.1.0: {} + memoize-one@5.2.1: {} meow@8.1.2: @@ -6764,6 +7130,8 @@ snapshots: type-fest: 0.18.1 yargs-parser: 20.2.9 + merge-descriptors@2.0.0: {} + merge-refs@1.3.0(@types/react@18.2.48): optionalDependencies: '@types/react': 18.2.48 @@ -6910,10 +7278,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + min-indent@1.0.1: {} minimatch@3.1.2: @@ -6934,11 +7308,11 @@ snapshots: modify-values@1.0.1: {} - motion-dom@12.9.1: + motion-dom@12.10.2: dependencies: - motion-utils: 12.8.3 + motion-utils: 12.9.4 - motion-utils@12.8.3: {} + motion-utils@12.9.4: {} ms@2.1.3: {} @@ -6948,6 +7322,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} node-releases@2.0.19: {} @@ -7035,6 +7411,14 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.3: dependencies: '@aashutoshrathi/word-wrap': 1.2.6 @@ -7113,6 +7497,8 @@ snapshots: dependencies: entities: 4.5.0 + parseurl@1.3.3: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -7121,6 +7507,8 @@ snapshots: path-parse@1.0.7: {} + path-to-regexp@8.2.0: {} + path-type@3.0.0: dependencies: pify: 3.0.0 @@ -7137,6 +7525,8 @@ snapshots: pify@3.0.0: {} + pkce-challenge@5.0.0: {} + pngjs@5.0.0: {} possible-typed-array-names@1.0.0: {} @@ -7167,6 +7557,11 @@ snapshots: property-information@6.4.1: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-compare@3.0.1: {} proxy-from-env@1.1.0: {} @@ -7183,12 +7578,25 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} quick-lru@4.0.1: {} radash@12.1.0: {} + range-parser@1.2.1: {} + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + react-click-away-listener@2.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0 @@ -7208,7 +7616,7 @@ snapshots: react: 18.2.0 scheduler: 0.23.0 - react-hook-form@7.56.1(react@18.2.0): + react-hook-form@7.56.3(react@18.2.0): dependencies: react: 18.2.0 @@ -7486,6 +7894,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.9 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.0 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7523,6 +7941,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + sass@1.70.0: dependencies: chokidar: 3.5.3 @@ -7545,6 +7965,31 @@ snapshots: semver@7.6.3: {} + send@1.2.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-function-length@1.2.0: @@ -7577,6 +8022,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.0.0 + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -7668,6 +8115,8 @@ snapshots: stringify-package: 1.0.1 yargs: 16.2.0 + statuses@2.0.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -7905,6 +8354,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -7917,6 +8368,10 @@ snapshots: dependencies: typescript: 5.8.3 + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -7938,6 +8393,12 @@ snapshots: type-fest@0.8.1: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.3 @@ -7977,18 +8438,18 @@ snapshots: dependencies: typescript: 5.8.3 - typescript-eslint-language-service@5.0.5(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3): + typescript-eslint-language-service@5.0.5(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3): dependencies: - '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - eslint: 9.25.1 + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + eslint: 9.26.0 typescript: 5.8.3 - typescript-eslint@8.31.0(eslint@9.25.1)(typescript@5.8.3): + typescript-eslint@8.32.0(eslint@9.26.0)(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) - '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - eslint: 9.25.1 + '@typescript-eslint/eslint-plugin': 8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + eslint: 9.26.0 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8045,11 +8506,7 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - update-browserslist-db@1.1.1(browserslist@4.24.2): - dependencies: - browserslist: 4.24.2 - escalade: 3.2.0 - picocolors: 1.1.1 + unpipe@1.0.0: {} update-browserslist-db@1.1.1(browserslist@4.24.4): dependencies: @@ -8090,6 +8547,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.2 @@ -8123,28 +8582,28 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-eslint@1.8.1(eslint@9.25.1)(vite@6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): + vite-plugin-eslint@1.8.1(eslint@9.26.0)(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): dependencies: '@rollup/pluginutils': 4.2.1 '@types/eslint': 8.56.2 - eslint: 9.25.1 + eslint: 9.26.0 rollup: 2.79.2 - vite: 6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + vite: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) - vite-plugin-package-version@1.1.0(vite@6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): + vite-plugin-package-version@1.1.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): dependencies: - vite: 6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + vite: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) - vite@6.3.3(@types/node@22.15.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1): + vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1): dependencies: - esbuild: 0.25.3 + esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) picomatch: 4.0.2 postcss: 8.5.3 rollup: 4.34.9 tinyglobby: 0.2.13 optionalDependencies: - '@types/node': 22.15.2 + '@types/node': 22.15.16 fsevents: 2.3.3 sass: 1.70.0 terser: 5.37.0 @@ -8212,6 +8671,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + xtend@4.0.2: {} y18n@4.0.3: {} @@ -8272,9 +8733,13 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.24.3: {} + zod-to-json-schema@3.24.5(zod@3.24.4): + dependencies: + zod: 3.24.4 + + zod@3.24.4: {} - zustand@5.0.3(@types/react@18.2.48)(react@18.2.0)(use-sync-external-store@1.4.0(react@18.2.0)): + zustand@5.0.4(@types/react@18.2.48)(react@18.2.0)(use-sync-external-store@1.4.0(react@18.2.0)): optionalDependencies: '@types/react': 18.2.48 react: 18.2.0 diff --git a/web/src/components/App/App.tsx b/web/src/components/App/App.tsx index 7c6ba65ad8..2fe67d888d 100644 --- a/web/src/components/App/App.tsx +++ b/web/src/components/App/App.tsx @@ -13,6 +13,7 @@ import { GroupsPage } from '../../pages/groups/GroupsPage'; import { NetworkPage } from '../../pages/network/NetworkPage'; import { OpenidClientsListPage } from '../../pages/openid/OpenidClientsListPage/OpenidClientsListPage'; import { OverviewPage } from '../../pages/overview/OverviewPage'; +import { OverviewIndexPage } from '../../pages/overview-index/OverviewIndexPage'; import { ProvisionersPage } from '../../pages/provisioners/ProvisionersPage'; import { SettingsPage } from '../../pages/settings/SettingsPage'; import { SupportPage } from '../../pages/support/SupportPage'; @@ -28,7 +29,6 @@ import { ProtectedRoute } from '../../shared/components/Router/Guards/ProtectedR import { ToastManager } from '../../shared/defguard-ui/components/Layout/ToastManager/ToastManager'; import { useAuthStore } from '../../shared/hooks/store/useAuthStore'; import { Navigation } from '../Navigation/Navigation'; -import { OverviewIndexPage } from '../../pages/overview-index/OverviewIndexPage'; const App = () => { const currentUser = useAuthStore((state) => state.user); diff --git a/web/src/components/Navigation/Navigation.tsx b/web/src/components/Navigation/Navigation.tsx index c0459df94f..ac56892266 100644 --- a/web/src/components/Navigation/Navigation.tsx +++ b/web/src/components/Navigation/Navigation.tsx @@ -88,6 +88,13 @@ export const Navigation = () => { }, ]; let middle: NavigationItem[] = [ + { + title: 'Overview Index wip', + linkPath: '/admin/overview-index', + enabled: true, + adminOnly: true, + icon: , + }, { title: LL.navigation.bar.overview(), linkPath: overviewLink, diff --git a/web/src/pages/overview-index/OverviewIndexPage.tsx b/web/src/pages/overview-index/OverviewIndexPage.tsx index 18e2a9297e..eb9a2d589b 100644 --- a/web/src/pages/overview-index/OverviewIndexPage.tsx +++ b/web/src/pages/overview-index/OverviewIndexPage.tsx @@ -1,27 +1,74 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; +import { sumBy } from 'lodash-es'; import { useCallback, useMemo } from 'react'; import { ExpandableSection } from '../../shared/components/Layout/ExpandableSection/ExpandableSection'; import { PageContainer } from '../../shared/components/Layout/PageContainer/PageContainer'; +import { GatewaysStatus } from '../../shared/components/network/GatewaysStatus/GatewaysStatus'; +import { Button } from '../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../shared/defguard-ui/components/Layout/Button/types'; +import { isPresent } from '../../shared/defguard-ui/utils/isPresent'; import useApi from '../../shared/hooks/useApi'; -import { Network, WireguardNetworkStats } from '../../shared/types'; -import { sumBy } from 'lodash-es'; +import { useToaster } from '../../shared/hooks/useToaster'; +import { Network, NetworkSpeedStats, WireguardNetworkStats } from '../../shared/types'; +import { OverviewStats } from '../overview/OverviewStats/OverviewStats'; type OverviewIndexNetworkStats = Network & { stats: WireguardNetworkStats; }; +type NetworkSpeedTickData = { + upload: number; + download: number; +}; + +type SumMap = Record; + +const sumTickData = ( + a: NetworkSpeedTickData, + b: NetworkSpeedTickData, +): NetworkSpeedTickData => { + return { + download: a.download + b.download, + upload: a.upload + b.upload, + }; +}; + +const sumTransferSeries = (transferStats: NetworkSpeedStats[][]): NetworkSpeedStats[] => { + const sumMap: SumMap = {}; + for (const stats of transferStats) { + for (const tick of stats) { + const tickValue = sumMap[tick.collected_at]; + if (isPresent(tickValue)) { + sumMap[tick.collected_at] = sumTickData(tickValue, tick); + } else { + sumMap[tick.collected_at] = tick; + } + } + } + const res: NetworkSpeedStats[] = []; + for (const sumMapKey of Object.keys(sumMap)) { + const value = sumMap[sumMapKey]; + res.push({ collected_at: sumMapKey, download: value.download, upload: value.upload }); + } + return res; +}; + export const OverviewIndexPage = () => { const { network: { getNetworks, getNetworkStats }, } = useApi(); + const toaster = useToaster(); + const query = useCallback(async () => { const res: OverviewIndexNetworkStats[] = []; const networks = await getNetworks(); - const stats: WireguardNetworkStats[] = []; for (const network of networks) { const stats = await getNetworkStats({ id: network.id, @@ -31,38 +78,63 @@ export const OverviewIndexPage = () => { stats, }); } - return stats; + return res; }, [getNetworkStats, getNetworks]); const { data } = useQuery({ queryKey: ['overview-index'], queryFn: query, + refetchInterval: 10 * 1000, }); const summaryData = useMemo(() => { if (!data) return undefined; const res: WireguardNetworkStats = { - active_devices: sumBy(data, 'active_devices'), - active_users: sumBy(data, 'active_users'), - current_active_devices: sumBy(data, 'current_active_devices'), - current_active_users: sumBy(data, 'current_active_users'), - download: sumBy(data, 'download'), - upload: sumBy(data, 'upload'), - transfer_series: [], + active_devices: sumBy(data, 'stats.active_devices'), + active_users: sumBy(data, 'stats.active_users'), + current_active_devices: sumBy(data, 'stats.current_active_devices'), + current_active_users: sumBy(data, 'stats.current_active_users'), + download: sumBy(data, 'stats.download'), + upload: sumBy(data, 'stats.upload'), + transfer_series: sumTransferSeries( + data.map((network) => network.stats.transfer_series), + ), }; return res; }, [data]); return ( -
+

All locations overview

-

Summary indeed

+ {isPresent(summaryData) && }
+ {isPresent(data) && + data.map((network) => ( + +
+ +
+ +
+ ))}
); diff --git a/web/src/pages/overview-index/style.scss b/web/src/pages/overview-index/style.scss index e78f8a3cb9..58a65be44b 100644 --- a/web/src/pages/overview-index/style.scss +++ b/web/src/pages/overview-index/style.scss @@ -1,5 +1,5 @@ #overview-index { - &>.page-content { + & > .page-content { --nav-width: 88px; --page-spacing: var(--spacing-s); --page-content-spacing: var(--spacing-s); @@ -9,7 +9,6 @@ align-items: center; justify-content: center; box-sizing: border-box; - width: 100%; @include media-breakpoint-up(lg) { --page-spacing: var(--spacing-xl); @@ -17,20 +16,30 @@ padding: var(--page-spacing) var(--page-spacing) var(--spacing-l); - &.nav-open { --nav-width: 230px; } - &>div { + & > div { width: 100%; max-width: calc(1920px - 2 * var(--page-spacing) - var(--nav-width)); } } } -#overview-index>.page-content>div { - &>header { - padding-bottom: var(--page-content-spacing); +#overview-index > .page-content > div { + display: flex; + flex-flow: column; + row-gap: var(--page-content-spacing); +} + +#overview-index { + .network-section { + .top-track { + display: flex; + flex-flow: row; + align-items: center; + padding-bottom: var(--spacing-s); + } } -} \ No newline at end of file +} diff --git a/web/src/pages/overview/OverviewExpandable/style.scss b/web/src/pages/overview/OverviewExpandable/style.scss index 245010c97a..652b3fcda8 100644 --- a/web/src/pages/overview/OverviewExpandable/style.scss +++ b/web/src/pages/overview/OverviewExpandable/style.scss @@ -1,7 +1,7 @@ .overview-expandable { width: 100%; - &>.header { + & > .header { display: flex; flex-flow: row; align-items: center; @@ -42,7 +42,7 @@ transition-duration: 100ms; transition-timing-function: ease-in-out; - &>div { + & > div { box-sizing: border-box; padding-top: 20px; overflow: hidden; @@ -53,4 +53,4 @@ grid-template-rows: 1fr; } } -} \ No newline at end of file +} diff --git a/web/src/pages/overview/OverviewStats/style.scss b/web/src/pages/overview/OverviewStats/style.scss index df29a3cd4e..7dba582103 100644 --- a/web/src/pages/overview/OverviewStats/style.scss +++ b/web/src/pages/overview/OverviewStats/style.scss @@ -1,268 +1,259 @@ -#network-overview-page { - .page-content { - & > .overview-network-stats { +.overview-network-stats { + display: grid; + column-gap: 3rem; + row-gap: 2rem; + grid-template-columns: 1fr; + justify-items: start; + box-sizing: border-box; + + @include media-breakpoint-down(md) { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + @include media-breakpoint-up(md) { + justify-items: center; + } + + @include media-breakpoint-down(xl) { + padding-left: 2rem; + padding-right: 2rem; + } + + @include media-breakpoint-up(xl) { + padding-left: 6rem; + padding-right: 6rem; + } + + @include media-breakpoint-up(xxl) { + grid-template-columns: 1fr 1fr; + } + + & > .summary { + position: relative; + grid-row: 1; + grid-column: 1; + width: 100%; + + @include media-breakpoint-down(lg) { display: grid; - column-gap: 3rem; - row-gap: 2rem; - grid-template-columns: 1fr; - justify-items: start; - box-sizing: border-box; - @include media-breakpoint-down(md) { - padding-left: 1.5rem; - padding-right: 1.5rem; - } - @include media-breakpoint-up(md) { - justify-items: center; - } - @include media-breakpoint-down(xl) { - padding-left: 2rem; - padding-right: 2rem; - } - @include media-breakpoint-up(xl) { - padding-left: 6rem; - padding-right: 6rem; - } - @include media-breakpoint-up(xxl) { - grid-template-columns: 1fr 1fr; - } + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; + width: 100%; + row-gap: 1.5rem; + column-gap: 1.5rem; + } - & > .summary { - position: relative; - grid-row: 1; - grid-column: 1; + @include media-breakpoint-up(lg) { + box-shadow: 5px 5px 15px #00000005; + background-color: var(--white); + border-radius: 15px; + display: flex; + align-items: center; + align-content: center; + justify-content: flex-start; + flex-flow: row nowrap; + height: 120px; + width: 820px; + } + + & > .info { + display: flex; + flex-direction: column; + justify-content: flex-start; + + @include media-breakpoint-down(lg) { + padding: 2rem; + box-sizing: border-box; + box-shadow: 5px 5px 15px #00000005; + background-color: var(--white); + border-radius: 15px; width: 100%; - @include media-breakpoint-down(lg) { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto auto; - width: 100%; - row-gap: 1.5rem; - column-gap: 1.5rem; - } - @include media-breakpoint-up(lg) { - box-shadow: 5px 5px 15px #00000005; - background-color: var(--white); - border-radius: 15px; - display: flex; - align-items: center; - align-content: center; - justify-content: flex-start; - flex-flow: row nowrap; - height: 120px; - width: 820px; + row-gap: 1rem; + + &:nth-of-type(1) { + grid-row: 1; + grid-column: 1; } - & > .info { - display: flex; - flex-direction: column; - justify-content: flex-start; - @include media-breakpoint-down(lg) { - padding: 2rem; - box-sizing: border-box; - box-shadow: 5px 5px 15px #00000005; - background-color: var(--white); - border-radius: 15px; - width: 100%; - row-gap: 1rem; - - &:nth-of-type(1) { - grid-row: 1; - grid-column: 1; - } + &:nth-of-type(2) { + grid-column: 2; + grid-row: 1; + } - &:nth-of-type(2) { - grid-column: 2; - grid-row: 1; - } + &:nth-of-type(3) { + grid-column: 1; + grid-row: 2; + } - &:nth-of-type(3) { - grid-column: 1; - grid-row: 2; - } + &:nth-of-type(4) { + grid-column: 2; + grid-row: 2; + } + } - &:nth-of-type(4) { - grid-column: 2; - grid-row: 2; - } - } - @include media-breakpoint-up(lg) { - box-sizing: border-box; - padding-top: 2rem; - grid-row: 1; - height: 100%; - } - @include media-breakpoint-down(xl) { - width: 100%; - } - @include media-breakpoint-up(xl) { - padding-right: 1.5rem; - } + @include media-breakpoint-up(lg) { + box-sizing: border-box; + padding-top: 2rem; + grid-row: 1; + height: 100%; + } - @include media-breakpoint-up(lg) { - &:first-of-type { - @include media-breakpoint-down(md) { - padding-left: 1.5rem; - } - @include media-breakpoint-up(md) { - padding-left: 2rem; - } - } + @include media-breakpoint-down(xl) { + width: 100%; + } - &:not(:first-of-type) { - @include media-breakpoint-down(md) { - padding-left: 1rem; - } - @include media-breakpoint-up(md) { - padding-left: 1.95rem; - } + @include media-breakpoint-up(xl) { + padding-right: 1.5rem; + } - border-left: 1px solid var(--gray-border); - } + @include media-breakpoint-up(lg) { + &:first-of-type { + @include media-breakpoint-down(md) { + padding-left: 1.5rem; } - & > .info-title { - @include typography-legacy(12px, 17px, medium, var(--gray-light), 'Poppins'); + @include media-breakpoint-up(md) { + padding-left: 2rem; } + } - & > .content { - display: flex; - flex-direction: row; - align-items: center; - align-content: center; - justify-content: flex-start; - column-gap: 1.15rem; - - & > .info-value { - @include typography-legacy( - 41px, - 41px, - semiBold, - var(--text-main), - 'Poppins' - ); - @include media-breakpoint-up(md) { - @include typography-legacy( - 41px, - 57px, - semiBold, - var(--text-main), - 'Poppins' - ); - } - } + &:not(:first-of-type) { + @include media-breakpoint-down(md) { + padding-left: 1rem; } - &.network-usage { - flex-grow: 1; - row-gap: 0.8rem; - - & > .content { - column-gap: 2.7rem; - - & > .network-usage { - & > span { - display: flex; - flex-direction: row; - align-items: center; - align-content: center; - justify-content: flex-start; - column-gap: 0.4rem; - @include typography-legacy( - 12px, - 17px, - medium, - var(--text-main), - 'Poppins' - ); - } - } - } + @include media-breakpoint-up(md) { + padding-left: 1.95rem; } + + border-left: 1px solid var(--gray-border); } } - & > .activity-graph { - padding: 1.5rem 2rem 1rem; - box-sizing: border-box; - border-radius: 15px; - background-color: var(--white); - box-shadow: 5px 5px 15px #00000005; - height: 120px; - display: grid; - grid-template-rows: 21px 1fr; - grid-template-columns: 1fr; - row-gap: 15px; - width: 100%; + & > .info-title { + @include typography-legacy(12px, 17px, medium, var(--gray-light), 'Poppins'); + } - & > .chart { - grid-row: 2; - grid-column: 1 / 2; - } + & > .content { + display: flex; + flex-direction: row; + align-items: center; + align-content: center; + justify-content: flex-start; + column-gap: 1.15rem; - & > header { - grid-row: 1; - grid-column: 1 / 2; - display: flex; - flex-direction: row; - align-items: center; - align-content: center; - justify-content: flex-start; + & > .info-value { + @include typography-legacy(41px, 41px, semiBold, var(--text-main), 'Poppins'); - h3 { - @include typography-legacy(15px, 21px, medium, var(--text-main), 'Poppins'); - @include media-breakpoint-down(md) { - text-transform: uppercase; - @include text-weight(semiBold); - } - @include media-breakpoint-up(md) { - } + @include media-breakpoint-up(md) { + @include typography-legacy(41px, 57px, semiBold, var(--text-main), 'Poppins'); } + } + } - & > .peaks { - margin-left: auto; - display: flex; - flex-direction: row; - align-items: center; - align-content: center; - justify-content: flex-start; - height: 17px; - @include media-breakpoint-down(md) { - column-gap: 1rem; - } - @include media-breakpoint-up(md) { - column-gap: 2rem; - } + &.network-usage { + flex-grow: 1; + row-gap: 0.8rem; - & > span { - @include media-breakpoint-down(md) { - &:first-of-type { - display: none; - } - } - @include typography-legacy( - 12px, - 17px, - medium, - var(--gray-light), - 'Poppins' - ); - } + & > .content { + column-gap: 2.7rem; - & > .network-speed { + & > .network-usage { + & > span { display: flex; flex-direction: row; align-items: center; align-content: center; justify-content: flex-start; column-gap: 0.4rem; + @include typography-legacy(12px, 17px, medium, var(--text-main), 'Poppins'); } } } + } + } + } + + & > .activity-graph { + padding: 1.5rem 2rem 1rem; + box-sizing: border-box; + border-radius: 15px; + background-color: var(--white); + box-shadow: 5px 5px 15px #00000005; + height: 120px; + display: grid; + grid-template-rows: 21px 1fr; + grid-template-columns: 1fr; + row-gap: 15px; + width: 100%; + + & > .chart { + grid-row: 2; + grid-column: 1 / 2; + } + + & > header { + grid-row: 1; + grid-column: 1 / 2; + display: flex; + flex-direction: row; + align-items: center; + align-content: center; + justify-content: flex-start; + + h3 { + @include typography-legacy(15px, 21px, medium, var(--text-main), 'Poppins'); + + @include media-breakpoint-down(md) { + text-transform: uppercase; + @include text-weight(semiBold); + } + + @include media-breakpoint-up(md) { + } + } + + & > .peaks { + margin-left: auto; + display: flex; + flex-direction: row; + align-items: center; + align-content: center; + justify-content: flex-start; + height: 17px; + + @include media-breakpoint-down(md) { + column-gap: 1rem; + } + + @include media-breakpoint-up(md) { + column-gap: 2rem; + } + + & > span { + @include media-breakpoint-down(md) { + &:first-of-type { + display: none; + } + } + + @include typography-legacy(12px, 17px, medium, var(--gray-light), 'Poppins'); + } & > .network-speed { - margin-top: 1rem; + display: flex; + flex-direction: row; + align-items: center; + align-content: center; + justify-content: flex-start; + column-gap: 0.4rem; } } } + + & > .network-speed { + margin-top: 1rem; + } } } diff --git a/web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx b/web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx index e671c952ec..71a18e7287 100644 --- a/web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx +++ b/web/src/shared/components/Layout/ExpandableSection/ExpandableSection.tsx @@ -10,18 +10,22 @@ type Props = { text: string; initOpen?: boolean; textAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'; + className?: string; + id?: string; } & PropsWithChildren; export const ExpandableSection = ({ children, text, + className, + id, textAs: Tag = 'p', initOpen = true, }: Props) => { const [expanded, setExpanded] = useState(initOpen); return ( -
+
{ diff --git a/web/src/shared/components/Layout/ExpandableSection/style.scss b/web/src/shared/components/Layout/ExpandableSection/style.scss index 8075d0f68a..e2cd654354 100644 --- a/web/src/shared/components/Layout/ExpandableSection/style.scss +++ b/web/src/shared/components/Layout/ExpandableSection/style.scss @@ -1,5 +1,5 @@ .expandable-section { - &>.track { + & > .track { width: 100%; user-select: none; cursor: pointer; @@ -8,6 +8,7 @@ column-gap: var(--spacing-xs); align-items: center; justify-items: start; + border-bottom: 1px solid var(--border-primary); p { width: 100%; @@ -27,7 +28,7 @@ } } - &>.expandable { + & > .expandable { display: grid; width: 100%; transition-property: grid-template-rows; @@ -42,8 +43,9 @@ grid-template-rows: 1fr; } - &>div { - overflow: hidden + & > div { + overflow: hidden; + padding-top: var(--spacing-s); } } } @@ -54,4 +56,4 @@ .expandable-section .track h2 { @include typography(app-body-1); -} \ No newline at end of file +} diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 9fcfe20f8c..f4d97a9ba2 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 9fcfe20f8cb666a299d9c9428aeb65604163e398 +Subproject commit f4d97a9ba26c34c1719d07d287f62b2ccee2d620 From e1aef9da621bfeb625d0b6f7e1a76c1cb9d6aaef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 9 May 2025 11:58:01 +0200 Subject: [PATCH 04/10] update stats view --- ...9095404_fix_wireguard_network_stats_view.down.sql | 12 ++++++++++++ ...509095404_fix_wireguard_network_stats_view.up.sql | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 migrations/20250509095404_fix_wireguard_network_stats_view.down.sql create mode 100644 migrations/20250509095404_fix_wireguard_network_stats_view.up.sql diff --git a/migrations/20250509095404_fix_wireguard_network_stats_view.down.sql b/migrations/20250509095404_fix_wireguard_network_stats_view.down.sql new file mode 100644 index 0000000000..91984411d4 --- /dev/null +++ b/migrations/20250509095404_fix_wireguard_network_stats_view.down.sql @@ -0,0 +1,12 @@ +CREATE OR REPLACE VIEW wireguard_peer_stats_view AS + SELECT + device_id, + greatest(upload - lag(upload, 1, 0::bigint) OVER (PARTITION BY device_id ORDER BY collected_at), 0) upload, + greatest(download - lag(download, 1, 0::bigint) OVER (PARTITION BY device_id ORDER BY collected_at), 0) download, + latest_handshake - (lag(latest_handshake, 1, latest_handshake) OVER (PARTITION BY device_id ORDER BY collected_at)) latest_handshake_diff, + latest_handshake, + collected_at, + network, + endpoint, + allowed_ips + FROM wireguard_peer_stats; diff --git a/migrations/20250509095404_fix_wireguard_network_stats_view.up.sql b/migrations/20250509095404_fix_wireguard_network_stats_view.up.sql new file mode 100644 index 0000000000..31da84ac71 --- /dev/null +++ b/migrations/20250509095404_fix_wireguard_network_stats_view.up.sql @@ -0,0 +1,12 @@ +CREATE OR REPLACE VIEW wireguard_peer_stats_view AS + SELECT + device_id, + greatest(upload - lag(upload, 1, upload) OVER (PARTITION BY device_id, network ORDER BY collected_at), 0) upload, + greatest(download - lag(download, 1, download) OVER (PARTITION BY device_id, network ORDER BY collected_at), 0) download, + latest_handshake - (lag(latest_handshake, 1, latest_handshake) OVER (PARTITION BY device_id, network ORDER BY collected_at)) latest_handshake_diff, + latest_handshake, + collected_at, + network, + endpoint, + allowed_ips + FROM wireguard_peer_stats; From b6359830ae5bc48b96336fe239fc9cc5df77c916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Tue, 13 May 2025 17:08:56 +0200 Subject: [PATCH 05/10] complete overview index page --- src/db/models/wireguard.rs | 102 ++++- src/grpc/mod.rs | 12 +- src/handlers/wireguard.rs | 42 +- src/lib.rs | 7 +- web/package.json | 14 +- web/pnpm-lock.yaml | 130 +++--- web/src/i18n/en/index.ts | 25 +- web/src/i18n/i18n-types.ts | 126 ++++-- .../overview-index/OverviewIndexPage.tsx | 225 +++++----- web/src/pages/overview-index/style.scss | 43 ++ .../NetworkUsageChart/NetworkUsageChart.tsx | 8 +- web/src/pages/overview/OverviewPage.tsx | 11 +- .../overview/OverviewStats/OverviewStats.tsx | 409 +++++++++++++----- .../pages/overview/OverviewStats/style.scss | 192 +++----- web/src/pages/overview/OverviewStats/utils.ts | 49 +++ .../OverviewStatsFilterSelect.tsx | 2 + .../AllNetworksGatewaysStatus.tsx | 94 ++++ .../AllNetworksGatewaysStatus/style.scss | 21 + .../GatewaysFloatingStatus.tsx | 125 ++++++ .../GatewaysFloatingStatus/style.scss | 60 +++ .../network/GatewaysStatus/GatewaysStatus.tsx | 251 ----------- .../GatewaysStatusInfo/GatewaysStatusInfo.tsx | 111 +++++ .../GatewaysStatusInfo/style.scss | 56 +++ .../NetworkGatewaysStatus.tsx | 41 ++ .../shared/components/svg/IconTagDismiss.tsx | 67 +-- web/src/shared/hooks/api/api.ts | 12 + web/src/shared/types.ts | 11 +- 27 files changed, 1478 insertions(+), 768 deletions(-) create mode 100644 web/src/pages/overview/OverviewStats/utils.ts create mode 100644 web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/AllNetworksGatewaysStatus.tsx create mode 100644 web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/style.scss create mode 100644 web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/GatewaysFloatingStatus.tsx create mode 100644 web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/style.scss delete mode 100644 web/src/shared/components/network/GatewaysStatus/GatewaysStatus.tsx create mode 100644 web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/GatewaysStatusInfo.tsx create mode 100644 web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/style.scss create mode 100644 web/src/shared/components/network/GatewaysStatus/NetworkGatewaysStatus/NetworkGatewaysStatus.tsx diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 5493309020..9f7468a100 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -1003,11 +1003,12 @@ impl WireguardNetwork { let activity_stats = query_as!( WireguardNetworkActivityStats, "SELECT \ - COALESCE(COUNT(DISTINCT(u.id)), 0) \"active_users!\", \ - COALESCE(COUNT(DISTINCT(s.device_id)), 0) \"active_devices!\" \ - FROM \"user\" u \ - JOIN device d ON d.user_id = u.id \ - JOIN wireguard_peer_stats s ON s.device_id = d.id \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ + FROM wireguard_peer_stats s \ + JOIN device d ON d.id = s.device_id \ + LEFT JOIN \"user\" u ON u.id = d.user_id \ WHERE latest_handshake >= $1 AND s.network = $2", from, self.id, @@ -1027,11 +1028,12 @@ impl WireguardNetwork { let activity_stats = query_as!( WireguardNetworkActivityStats, "SELECT \ - COALESCE(COUNT(DISTINCT(u.id)), 0) \"active_users!\", \ - COALESCE(COUNT(DISTINCT(s.device_id)), 0) \"active_devices!\" \ - FROM \"user\" u \ - JOIN device d ON d.user_id = u.id \ - JOIN wireguard_peer_stats s ON s.device_id = d.id \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ + FROM wireguard_peer_stats s \ + JOIN device d ON d.id = s.device_id \ + LEFT JOIN \"user\" u ON u.id = d.user_id \ WHERE latest_handshake >= $1 AND s.network = $2", from, self.id @@ -1082,10 +1084,12 @@ impl WireguardNetwork { let current_activity = self.current_activity(conn).await?; let transfer_series = self.transfer_series(conn, from, aggregation).await?; Ok(WireguardNetworkStats { - current_active_users: current_activity.active_users, - current_active_devices: current_activity.active_devices, active_users: total_activity.active_users, - active_devices: total_activity.active_devices, + active_network_devices: total_activity.active_network_devices, + active_user_devices: total_activity.active_user_devices, + current_active_network_devices: current_activity.active_network_devices, + current_active_user_devices: current_activity.active_user_devices, + current_active_users: current_activity.active_users, upload: transfer_series.iter().filter_map(|t| t.upload).sum(), download: transfer_series.iter().filter_map(|t| t.download).sum(), transfer_series, @@ -1181,7 +1185,8 @@ pub struct WireguardUserStatsRow { pub struct WireguardNetworkActivityStats { pub active_users: i64, - pub active_devices: i64, + pub active_user_devices: i64, + pub active_network_devices: i64, } pub struct WireguardNetworkTransferStats { @@ -1192,9 +1197,11 @@ pub struct WireguardNetworkTransferStats { #[derive(Deserialize, Serialize)] pub struct WireguardNetworkStats { pub current_active_users: i64, - pub current_active_devices: i64, + pub current_active_user_devices: i64, + pub current_active_network_devices: i64, pub active_users: i64, - pub active_devices: i64, + pub active_user_devices: i64, + pub active_network_devices: i64, pub upload: i64, pub download: i64, pub transfer_series: Vec, @@ -1767,3 +1774,66 @@ mod test { transaction.commit().await.unwrap(); } } + +pub(crate) async fn networks_stats( + conn: &PgPool, + from: &NaiveDateTime, + aggregation: &DateTimeAggregation, +) -> Result { + let total_activity = query_as!( + WireguardNetworkActivityStats, + "SELECT \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ + FROM wireguard_peer_stats s \ + JOIN device d ON d.id = s.device_id \ + LEFT JOIN \"user\" u ON u.id = d.user_id \ + WHERE latest_handshake >= $1", + from + ) + .fetch_one(conn) + .await?; + let current_activity_from = (Utc::now() - WIREGUARD_MAX_HANDSHAKE).naive_utc(); + let current_activity = query_as!( + WireguardNetworkActivityStats, + "SELECT \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ + FROM wireguard_peer_stats s \ + JOIN device d ON d.id = s.device_id \ + LEFT JOIN \"user\" u ON u.id = d.user_id \ + WHERE latest_handshake >= $1", + current_activity_from + ) + .fetch_one(conn) + .await?; + let transfer_series = query_as!( + WireguardStatsRow, + "SELECT \ + date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", \ + cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download \ + FROM wireguard_peer_stats_view \ + WHERE collected_at >= $2 \ + GROUP BY 1 \ + ORDER BY 1 \ + LIMIT $3", + aggregation.fstring(), + from, + PEER_STATS_LIMIT, + ) + .fetch_all(conn) + .await?; + Ok(WireguardNetworkStats { + current_active_users: current_activity.active_users, + current_active_network_devices: current_activity.active_network_devices, + current_active_user_devices: current_activity.active_user_devices, + active_users: total_activity.active_users, + active_network_devices: total_activity.active_network_devices, + active_user_devices: total_activity.active_user_devices, + download: transfer_series.iter().filter_map(|t| t.download).sum(), + upload: transfer_series.iter().filter_map(|t| t.upload).sum(), + transfer_series, + }) +} diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 891c24131e..3da78d7a27 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -110,7 +110,7 @@ use proto::proxy::{ // Helper struct used to handle gateway state // gateways are grouped by network type GatewayHostname = String; -#[derive(Debug)] +#[derive(Debug, Serialize, Clone)] pub struct GatewayMap(HashMap>); #[derive(Error, Debug)] @@ -286,6 +286,16 @@ impl GatewayMap { None => None, } } + + pub fn into_flattened(self) -> HashMap> { + self.0 + .into_iter() + .map(|(id, inner_map)| { + let states: Vec = inner_map.into_values().collect(); + (id, states) + }) + .collect() + } } impl Default for GatewayMap { diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index bcd88ee54e..1014a9c473 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -28,14 +28,14 @@ use crate::{ WireguardNetworkDevice, }, wireguard::{ - DateTimeAggregation, MappedDevice, WireguardDeviceStatsRow, WireguardNetworkInfo, - WireguardUserStatsRow, + networks_stats, DateTimeAggregation, MappedDevice, WireguardDeviceStatsRow, + WireguardNetworkInfo, WireguardNetworkStats, WireguardUserStatsRow, }, }, AddDevice, Device, GatewayEvent, Id, WireguardNetwork, }, enterprise::{handlers::CanManageDevices, limits::update_counts}, - grpc::GatewayMap, + grpc::{GatewayMap, GatewayState}, handlers::mail::send_new_device_added_email, server_config, templates::TemplateLocation, @@ -413,6 +413,24 @@ pub(crate) async fn gateway_status( }) } +pub(crate) async fn all_gateways_status( + _role: AdminRole, + Extension(gateway_state): Extension>>, +) -> ApiResult { + debug!("Displaying gateways status for all networks."); + let gateway_state = { + let lock = gateway_state + .lock() + .expect("Failed to acquire gateway state lock"); + lock.clone() + }; + let flattened = gateway_state.into_flattened(); + Ok(ApiResponse { + json: json!(flattened), + status: StatusCode::OK, + }) +} + pub(crate) async fn remove_gateway( Path((network_id, gateway_id)): Path<(i64, String)>, _role: AdminRole, @@ -1183,7 +1201,7 @@ pub(crate) async fn network_stats( }; let from = query_from.parse_timestamp()?.naive_utc(); let aggregation = get_aggregation(from)?; - let stats = network + let stats: WireguardNetworkStats = network .network_stats(&appstate.pool, &from, &aggregation) .await?; debug!("Displayed WireGuard network stats for network {network_id}"); @@ -1193,3 +1211,19 @@ pub(crate) async fn network_stats( status: StatusCode::OK, }) } + +pub(crate) async fn networks_overview_stats( + _role: AdminRole, + State(appstate): State, + Query(query_from): Query, +) -> ApiResult { + debug!("Preparing networks overview stats"); + let from = query_from.parse_timestamp()?.naive_utc(); + let aggregation = get_aggregation(from)?; + let all_networks_stats = networks_stats(&appstate.pool, &from, &aggregation).await?; + debug!("Finished processing networks overview stats"); + Ok(ApiResponse { + json: json!(all_networks_stats), + status: StatusCode::OK, + }) +} diff --git a/src/lib.rs b/src/lib.rs index b1cfaa7cbb..5c81f7a9ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,7 @@ use handlers::{ rename_authentication_key, }, updates::check_new_version, + wireguard::{all_gateways_status, networks_overview_stats}, yubikey::{delete_yubikey, rename_yubikey}, }; use ipnetwork::IpNetwork; @@ -534,16 +535,18 @@ pub fn build_webapp( post(start_network_device_setup_for_device), ) .route("/network", post(create_network)) + .route("/network", get(list_networks)) + .route("/network/import", post(import_network)) + .route("/network/stats", get(networks_overview_stats)) + .route("/network/gateways", get(all_gateways_status)) .route("/network/{network_id}", put(modify_network)) .route("/network/{network_id}", delete(delete_network)) - .route("/network", get(list_networks)) .route("/network/{network_id}", get(network_details)) .route("/network/{network_id}/gateways", get(gateway_status)) .route( "/network/{network_id}/gateways/{gateway_id}", delete(remove_gateway), ) - .route("/network/import", post(import_network)) .route("/network/{network_id}/devices", post(add_user_devices)) .route( "/network/{network_id}/device/{device_id}/config", diff --git a/web/package.json b/web/package.json index edc7b27cf4..f625de8c84 100644 --- a/web/package.json +++ b/web/package.json @@ -47,8 +47,8 @@ "@react-rxjs/core": "^0.10.8", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", - "@tanstack/query-core": "^5.75.5", - "@tanstack/react-query": "^5.75.5", + "@tanstack/query-core": "^5.76.0", + "@tanstack/react-query": "^5.76.0", "@tanstack/react-virtual": "3.13.8", "@tanstack/virtual-core": "3.13.8", "@use-gesture/react": "^10.3.1", @@ -64,11 +64,11 @@ "events": "^3.3.0", "fast-deep-equal": "^3.1.3", "file-saver": "^2.0.5", - "framer-motion": "^12.10.4", + "framer-motion": "^12.11.0", "fuse.js": "^7.1.0", "get-text-width": "^1.0.3", "hex-rgb": "^5.0.0", - "html-react-parser": "^5.2.3", + "html-react-parser": "^5.2.5", "humanize-duration": "^3.32.1", "ipaddr.js": "^2.2.0", "itertools": "^2.4.1", @@ -113,12 +113,12 @@ "@eslint/js": "^9.26.0", "@hookform/devtools": "^4.4.0", "@stylistic/eslint-plugin-ts": "^4.2.0", - "@tanstack/react-query-devtools": "^5.75.5", + "@tanstack/react-query-devtools": "^5.76.0", "@types/byte-size": "^8.1.2", "@types/file-saver": "^2.0.7", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", - "@types/node": "^22.15.16", + "@types/node": "^22.15.17", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@types/react-router-dom": "^5.3.3", @@ -131,7 +131,7 @@ "dotenv": "^16.5.0", "esbuild": "^0.25.4", "eslint": "^9.26.0", - "eslint-config-prettier": "^10.1.3", + "eslint-config-prettier": "^10.1.5", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.4.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 04343b3e75..632679393c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -30,11 +30,11 @@ importers: specifier: ^2.0.1 version: 2.0.1 '@tanstack/query-core': - specifier: ^5.75.5 - version: 5.75.5 + specifier: ^5.76.0 + version: 5.76.0 '@tanstack/react-query': - specifier: ^5.75.5 - version: 5.75.5(react@18.2.0) + specifier: ^5.76.0 + version: 5.76.0(react@18.2.0) '@tanstack/react-virtual': specifier: 3.13.8 version: 3.13.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -81,8 +81,8 @@ importers: specifier: ^2.0.5 version: 2.0.5 framer-motion: - specifier: ^12.10.4 - version: 12.10.4(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^12.11.0 + version: 12.11.0(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -93,8 +93,8 @@ importers: specifier: ^5.0.0 version: 5.0.0 html-react-parser: - specifier: ^5.2.3 - version: 5.2.3(@types/react@18.2.48)(react@18.2.0) + specifier: ^5.2.5 + version: 5.2.5(@types/react@18.2.48)(react@18.2.0) humanize-duration: specifier: ^3.32.1 version: 3.32.1 @@ -223,8 +223,8 @@ importers: specifier: ^4.2.0 version: 4.2.0(eslint@9.26.0)(typescript@5.8.3) '@tanstack/react-query-devtools': - specifier: ^5.75.5 - version: 5.75.5(@tanstack/react-query@5.75.5(react@18.2.0))(react@18.2.0) + specifier: ^5.76.0 + version: 5.76.0(@tanstack/react-query@5.76.0(react@18.2.0))(react@18.2.0) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -238,8 +238,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^22.15.16 - version: 22.15.16 + specifier: ^22.15.17 + version: 22.15.17 '@types/react': specifier: ^18.2.48 version: 18.2.48 @@ -260,7 +260,7 @@ importers: version: 8.32.0(eslint@9.26.0)(typescript@5.8.3) '@vitejs/plugin-react-swc': specifier: ^3.9.0 - version: 3.9.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + version: 3.9.0(vite@6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) @@ -277,8 +277,8 @@ importers: specifier: ^9.26.0 version: 9.26.0 eslint-config-prettier: - specifier: ^10.1.3 - version: 10.1.3(eslint@9.26.0) + specifier: ^10.1.5 + version: 10.1.5(eslint@9.26.0) eslint-plugin-import: specifier: ^2.31.0 version: 2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0) @@ -287,7 +287,7 @@ importers: version: 6.10.2(eslint@9.26.0) eslint-plugin-prettier: specifier: ^5.4.0 - version: 5.4.0(@types/eslint@8.56.2)(eslint-config-prettier@10.1.3(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3) + version: 5.4.0(@types/eslint@8.56.2)(eslint-config-prettier@10.1.5(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3) eslint-plugin-react: specifier: ^7.37.5 version: 7.37.5(eslint@9.26.0) @@ -326,13 +326,13 @@ importers: version: 5.0.5(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + version: 6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) vite-plugin-eslint: specifier: ^1.8.1 - version: 1.8.1(eslint@9.26.0)(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + version: 1.8.1(eslint@9.26.0)(vite@6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) vite-plugin-package-version: specifier: ^1.1.0 - version: 1.1.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + version: 1.1.0(vite@6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) packages: @@ -1056,20 +1056,20 @@ packages: '@swc/types@0.1.21': resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==} - '@tanstack/query-core@5.75.5': - resolution: {integrity: sha512-kPDOxtoMn2Ycycb76Givx2fi+2pzo98F9ifHL/NFiahEDpDwSVW6o12PRuQ0lQnBOunhRG5etatAhQij91M3MQ==} + '@tanstack/query-core@5.76.0': + resolution: {integrity: sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg==} - '@tanstack/query-devtools@5.74.7': - resolution: {integrity: sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==} + '@tanstack/query-devtools@5.76.0': + resolution: {integrity: sha512-1p92nqOBPYVqVDU0Ua5nzHenC6EGZNrLnB2OZphYw8CNA1exuvI97FVgIKON7Uug3uQqvH/QY8suUKpQo8qHNQ==} - '@tanstack/react-query-devtools@5.75.5': - resolution: {integrity: sha512-S31U00nJOQIbxydRH1kOwdLRaLBrda8O5QjzmgkRg60UZzPGdbI6+873Qa0YGUfPeILDbR2ukgWyg7CJQPy4iA==} + '@tanstack/react-query-devtools@5.76.0': + resolution: {integrity: sha512-RoyRzH5XJB//OhAdzQTutesw9uHyNZroLp/I7NDAQf8OVJKTTcoaYBmaw5pmB2e3bVdgqFu6nHFZUr5j5qBdZw==} peerDependencies: - '@tanstack/react-query': ^5.75.5 + '@tanstack/react-query': ^5.76.0 react: ^18 || ^19 - '@tanstack/react-query@5.75.5': - resolution: {integrity: sha512-QrLCJe40BgBVlWdAdf2ZEVJ0cISOuEy/HKupId1aTKU6gPJZVhSvZpH+Si7csRflCJphzlQ77Yx6gUxGW9o0XQ==} + '@tanstack/react-query@5.76.0': + resolution: {integrity: sha512-dZLYzVuUFZJkenxd8o01oyFimeLBmSkaUviPHuDzXe7LSLO4WTTx92jwJlNUXOOHzg6t0XknklZ15cjhYNSDjA==} peerDependencies: react: ^18 || ^19 @@ -1163,8 +1163,8 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - '@types/node@22.15.16': - resolution: {integrity: sha512-3pr+KjwpVujqWqOKT8mNR+rd09FqhBLwg+5L/4t0cNYBzm/yEiYGCxWttjaPBsLtAo+WFNoXzGJfolM1JuRXoA==} + '@types/node@22.15.17': + resolution: {integrity: sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1972,8 +1972,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-prettier@10.1.3: - resolution: {integrity: sha512-vDo4d9yQE+cS2tdIT4J02H/16veRvkHgiLDRpej+WL67oCfbOb97itZXn8wMPJ/GsiEBVjrjs//AVNw2Cp1EcA==} + eslint-config-prettier@10.1.5: + resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -2233,8 +2233,8 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - framer-motion@12.10.4: - resolution: {integrity: sha512-gGkavMW3QFDCxI+adVu5sn2NtIRHYPGVLDSJ0S/6B0ZoxPaql2F9foWdg9sGIP6sPA8cbNDfxYf9VlhD3+FkVQ==} + framer-motion@12.11.0: + resolution: {integrity: sha512-BaBPmkhaC2l0n619Kt1nQaxSdUdyyz5V1Z7EKJ1CcraOTZitgVx0RTbL8lmg2XesaFi6o8MPBIhkWDIvzDpGaQ==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2448,11 +2448,11 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} - html-dom-parser@5.0.13: - resolution: {integrity: sha512-B7JonBuAfG32I7fDouUQEogBrz3jK9gAuN1r1AaXpED6dIhtg/JwiSRhjGL7aOJwRz3HU4efowCjQBaoXiREqg==} + html-dom-parser@5.1.1: + resolution: {integrity: sha512-+o4Y4Z0CLuyemeccvGN4bAO20aauB2N9tFEAep5x4OW34kV4PTarBHm6RL02afYt2BMKcr0D2Agep8S3nJPIBg==} - html-react-parser@5.2.3: - resolution: {integrity: sha512-y34oKTu+9T1fKdJE1cN9QWFWu8sx8Qa5tJOafUfMUF5Niah+yF6zlEHhWh7a0iZEcLRPIMw54bY14ajQF7xP7A==} + html-react-parser@5.2.5: + resolution: {integrity: sha512-bRPdv8KTqG9CEQPMNGksDqmbiRfVQeOidry8pVetdh/1jQ1Edx4KX5m0lWvDD89Pt4CqTYjK1BLz6NoNVxN/Uw==} peerDependencies: '@types/react': 0.14 || 15 || 16 || 17 || 18 || 19 react: 0.14 || 15 || 16 || 17 || 18 || 19 @@ -2985,8 +2985,8 @@ packages: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} - motion-dom@12.10.4: - resolution: {integrity: sha512-GSv8kz0ANFfeGrFKi99s3GQjLiL1IKH3KtSNEqrPiVbloHVRiNbNtpsYQq9rkV2AV+7jxvd1X1ObUMVDnAEnXA==} + motion-dom@12.11.0: + resolution: {integrity: sha512-CItkGYJenn5ZsbzTX0D9mE0UWdjdd9r535FrxEXhzR8Kwa9I2dLr1uhEJgQPWbgaIJ6i0sNFnf2T9NvVDWQVBw==} motion-utils@12.9.4: resolution: {integrity: sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg==} @@ -4910,19 +4910,19 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.75.5': {} + '@tanstack/query-core@5.76.0': {} - '@tanstack/query-devtools@5.74.7': {} + '@tanstack/query-devtools@5.76.0': {} - '@tanstack/react-query-devtools@5.75.5(@tanstack/react-query@5.75.5(react@18.2.0))(react@18.2.0)': + '@tanstack/react-query-devtools@5.76.0(@tanstack/react-query@5.76.0(react@18.2.0))(react@18.2.0)': dependencies: - '@tanstack/query-devtools': 5.74.7 - '@tanstack/react-query': 5.75.5(react@18.2.0) + '@tanstack/query-devtools': 5.76.0 + '@tanstack/react-query': 5.76.0(react@18.2.0) react: 18.2.0 - '@tanstack/react-query@5.75.5(react@18.2.0)': + '@tanstack/react-query@5.76.0(react@18.2.0)': dependencies: - '@tanstack/query-core': 5.75.5 + '@tanstack/query-core': 5.76.0 react: 18.2.0 '@tanstack/react-virtual@3.13.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -5008,7 +5008,7 @@ snapshots: '@types/ms@0.7.34': {} - '@types/node@22.15.16': + '@types/node@22.15.17': dependencies: undici-types: 6.21.0 @@ -5172,10 +5172,10 @@ snapshots: '@use-gesture/core': 10.3.1 react: 18.2.0 - '@vitejs/plugin-react-swc@3.9.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': + '@vitejs/plugin-react-swc@3.9.0(vite@6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': dependencies: '@swc/core': 1.11.21 - vite: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + vite: 6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) transitivePeerDependencies: - '@swc/helpers' @@ -6100,7 +6100,7 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.3(eslint@9.26.0): + eslint-config-prettier@10.1.5(eslint@9.26.0): dependencies: eslint: 9.26.0 @@ -6170,7 +6170,7 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@5.4.0(@types/eslint@8.56.2)(eslint-config-prettier@10.1.3(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3): + eslint-plugin-prettier@5.4.0(@types/eslint@8.56.2)(eslint-config-prettier@10.1.5(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3): dependencies: eslint: 9.26.0 prettier: 3.5.3 @@ -6178,7 +6178,7 @@ snapshots: synckit: 0.11.2 optionalDependencies: '@types/eslint': 8.56.2 - eslint-config-prettier: 10.1.3(eslint@9.26.0) + eslint-config-prettier: 10.1.5(eslint@9.26.0) eslint-plugin-react-hooks@5.2.0(eslint@9.26.0): dependencies: @@ -6431,9 +6431,9 @@ snapshots: fraction.js@4.3.7: {} - framer-motion@12.10.4(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + framer-motion@12.11.0(@emotion/is-prop-valid@1.2.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - motion-dom: 12.10.4 + motion-dom: 12.11.0 motion-utils: 12.9.4 tslib: 2.8.1 optionalDependencies: @@ -6709,15 +6709,15 @@ snapshots: dependencies: lru-cache: 6.0.0 - html-dom-parser@5.0.13: + html-dom-parser@5.1.1: dependencies: domhandler: 5.0.3 htmlparser2: 10.0.0 - html-react-parser@5.2.3(@types/react@18.2.48)(react@18.2.0): + html-react-parser@5.2.5(@types/react@18.2.48)(react@18.2.0): dependencies: domhandler: 5.0.3 - html-dom-parser: 5.0.13 + html-dom-parser: 5.1.1 react: 18.2.0 react-property: 2.0.2 style-to-js: 1.1.16 @@ -7337,7 +7337,7 @@ snapshots: modify-values@1.0.1: {} - motion-dom@12.10.4: + motion-dom@12.11.0: dependencies: motion-utils: 12.9.4 @@ -8611,19 +8611,19 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-eslint@1.8.1(eslint@9.26.0)(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): + vite-plugin-eslint@1.8.1(eslint@9.26.0)(vite@6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): dependencies: '@rollup/pluginutils': 4.2.1 '@types/eslint': 8.56.2 eslint: 9.26.0 rollup: 2.79.2 - vite: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + vite: 6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) - vite-plugin-package-version@1.1.0(vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): + vite-plugin-package-version@1.1.0(vite@6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): dependencies: - vite: 6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + vite: 6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) - vite@6.3.5(@types/node@22.15.16)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1): + vite@6.3.5(@types/node@22.15.17)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -8632,7 +8632,7 @@ snapshots: rollup: 4.34.9 tinyglobby: 0.2.13 optionalDependencies: - '@types/node': 22.15.16 + '@types/node': 22.15.17 fsevents: 2.3.3 sass: 1.70.0 terser: 5.37.0 diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 352e13126e..4d370fbb1d 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1072,11 +1072,10 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do gatewaysStatus: { label: 'Gateways', states: { - connected: 'All connected', - partial: 'One or more are not working', - disconnected: 'Disconnected', - error: 'Retrieving connections failed', - loading: 'Retrieving connections', + all: 'All ({count: number}) Connected', + some: 'Some ({count: number}) Connected', + none: 'None connected', + error: 'Status check failed', }, messages: { error: 'Failed to get gateways status', @@ -1828,13 +1827,21 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do grid: 'Grid view', list: 'List view', }, + gatewayStatus: { + all: 'All ({count: number}) Connected', + some: 'Some ({count: number}) Connected', + none: 'None connected', + }, stats: { currentlyActiveUsers: 'Currently active users', - currentlyActiveDevices: 'Currently active devices', - activeUsersFilter: 'Active users in {hour: number}H', - activeDevicesFilter: 'Active devices in {hour: number}H', - totalTransfer: 'Total transfer:', + currentlyActiveNetworkDevices: 'Currently active network devices', + totalUserDevices: 'Total user devices: {count: number}', + activeNetworkDevices: 'Active network devices in {hour: number}h', + activeUsersFilter: 'Active users in {hour: number}h', + activeDevicesFilter: 'Active devices in {hour: number}h', activityIn: 'Activity in {hour: number}H', + networkUsage: 'Network usage', + peak: 'Peak', in: 'In:', out: 'Out:', gatewayDisconnected: 'Gateway disconnected', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 57e3ef7254..64e62cf31a 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2615,25 +2615,23 @@ type RootTranslation = { label: string states: { /** - * A​l​l​ ​c​o​n​n​e​c​t​e​d + * A​l​l​ ​(​{​c​o​u​n​t​}​)​ ​C​o​n​n​e​c​t​e​d + * @param {number} count */ - connected: string + all: RequiredParams<'count'> /** - * O​n​e​ ​o​r​ ​m​o​r​e​ ​a​r​e​ ​n​o​t​ ​w​o​r​k​i​n​g + * S​o​m​e​ ​(​{​c​o​u​n​t​}​)​ ​C​o​n​n​e​c​t​e​d + * @param {number} count */ - partial: string + some: RequiredParams<'count'> /** - * D​i​s​c​o​n​n​e​c​t​e​d + * N​o​n​e​ ​c​o​n​n​e​c​t​e​d */ - disconnected: string + none: string /** - * R​e​t​r​i​e​v​i​n​g​ ​c​o​n​n​e​c​t​i​o​n​s​ ​f​a​i​l​e​d + * S​t​a​t​u​s​ ​c​h​e​c​k​ ​f​a​i​l​e​d */ error: string - /** - * R​e​t​r​i​e​v​i​n​g​ ​c​o​n​n​e​c​t​i​o​n​s - */ - loading: string } messages: { /** @@ -4346,34 +4344,64 @@ type RootTranslation = { */ list: string } + gatewayStatus: { + /** + * A​l​l​ ​(​{​c​o​u​n​t​}​)​ ​C​o​n​n​e​c​t​e​d + * @param {number} count + */ + all: RequiredParams<'count'> + /** + * S​o​m​e​ ​(​{​c​o​u​n​t​}​)​ ​C​o​n​n​e​c​t​e​d + * @param {number} count + */ + some: RequiredParams<'count'> + /** + * N​o​n​e​ ​c​o​n​n​e​c​t​e​d + */ + none: string + } stats: { /** * C​u​r​r​e​n​t​l​y​ ​a​c​t​i​v​e​ ​u​s​e​r​s */ currentlyActiveUsers: string /** - * C​u​r​r​e​n​t​l​y​ ​a​c​t​i​v​e​ ​d​e​v​i​c​e​s + * C​u​r​r​e​n​t​l​y​ ​a​c​t​i​v​e​ ​n​e​t​w​o​r​k​ ​d​e​v​i​c​e​s + */ + currentlyActiveNetworkDevices: string + /** + * T​o​t​a​l​ ​u​s​e​r​ ​d​e​v​i​c​e​s​:​ ​{​c​o​u​n​t​} + * @param {number} count */ - currentlyActiveDevices: string + totalUserDevices: RequiredParams<'count'> /** - * A​c​t​i​v​e​ ​u​s​e​r​s​ ​i​n​ ​{​h​o​u​r​}​H + * A​c​t​i​v​e​ ​n​e​t​w​o​r​k​ ​d​e​v​i​c​e​s​ ​i​n​ ​{​h​o​u​r​}​h * @param {number} hour */ - activeUsersFilter: RequiredParams<'hour'> + activeNetworkDevices: RequiredParams<'hour'> /** - * A​c​t​i​v​e​ ​d​e​v​i​c​e​s​ ​i​n​ ​{​h​o​u​r​}​H + * A​c​t​i​v​e​ ​u​s​e​r​s​ ​i​n​ ​{​h​o​u​r​}​h * @param {number} hour */ - activeDevicesFilter: RequiredParams<'hour'> + activeUsersFilter: RequiredParams<'hour'> /** - * T​o​t​a​l​ ​t​r​a​n​s​f​e​r​: + * A​c​t​i​v​e​ ​d​e​v​i​c​e​s​ ​i​n​ ​{​h​o​u​r​}​h + * @param {number} hour */ - totalTransfer: string + activeDevicesFilter: RequiredParams<'hour'> /** * A​c​t​i​v​i​t​y​ ​i​n​ ​{​h​o​u​r​}​H * @param {number} hour */ activityIn: RequiredParams<'hour'> + /** + * N​e​t​w​o​r​k​ ​u​s​a​g​e + */ + networkUsage: string + /** + * P​e​a​k + */ + peak: string /** * I​n​: */ @@ -8501,25 +8529,21 @@ export type TranslationFunctions = { label: () => LocalizedString states: { /** - * All connected + * All ({count}) Connected */ - connected: () => LocalizedString + all: (arg: { count: number }) => LocalizedString /** - * One or more are not working + * Some ({count}) Connected */ - partial: () => LocalizedString + some: (arg: { count: number }) => LocalizedString /** - * Disconnected + * None connected */ - disconnected: () => LocalizedString + none: () => LocalizedString /** - * Retrieving connections failed + * Status check failed */ error: () => LocalizedString - /** - * Retrieving connections - */ - loading: () => LocalizedString } messages: { /** @@ -10224,31 +10248,57 @@ export type TranslationFunctions = { */ list: () => LocalizedString } + gatewayStatus: { + /** + * All ({count}) Connected + */ + all: (arg: { count: number }) => LocalizedString + /** + * Some ({count}) Connected + */ + some: (arg: { count: number }) => LocalizedString + /** + * None connected + */ + none: () => LocalizedString + } stats: { /** * Currently active users */ currentlyActiveUsers: () => LocalizedString /** - * Currently active devices + * Currently active network devices */ - currentlyActiveDevices: () => LocalizedString + currentlyActiveNetworkDevices: () => LocalizedString /** - * Active users in {hour}H + * Total user devices: {count} */ - activeUsersFilter: (arg: { hour: number }) => LocalizedString + totalUserDevices: (arg: { count: number }) => LocalizedString /** - * Active devices in {hour}H + * Active network devices in {hour}h */ - activeDevicesFilter: (arg: { hour: number }) => LocalizedString + activeNetworkDevices: (arg: { hour: number }) => LocalizedString + /** + * Active users in {hour}h + */ + activeUsersFilter: (arg: { hour: number }) => LocalizedString /** - * Total transfer: + * Active devices in {hour}h */ - totalTransfer: () => LocalizedString + activeDevicesFilter: (arg: { hour: number }) => LocalizedString /** * Activity in {hour}H */ activityIn: (arg: { hour: number }) => LocalizedString + /** + * Network usage + */ + networkUsage: () => LocalizedString + /** + * Peak + */ + peak: () => LocalizedString /** * In: */ diff --git a/web/src/pages/overview-index/OverviewIndexPage.tsx b/web/src/pages/overview-index/OverviewIndexPage.tsx index eb9a2d589b..818447fd6b 100644 --- a/web/src/pages/overview-index/OverviewIndexPage.tsx +++ b/web/src/pages/overview-index/OverviewIndexPage.tsx @@ -1,141 +1,152 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; -import { sumBy } from 'lodash-es'; -import { useCallback, useMemo } from 'react'; +import { range } from 'lodash-es'; +import { useMemo } from 'react'; +import Skeleton from 'react-loading-skeleton'; import { ExpandableSection } from '../../shared/components/Layout/ExpandableSection/ExpandableSection'; import { PageContainer } from '../../shared/components/Layout/PageContainer/PageContainer'; -import { GatewaysStatus } from '../../shared/components/network/GatewaysStatus/GatewaysStatus'; +import { AllNetworksGatewaysStatus } from '../../shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/AllNetworksGatewaysStatus'; +import { NetworkGatewaysStatus } from '../../shared/components/network/GatewaysStatus/NetworkGatewaysStatus/NetworkGatewaysStatus'; import { Button } from '../../shared/defguard-ui/components/Layout/Button/Button'; import { ButtonSize, ButtonStyleVariant, } from '../../shared/defguard-ui/components/Layout/Button/types'; +import { NoData } from '../../shared/defguard-ui/components/Layout/NoData/NoData'; import { isPresent } from '../../shared/defguard-ui/utils/isPresent'; import useApi from '../../shared/hooks/useApi'; import { useToaster } from '../../shared/hooks/useToaster'; -import { Network, NetworkSpeedStats, WireguardNetworkStats } from '../../shared/types'; +import { Network } from '../../shared/types'; +import { getNetworkStatsFilterValue } from '../overview/helpers/stats'; +import { useOverviewStore } from '../overview/hooks/store/useOverviewStore'; import { OverviewStats } from '../overview/OverviewStats/OverviewStats'; - -type OverviewIndexNetworkStats = Network & { - stats: WireguardNetworkStats; -}; - -type NetworkSpeedTickData = { - upload: number; - download: number; -}; - -type SumMap = Record; - -const sumTickData = ( - a: NetworkSpeedTickData, - b: NetworkSpeedTickData, -): NetworkSpeedTickData => { - return { - download: a.download + b.download, - upload: a.upload + b.upload, - }; -}; - -const sumTransferSeries = (transferStats: NetworkSpeedStats[][]): NetworkSpeedStats[] => { - const sumMap: SumMap = {}; - for (const stats of transferStats) { - for (const tick of stats) { - const tickValue = sumMap[tick.collected_at]; - if (isPresent(tickValue)) { - sumMap[tick.collected_at] = sumTickData(tickValue, tick); - } else { - sumMap[tick.collected_at] = tick; - } - } - } - const res: NetworkSpeedStats[] = []; - for (const sumMapKey of Object.keys(sumMap)) { - const value = sumMap[sumMapKey]; - res.push({ collected_at: sumMapKey, download: value.download, upload: value.upload }); - } - return res; -}; +import { OverviewStatsFilterSelect } from '../overview/OverviewStatsFilterSelect/OverviewStatsFilterSelect'; export const OverviewIndexPage = () => { const { - network: { getNetworks, getNetworkStats }, + network: { getNetworks }, } = useApi(); - const toaster = useToaster(); - - const query = useCallback(async () => { - const res: OverviewIndexNetworkStats[] = []; - const networks = await getNetworks(); - for (const network of networks) { - const stats = await getNetworkStats({ - id: network.id, - }); - res.push({ - ...network, - stats, - }); - } - return res; - }, [getNetworkStats, getNetworks]); - - const { data } = useQuery({ - queryKey: ['overview-index'], - queryFn: query, - refetchInterval: 10 * 1000, + const { data, isLoading } = useQuery({ + queryKey: ['network'], + queryFn: getNetworks, }); - const summaryData = useMemo(() => { - if (!data) return undefined; - const res: WireguardNetworkStats = { - active_devices: sumBy(data, 'stats.active_devices'), - active_users: sumBy(data, 'stats.active_users'), - current_active_devices: sumBy(data, 'stats.current_active_devices'), - current_active_users: sumBy(data, 'stats.current_active_users'), - download: sumBy(data, 'stats.download'), - upload: sumBy(data, 'stats.upload'), - transfer_series: sumTransferSeries( - data.map((network) => network.stats.transfer_series), - ), - }; - return res; - }, [data]); - return (

All locations overview

-
+
+ +
- - {isPresent(summaryData) && } + + + + {!data && + isLoading && + range(6).map((skeletonIndex) => )} {isPresent(data) && - data.map((network) => ( - -
- -
- -
- ))} + !isLoading && + data.length > 0 && + data.map((network) => )} + {isPresent(data) && data.length === 0 && !isLoading && ( + + )}
); }; + +const NetworkSectionSkeleton = () => { + return ( +
+ + + +
+ ); +}; + +type NetworkSectionProps = { + network: Network; +}; + +const NetworkSection = ({ network }: NetworkSectionProps) => { + const toaster = useToaster(); + const statsFilter = useOverviewStore((s) => s.statsFilter); + + const from = useMemo(() => getNetworkStatsFilterValue(statsFilter), [statsFilter]); + + const { + network: { getNetworkStats }, + } = useApi(); + + const { data } = useQuery({ + queryFn: () => getNetworkStats({ id: network.id, from }), + queryKey: ['network', network.id, 'stats', from], + refetchInterval: 60 * 1000, + placeholderData: (perv) => perv, + }); + + return ( + +
+ +
+ {!data && } + {data && } +
+ ); +}; + +const SummaryStats = () => { + const statsFilter = useOverviewStore((s) => s.statsFilter); + + const from = useMemo(() => getNetworkStatsFilterValue(statsFilter), [statsFilter]); + const { + network: { getAllNetworksStats }, + } = useApi(); + const { data, isLoading } = useQuery({ + queryKey: ['network', 'stats', from], + queryFn: () => getAllNetworksStats({ from }), + refetchInterval: 60 * 1000, + placeholderData: (perv) => perv, + }); + return ( + <> + {!data && isLoading && } + {data && !isLoading && } + + ); +}; + +const StatsSkeleton = () => { + return ( +
+ + +
+ ); +}; diff --git a/web/src/pages/overview-index/style.scss b/web/src/pages/overview-index/style.scss index 58a65be44b..1ccce8d51f 100644 --- a/web/src/pages/overview-index/style.scss +++ b/web/src/pages/overview-index/style.scss @@ -31,6 +31,21 @@ display: flex; flex-flow: column; row-gap: var(--page-content-spacing); + + & > header { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-m); + + & > .controls { + .select { + width: 120px; + max-width: 100%; + } + } + } } #overview-index { @@ -43,3 +58,31 @@ } } } + +#overview-index { + .network-section-skeleton { + display: flex; + flex-flow: column; + row-gap: var(--spacing-s); + } + + .network-stats-skeleton { + width: 100%; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 1fr 1fr; + column-gap: var(--spacing-xs); + + .react-loading-skeleton { + border-radius: 10px; + height: 142px; + } + } +} + +#all-networks-summary { + .gateways-status-info { + padding-bottom: var(--spacing-s); + min-height: 32px; + } +} diff --git a/web/src/pages/overview/OverviewConnectedUsers/shared/components/NetworkUsageChart/NetworkUsageChart.tsx b/web/src/pages/overview/OverviewConnectedUsers/shared/components/NetworkUsageChart/NetworkUsageChart.tsx index daf469b41f..83b54bc482 100644 --- a/web/src/pages/overview/OverviewConnectedUsers/shared/components/NetworkUsageChart/NetworkUsageChart.tsx +++ b/web/src/pages/overview/OverviewConnectedUsers/shared/components/NetworkUsageChart/NetworkUsageChart.tsx @@ -13,6 +13,8 @@ interface NetworkUsageProps { hideX?: boolean; barSize?: number; heightX?: number; + barGap?: number; + categoryGap?: number; } export const NetworkUsageChart = ({ @@ -22,6 +24,8 @@ export const NetworkUsageChart = ({ hideX = true, barSize = 2, heightX = 20, + barGap = 0, + categoryGap = 0, }: NetworkUsageProps) => { const getFormattedData = useMemo(() => parseStatsForCharts(data), [data]); @@ -32,8 +36,8 @@ export const NetworkUsageChart = ({ width={width} data={getFormattedData} margin={{ bottom: 0, left: 0, right: 0, top: 0 }} - barGap={0} - barCategoryGap={0} + barGap={barGap} + barCategoryGap={categoryGap} barSize={barSize} > { {breakpoint === 'desktop' && !isUndefined(selectedNetworkId) && ( - - )} - {networkStats && overviewStats && ( - + )} + {networkStats && }
{userStatsLoading && (
diff --git a/web/src/pages/overview/OverviewStats/OverviewStats.tsx b/web/src/pages/overview/OverviewStats/OverviewStats.tsx index aa88425436..e687276db9 100644 --- a/web/src/pages/overview/OverviewStats/OverviewStats.tsx +++ b/web/src/pages/overview/OverviewStats/OverviewStats.tsx @@ -1,134 +1,142 @@ import './style.scss'; -import numbro from 'numbro'; -import { forwardRef } from 'react'; +import { orderBy } from 'lodash-es'; +import millify from 'millify'; +import { forwardRef, ReactNode, useId, useMemo } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { useBreakpoint } from 'use-breakpoint'; import { useI18nContext } from '../../../i18n/i18n-react'; -import Icon24HConnections from '../../../shared/components/svg/Icon24HConnections'; -import IconActiveConnections from '../../../shared/components/svg/IconActiveConnections'; import IconPacketsIn from '../../../shared/components/svg/IconPacketsIn'; import IconPacketsOut from '../../../shared/components/svg/IconPacketsOut'; -import { deviceBreakpoints } from '../../../shared/constants'; +import { Card } from '../../../shared/defguard-ui/components/Layout/Card/Card'; import { NetworkSpeed } from '../../../shared/defguard-ui/components/Layout/NetworkSpeed/NetworkSpeed'; import { NetworkDirection } from '../../../shared/defguard-ui/components/Layout/NetworkSpeed/types'; -import { NetworkUserStats, WireguardNetworkStats } from '../../../shared/types'; +import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; +import { WireguardNetworkStats } from '../../../shared/types'; import { useOverviewStore } from '../hooks/store/useOverviewStore'; import { NetworkUsageChart } from '../OverviewConnectedUsers/shared/components/NetworkUsageChart/NetworkUsageChart'; +import { networkTrafficToChartData } from './utils'; interface Props { - usersStats?: NetworkUserStats[]; networkStats: WireguardNetworkStats; } -const formatStats = (value: number): string => - numbro(value).format({ - average: false, - spaceSeparated: false, - mantissa: 0, - }); - export const OverviewStats = forwardRef( ({ networkStats }, ref) => { - const { breakpoint } = useBreakpoint(deviceBreakpoints); const filterValue = useOverviewStore((state) => state.statsFilter); + const peakDownload = useMemo(() => { + const sorted = orderBy(networkStats.transfer_series, (stats) => stats.download, [ + 'desc', + ]); + return sorted[0]?.download ?? 0; + }, [networkStats.transfer_series]); + const peakUpload = useMemo(() => { + const sorted = orderBy(networkStats.transfer_series, ['upload'], ['desc']); + return sorted[0]?.upload ?? 0; + }, [networkStats.transfer_series]); const { LL } = useI18nContext(); + const localLL = LL.networkOverview.stats; + + const chartData = useMemo( + () => networkTrafficToChartData(networkStats.transfer_series, filterValue), + [filterValue, networkStats.transfer_series], + ); + + const info = useMemo( + (): InfoProps[] => [ + { + key: 'currently-active-users', + count: networkStats.current_active_users, + icon: , + title: localLL.currentlyActiveUsers(), + subTitle: localLL.totalUserDevices({ + count: networkStats.current_active_users, + }), + }, + { + key: 'current-active-network-devices', + title: localLL.currentlyActiveNetworkDevices(), + icon: , + count: networkStats.current_active_network_devices, + }, + { + key: 'active-users-icon', + title: localLL.activeUsersFilter({ + hour: filterValue, + }), + count: networkStats.active_users, + icon: , + subTitle: localLL.totalUserDevices({ + count: networkStats.active_user_devices, + }), + }, + { + key: 'active-network-devices', + title: localLL.activeNetworkDevices({ + hour: filterValue, + }), + icon: , + count: networkStats.current_active_network_devices, + }, + ], + [ + filterValue, + localLL, + networkStats.active_user_devices, + networkStats.active_users, + networkStats.current_active_network_devices, + networkStats.current_active_users, + ], + ); + return (
-
-
- - {LL.networkOverview.stats.currentlyActiveUsers()} - -
- - - {formatStats(networkStats.current_active_users)} - -
-
-
- - {LL.networkOverview.stats.currentlyActiveDevices()} - -
- - - {formatStats(networkStats.current_active_devices)} - -
-
-
- - {LL.networkOverview.stats.activeUsersFilter({ - hour: filterValue, - })} - -
- - {networkStats.active_users} -
-
-
- - {LL.networkOverview.stats.activeDevicesFilter({ - hour: filterValue, - })} - -
- - - {formatStats(networkStats.active_devices)} - -
-
- {breakpoint === 'desktop' && ( -
- - {LL.networkOverview.stats.totalTransfer()} - -
-
- - {LL.networkOverview.stats.in()} - - -
-
- - {LL.networkOverview.stats.out()} - - -
+ + {info.map((info) => ( + + ))} +
+ {LL.networkOverview.stats.networkUsage()} +
+
+ + {LL.networkOverview.stats.in()} + + +
+
+ + {LL.networkOverview.stats.out()} + +
- )} -
-
+
+
+

{LL.networkOverview.stats.activityIn({ hour: filterValue })}

- {LL.networkOverview.stats.totalTransfer()} + {LL.networkOverview.stats.peak()}
@@ -140,18 +148,225 @@ export const OverviewStats = forwardRef( <> {networkStats.transfer_series && ( )} )}
-
+
); }, ); + +type InfoProps = { + icon: ReactNode; + title: string; + subTitle?: string; + count: number; + key: string | number; +}; + +const InfoContainer = ({ count, icon, subTitle, title }: InfoProps) => { + return ( +
+

{title}

+
+ {icon} +

+ {millify(count, { + precision: 0, + })} +

+
+ {isPresent(subTitle) &&

{subTitle}

} +
+ ); +}; + +const CurrentActiveUsersIcon = () => { + return ( + + + + + ); +}; + +const CurrentActiveNetworkDevicesIcon = () => { + const maskId = useId(); + return ( + + + + + + + + + + + + ); +}; + +const ActiveUsersIcon = () => { + return ( + + + + + + + ); +}; + +const ActiveNetworkDevicesIcon = () => { + const maskId = useId(); + const mask2Id = useId(); + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/web/src/pages/overview/OverviewStats/style.scss b/web/src/pages/overview/OverviewStats/style.scss index 7dba582103..95933dc106 100644 --- a/web/src/pages/overview/OverviewStats/style.scss +++ b/web/src/pages/overview/OverviewStats/style.scss @@ -1,176 +1,114 @@ .overview-network-stats { display: grid; - column-gap: 3rem; - row-gap: 2rem; - grid-template-columns: 1fr; - justify-items: start; - box-sizing: border-box; - - @include media-breakpoint-down(md) { - padding-left: 1.5rem; - padding-right: 1.5rem; - } - - @include media-breakpoint-up(md) { - justify-items: center; - } - - @include media-breakpoint-down(xl) { - padding-left: 2rem; - padding-right: 2rem; - } - @include media-breakpoint-up(xl) { - padding-left: 6rem; - padding-right: 6rem; - } - - @include media-breakpoint-up(xxl) { - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + gap: var(--spacing-s); + padding: 0 var(--spacing-xs) var(--spacing-xs); + align-items: stretch; + + @include media-breakpoint-up(lg) { + grid-template-columns: auto 1fr; + grid-template-rows: 1fr; } & > .summary { position: relative; - grid-row: 1; - grid-column: 1; width: 100%; - @include media-breakpoint-down(lg) { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto auto; - width: 100%; - row-gap: 1.5rem; - column-gap: 1.5rem; - } - @include media-breakpoint-up(lg) { box-shadow: 5px 5px 15px #00000005; background-color: var(--white); border-radius: 15px; display: flex; - align-items: center; + align-items: stretch; align-content: center; justify-content: flex-start; flex-flow: row nowrap; - height: 120px; - width: 820px; + min-height: 120px; } & > .info { display: flex; - flex-direction: column; + flex-flow: column; + align-items: center; justify-content: flex-start; + row-gap: var(--spacing-xs); + padding: var(--spacing-s); + width: 100%; + max-width: 148px; - @include media-breakpoint-down(lg) { - padding: 2rem; - box-sizing: border-box; - box-shadow: 5px 5px 15px #00000005; - background-color: var(--white); - border-radius: 15px; - width: 100%; - row-gap: 1rem; - - &:nth-of-type(1) { - grid-row: 1; - grid-column: 1; - } - - &:nth-of-type(2) { - grid-column: 2; - grid-row: 1; - } - - &:nth-of-type(3) { - grid-column: 1; - grid-row: 2; - } - - &:nth-of-type(4) { - grid-column: 2; - grid-row: 2; - } + &:not(:first-child) { + border-left: 1px solid var(--border-primary); } - @include media-breakpoint-up(lg) { - box-sizing: border-box; - padding-top: 2rem; - grid-row: 1; - height: 100%; + &.network-usage { + row-gap: var(--spacing-m); } - @include media-breakpoint-down(xl) { - width: 100%; + &:not(.network-usage) { + .info-title { + min-height: 29px; + } } - @include media-breakpoint-up(xl) { - padding-right: 1.5rem; + .info-title { + color: var(--text-body-tertiary); + text-align: center; + @include typography(app-modal-1); } - @include media-breakpoint-up(lg) { - &:first-of-type { - @include media-breakpoint-down(md) { - padding-left: 1.5rem; - } - - @include media-breakpoint-up(md) { - padding-left: 2rem; - } - } - - &:not(:first-of-type) { - @include media-breakpoint-down(md) { - padding-left: 1rem; - } - - @include media-breakpoint-up(md) { - padding-left: 1.95rem; - } + .info-track { + width: 100%; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + column-gap: var(--spacing-xs); + max-height: 42px; - border-left: 1px solid var(--gray-border); + .info-count { + @include typography(app-title); } } - & > .info-title { - @include typography-legacy(12px, 17px, medium, var(--gray-light), 'Poppins'); + .info-sub-title { + color: var(--text-body-tertiary); + @include typography(app-modal-3); } - & > .content { + .network-usage-track { display: flex; - flex-direction: row; + flex-flow: row; align-items: center; - align-content: center; - justify-content: flex-start; - column-gap: 1.15rem; + justify-content: center; + column-gap: var(--spacing-xs); - & > .info-value { - @include typography-legacy(41px, 41px, semiBold, var(--text-main), 'Poppins'); - - @include media-breakpoint-up(md) { - @include typography-legacy(41px, 57px, semiBold, var(--text-main), 'Poppins'); + & > :nth-child(1) { + svg { + transform: rotate(-90deg); } } - } - &.network-usage { - flex-grow: 1; - row-gap: 0.8rem; - - & > .content { - column-gap: 2.7rem; + & > :nth-child(2) { + svg { + transform: rotate(-90deg); + } + } - & > .network-usage { - & > span { - display: flex; - flex-direction: row; - align-items: center; - align-content: center; - justify-content: flex-start; - column-gap: 0.4rem; - @include typography-legacy(12px, 17px, medium, var(--text-main), 'Poppins'); + .network-speed { + &.download { + svg { + transform: rotate(90deg); } } } + + .network-usage { + & > span { + @include typography(app-modal-1); + } + } } } } @@ -181,7 +119,7 @@ border-radius: 15px; background-color: var(--white); box-shadow: 5px 5px 15px #00000005; - height: 120px; + min-height: 120px; display: grid; grid-template-rows: 21px 1fr; grid-template-columns: 1fr; diff --git a/web/src/pages/overview/OverviewStats/utils.ts b/web/src/pages/overview/OverviewStats/utils.ts new file mode 100644 index 0000000000..ba4bbfaef7 --- /dev/null +++ b/web/src/pages/overview/OverviewStats/utils.ts @@ -0,0 +1,49 @@ +import { groupBy, map, sortBy } from 'lodash-es'; + +import { NetworkSpeedStats } from '../../../shared/types'; + +type AggregatedTick = { + collected_at: string; + download: number; + upload: number; + count: number; +}; + +export const networkTrafficToChartData = ( + ticks: NetworkSpeedStats[], + filter: number, +): NetworkSpeedStats[] => { + if (filter >= 2 && filter <= 5 && ticks.length > 60) { + const sorted = sortBy(ticks, (tick) => new Date(tick.collected_at).getTime()); + const first = new Date(sorted[0].collected_at).getTime(); + const last = new Date(sorted[sorted.length - 1].collected_at).getTime(); + + const totalMinutes = Math.max(1, Math.floor((last - first) / (1000 * 60))); + const minutesPerBucket = Math.ceil(totalMinutes / 60); + + const grouped = groupBy(sorted, (tick) => { + const date = new Date(tick.collected_at); + const bucketTime = new Date( + Math.floor(date.getTime() / (minutesPerBucket * 60 * 1000)) * + minutesPerBucket * + 60 * + 1000, + ); + return bucketTime.toISOString(); + }); + + return map(grouped, (group, timestamp): AggregatedTick => { + const totalDownload = group.reduce((sum, t) => sum + t.download, 0); + const totalUpload = group.reduce((sum, t) => sum + t.upload, 0); + const count = group.length; + + return { + collected_at: timestamp, + download: totalDownload, + upload: totalUpload, + count, + }; + }); + } + return ticks; +}; diff --git a/web/src/pages/overview/OverviewStatsFilterSelect/OverviewStatsFilterSelect.tsx b/web/src/pages/overview/OverviewStatsFilterSelect/OverviewStatsFilterSelect.tsx index dd55ad238d..390b64fa89 100644 --- a/web/src/pages/overview/OverviewStatsFilterSelect/OverviewStatsFilterSelect.tsx +++ b/web/src/pages/overview/OverviewStatsFilterSelect/OverviewStatsFilterSelect.tsx @@ -4,6 +4,7 @@ import { Select } from '../../../shared/defguard-ui/components/Layout/Select/Sel import { SelectOption, SelectSelectedValue, + SelectSizeVariant, } from '../../../shared/defguard-ui/components/Layout/Select/types'; import { useOverviewStore } from '../hooks/store/useOverviewStore'; @@ -24,6 +25,7 @@ export const OverviewStatsFilterSelect = () => { options={selectOptions} selected={filterValue} onChangeSingle={(res) => setOverviewStore({ statsFilter: res })} + sizeVariant={SelectSizeVariant.SMALL} /> ); }; diff --git a/web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/AllNetworksGatewaysStatus.tsx b/web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/AllNetworksGatewaysStatus.tsx new file mode 100644 index 0000000000..f6bf59195d --- /dev/null +++ b/web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/AllNetworksGatewaysStatus.tsx @@ -0,0 +1,94 @@ +import './style.scss'; + +import { useQuery } from '@tanstack/react-query'; +import { flatten } from 'lodash-es'; +import { useEffect, useMemo } from 'react'; + +import { isPresent } from '../../../../defguard-ui/utils/isPresent'; +import useApi from '../../../../hooks/useApi'; +import { useToaster } from '../../../../hooks/useToaster'; +import { GatewayStatus } from '../../../../types'; +import { GatewaysFloatingStatus } from '../GatewaysFloatingStatus/GatewaysFloatingStatus'; +import { GatewaysStatusInfo } from '../GatewaysStatusInfo/GatewaysStatusInfo'; + +type MappedStats = { + id: number; + name: string; + gateways: GatewayStatus[]; +}; + +export const AllNetworksGatewaysStatus = () => { + const { + network: { getAllGatewaysStatus }, + } = useApi(); + + const toaster = useToaster(); + + const { data, isLoading, isError, error } = useQuery({ + queryKey: ['network', 'gateways'], + queryFn: getAllGatewaysStatus, + placeholderData: (perv) => perv, + refetchInterval: 60 * 1000, + }); + + const [totalConnections, connectedCount] = useMemo(() => { + if (data) { + const flat = flatten(Object.values(data)); + const totalCount = flat.length; + const connectedCount = flat.reduce( + (ac, current) => ac + (current.connected ? 1 : 0), + 0, + ); + return [totalCount, connectedCount]; + } + return [0, 0]; + }, [data]); + + const listData = useMemo(() => { + if (data) { + const res: MappedStats[] = []; + for (const networkId of Object.keys(data)) { + const gateways = data[networkId]; + if (gateways.length > 0) { + const name = gateways[0].network_name; + res.push({ + id: Number(networkId), + name, + gateways, + }); + } + } + return res; + } + return []; + }, [data]); + + useEffect(() => { + if (isPresent(error)) { + toaster.error('Failed to check full gateways status.'); + console.error(error); + } + }, [error, toaster]); + + return ( + +
+ {listData.map((stats) => ( +
+
+

{stats.name}

+
+ {stats.gateways.map((gateway) => ( + + ))} +
+ ))} +
+
+ ); +}; diff --git a/web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/style.scss b/web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/style.scss new file mode 100644 index 0000000000..d8996a7990 --- /dev/null +++ b/web/src/shared/components/network/GatewaysStatus/AllNetworksGatewaysStatus/style.scss @@ -0,0 +1,21 @@ +.gateways-status-floating-menu { + .all-networks-gateways { + display: flex; + flex-flow: column; + row-gap: var(--spacing-s); + } + + .network-gateways { + display: flex; + flex-flow: column; + row-gap: var(--spacing-xs); + + & > .network { + border-bottom: 1px solid var(--border-primary); + + p { + @include typography(app-modal-1); + } + } + } +} diff --git a/web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/GatewaysFloatingStatus.tsx b/web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/GatewaysFloatingStatus.tsx new file mode 100644 index 0000000000..94e5f5dbb8 --- /dev/null +++ b/web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/GatewaysFloatingStatus.tsx @@ -0,0 +1,125 @@ +import './style.scss'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { InteractionBox } from '../../../../defguard-ui/components/Layout/InteractionBox/InteractionBox'; +import useApi from '../../../../hooks/useApi'; +import { useToaster } from '../../../../hooks/useToaster'; +import { GatewayStatus } from '../../../../types'; + +type Props = { + status: GatewayStatus; +}; + +export const GatewaysFloatingStatus = ({ status }: Props) => { + const { + network: { deleteGateway }, + } = useApi(); + const toaster = useToaster(); + + const queryClient = useQueryClient(); + + const { mutate, isPending } = useMutation({ + mutationFn: deleteGateway, + onError: (err) => { + toaster.error('Failed to remove gateway'); + console.error(err); + }, + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ['network', 'gateways'], + }); + void queryClient.invalidateQueries({ + queryKey: ['network', status.network_id, 'gateways'], + }); + }, + }); + + return ( +
+ {status.connected && } + {!status.connected && } +
+ {status.name &&

{status.name}

} + {status.hostname &&

{status.hostname}

} +
+
+ {!status.connected && !isPending && ( + { + mutate({ + gatewayId: status.uid, + networkId: status.network_id, + }); + }} + > + + + )} +
+
+ ); +}; + +const IconDismiss = () => { + return ( + + + + + ); +}; + +const IconConnected = () => { + return ( + + + + + ); +}; + +const IconDisconnected = () => { + return ( + + + + + ); +}; diff --git a/web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/style.scss b/web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/style.scss new file mode 100644 index 0000000000..2acece9b65 --- /dev/null +++ b/web/src/shared/components/network/GatewaysStatus/GatewaysFloatingStatus/style.scss @@ -0,0 +1,60 @@ +.gateway-floating-status-info { + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + column-gap: 5px; + + & > .info { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: center; + row-gap: 5px; + + .name, + .hostname { + color: var(--text-body-secondary); + @include typography(app-modal-2); + } + + & > .name { + font-weight: 700; + } + + & > .hostname { + font-weight: 400; + } + } + + & > .dismiss { + .interaction-box { + width: 18px; + height: 18px; + + button { + width: 24px; + height: 24px; + } + } + + svg { + width: 18px; + height: 18px; + + path { + fill: var(--surface-icon-primary); + transition-property: fill; + @include animate-standard; + } + } + + &:hover { + svg { + path { + fill: var(--surface-alert-primary); + } + } + } + } +} diff --git a/web/src/shared/components/network/GatewaysStatus/GatewaysStatus.tsx b/web/src/shared/components/network/GatewaysStatus/GatewaysStatus.tsx deleted file mode 100644 index 0d573cd96e..0000000000 --- a/web/src/shared/components/network/GatewaysStatus/GatewaysStatus.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import './style.scss'; - -import { autoUpdate, offset, useFloating } from '@floating-ui/react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import classNames from 'classnames'; -import { AnimatePresence, motion, TargetAndTransition } from 'framer-motion'; -import { isUndefined } from 'lodash-es'; -import { useEffect, useMemo, useState } from 'react'; -import ClickAwayListener from 'react-click-away-listener'; - -import { useI18nContext } from '../../../../i18n/i18n-react'; -import { ColorsRGB } from '../../../constants'; -import { Label } from '../../../defguard-ui/components/Layout/Label/Label'; -import { LoaderSpinner } from '../../../defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; -import useApi from '../../../hooks/useApi'; -import { useToaster } from '../../../hooks/useToaster'; -import { QueryKeys } from '../../../queries'; -import { GatewayStatus } from '../../../types'; -import SvgIconArrowSingle from '../../svg/IconArrowSingle'; -import IconInfoError from '../../svg/IconInfoError'; -import SvgIconInfoSuccess from '../../svg/IconInfoSuccess'; -import SvgIconX from '../../svg/IconX'; -import { GatewayStatusIcon } from './GatewayStatusIcon'; -import { GatewayConnectionStatus } from './types'; - -type Props = { - networkId: number; -}; - -const REFETCH_INTERVAL = 5 * 1000; - -export const GatewaysStatus = ({ networkId }: Props) => { - const toaster = useToaster(); - const { - network: { getGatewaysStatus, deleteGateway }, - } = useApi(); - const { LL } = useI18nContext(); - const queryClient = useQueryClient(); - const [floatingOpen, setFloatingOpen] = useState(false); - const { x, y, strategy, refs } = useFloating({ - placement: 'bottom', - strategy: 'fixed', - open: floatingOpen, - onOpenChange: setFloatingOpen, - whileElementsMounted: (refElement, floatingElement, updateFunc) => - autoUpdate(refElement, floatingElement, updateFunc), - middleware: [offset(5)], - }); - - const { - data, - isError, - error: fetchError, - isLoading: queryLoading, - } = useQuery({ - queryFn: () => getGatewaysStatus(networkId), - queryKey: [QueryKeys.FETCH_NETWORK_GATEWAYS_STATUS, networkId], - refetchInterval: REFETCH_INTERVAL, - enabled: !isUndefined(networkId), - }); - - useEffect(() => { - if (fetchError) { - toaster.error(LL.components.gatewaysStatus.messages.error()); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fetchError]); - - const { mutate: deleteGatewayMutation } = useMutation({ - mutationFn: deleteGateway, - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: [QueryKeys.FETCH_NETWORK_GATEWAYS_STATUS], - }); - }, - onError: (err) => { - toaster.error(LL.components.gatewaysStatus.messages.deleteError()); - console.error(err); - }, - }); - - const isLoading = (queryLoading && !data) || !data; - - const getStatus = useMemo(() => { - if (isLoading) { - return GatewayConnectionStatus.LOADING; - } - if (isError) { - return GatewayConnectionStatus.ERROR; - } - if (data) { - const connected = data.filter((g) => g.connected) ?? []; - if (connected.length === 0) { - return GatewayConnectionStatus.DISCONNECTED; - } - if (connected.length === data.length) { - return GatewayConnectionStatus.CONNECTED; - } - return GatewayConnectionStatus.PARTIAL; - } - return GatewayConnectionStatus.ERROR; - }, [data, isError, isLoading]); - - const getMessage = useMemo((): string => { - switch (getStatus) { - case GatewayConnectionStatus.ERROR: - return LL.components.gatewaysStatus.states.error(); - case GatewayConnectionStatus.DISCONNECTED: - return LL.components.gatewaysStatus.states.disconnected(); - case GatewayConnectionStatus.PARTIAL: - return LL.components.gatewaysStatus.states.partial(); - case GatewayConnectionStatus.CONNECTED: - return LL.components.gatewaysStatus.states.connected(); - case GatewayConnectionStatus.LOADING: - return LL.components.gatewaysStatus.states.loading(); - default: - return LL.components.gatewaysStatus.states.error(); - } - }, [LL.components.gatewaysStatus.states, getStatus]); - - const getAnimate = useMemo(() => { - const res: TargetAndTransition = { - color: ColorsRGB.Error, - }; - switch (getStatus) { - case GatewayConnectionStatus.CONNECTED: - res.color = ColorsRGB.Success; - break; - case GatewayConnectionStatus.ERROR: - res.color = ColorsRGB.Error; - break; - case GatewayConnectionStatus.PARTIAL: - res.color = ColorsRGB.Warning; - break; - case GatewayConnectionStatus.DISCONNECTED: - res.color = ColorsRGB.Error; - break; - case GatewayConnectionStatus.LOADING: - res.color = ColorsRGB.GrayLight; - break; - } - return res; - }, [getStatus]); - - const cn = useMemo( - () => - classNames( - 'network-gateways-connection', - `status-${getStatus.valueOf().toLowerCase()}`, - ), - [getStatus], - ); - - return ( - <> -
- -
setFloatingOpen((state) => !state)} - > -
- - {getMessage} - - {!isLoading && } -
- {isLoading ? : } -
-
- - {floatingOpen && data && data?.length > 0 && ( - setFloatingOpen(false)}> - - {data?.map((g) => ( - - deleteGatewayMutation({ - networkId, - gatewayId: g.uid, - }) - } - /> - ))} - - - )} - - - ); -}; - -type GatewayStatusRowProps = { - status: GatewayStatus; - onDismiss: () => void; -}; - -const GatewayStatusRow = ({ status, onDismiss }: GatewayStatusRowProps) => { - const [loading, setLoading] = useState(false); - const cn = () => - classNames('gateway-status-row', { - disconnected: !status.connected, - }); - - return ( -
-
- {status.connected ? : } -
-
-

{status.name}

-

{status.hostname}

-
- {!status.connected && ( - - )} -
- ); -}; diff --git a/web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/GatewaysStatusInfo.tsx b/web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/GatewaysStatusInfo.tsx new file mode 100644 index 0000000000..8742514a39 --- /dev/null +++ b/web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/GatewaysStatusInfo.tsx @@ -0,0 +1,111 @@ +import './style.scss'; + +import clsx from 'clsx'; +import { PropsWithChildren, useMemo, useState } from 'react'; +import Skeleton from 'react-loading-skeleton'; + +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { ArrowSingle } from '../../../../defguard-ui/components/icons/ArrowSingle/ArrowSingle'; +import { ArrowSingleDirection } from '../../../../defguard-ui/components/icons/ArrowSingle/types'; +import { FloatingMenu } from '../../../../defguard-ui/components/Layout/FloatingMenu/FloatingMenu'; +import { FloatingMenuProvider } from '../../../../defguard-ui/components/Layout/FloatingMenu/FloatingMenuProvider'; +import { FloatingMenuTrigger } from '../../../../defguard-ui/components/Layout/FloatingMenu/FloatingMenuTrigger'; +import { Label } from '../../../../defguard-ui/components/Layout/Label/Label'; +import { GatewayStatusIcon } from '../GatewayStatusIcon'; +import { GatewayConnectionStatus } from '../types'; + +type Props = { + totalCount: number; + connectionCount: number; + isLoading?: boolean; + isError?: boolean; + forceStatus?: GatewayConnectionStatus; +} & PropsWithChildren; + +export const GatewaysStatusInfo = ({ + children, + connectionCount, + totalCount, + forceStatus, + isLoading = false, + isError = false, +}: Props) => { + const { LL } = useI18nContext(); + const localLL = LL.components.gatewaysStatus; + const [floatingOpen, setOpen] = useState(false); + + const status = useMemo((): GatewayConnectionStatus => { + if (forceStatus) { + return forceStatus; + } + if (isError) { + return GatewayConnectionStatus.ERROR; + } + if (isLoading) { + return GatewayConnectionStatus.LOADING; + } + if (totalCount === 0 || connectionCount === 0) { + return GatewayConnectionStatus.DISCONNECTED; + } + if (totalCount !== connectionCount) { + return GatewayConnectionStatus.PARTIAL; + } + return GatewayConnectionStatus.CONNECTED; + }, [connectionCount, forceStatus, isError, isLoading, totalCount]); + + const getInfoText = () => { + switch (status) { + case GatewayConnectionStatus.LOADING: + return ''; + case GatewayConnectionStatus.ERROR: + return localLL.states.error(); + case GatewayConnectionStatus.DISCONNECTED: + return localLL.states.none(); + case GatewayConnectionStatus.PARTIAL: + return localLL.states.some({ + count: connectionCount, + }); + case GatewayConnectionStatus.CONNECTED: + return localLL.states.all({ + count: connectionCount, + }); + } + }; + + return ( +
+ + + +
{ + if (totalCount > 0) { + setOpen(true); + } + }} + > + {isLoading && } + {!isLoading && ( +
+

{getInfoText()}

+ +
+ )} + {totalCount > 0 && } +
+
+ + {children} + +
+
+ ); +}; diff --git a/web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/style.scss b/web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/style.scss new file mode 100644 index 0000000000..51445bf520 --- /dev/null +++ b/web/src/shared/components/network/GatewaysStatus/GatewaysStatusInfo/style.scss @@ -0,0 +1,56 @@ +.gateways-status-info { + width: 100%; + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-xs); + min-height: 18px; + + & > label { + user-select: none; + + @include typography(app-modal-3); + } + + .info-track { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + + & > .info { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + user-select: none; + cursor: pointer; + + &.connected { + color: var(--text-positive); + } + + &.disconnected { + cursor: default; + color: var(--text-alert); + } + + &.partial { + color: var(--text-important); + } + + p { + color: inherit; + padding-right: 5px; + text-wrap: nowrap; + @include typography(app-modal-3); + } + + svg { + width: 8px; + height: 8px; + } + } + } +} diff --git a/web/src/shared/components/network/GatewaysStatus/NetworkGatewaysStatus/NetworkGatewaysStatus.tsx b/web/src/shared/components/network/GatewaysStatus/NetworkGatewaysStatus/NetworkGatewaysStatus.tsx new file mode 100644 index 0000000000..72072fab1a --- /dev/null +++ b/web/src/shared/components/network/GatewaysStatus/NetworkGatewaysStatus/NetworkGatewaysStatus.tsx @@ -0,0 +1,41 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import useApi from '../../../../hooks/useApi'; +import { GatewaysFloatingStatus } from '../GatewaysFloatingStatus/GatewaysFloatingStatus'; +import { GatewaysStatusInfo } from '../GatewaysStatusInfo/GatewaysStatusInfo'; + +type Props = { + networkId: number; +}; +export const NetworkGatewaysStatus = ({ networkId }: Props) => { + const { + network: { getGatewaysStatus }, + } = useApi(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['network', networkId, 'gateways'], + queryFn: () => getGatewaysStatus(networkId), + }); + + const [totalConnections, connectedCount] = useMemo(() => { + if (!data) return [0, 0]; + const total = data.length; + const connected = data.reduce( + (count, status) => count + (status.connected ? 1 : 0), + 0, + ); + return [total, connected]; + }, [data]); + + return ( + + {data?.map((status) => )} + + ); +}; diff --git a/web/src/shared/components/svg/IconTagDismiss.tsx b/web/src/shared/components/svg/IconTagDismiss.tsx index 4356bab085..4e5e146120 100644 --- a/web/src/shared/components/svg/IconTagDismiss.tsx +++ b/web/src/shared/components/svg/IconTagDismiss.tsx @@ -1,33 +1,36 @@ -import type { SVGProps } from 'react'; -const SvgIconTagDismiss = (props: SVGProps) => ( - - - - - - - - - - - -); +import { useId, type SVGProps } from 'react'; +const SvgIconTagDismiss = (props: SVGProps) => { + const maskId = useId(); + return ( + + + + + + + + + + + + ); +}; export default SvgIconTagDismiss; diff --git a/web/src/shared/hooks/api/api.ts b/web/src/shared/hooks/api/api.ts index 331f035036..7a54c4bf05 100644 --- a/web/src/shared/hooks/api/api.ts +++ b/web/src/shared/hooks/api/api.ts @@ -502,6 +502,16 @@ export const buildApi = (client: Axios): Api => { }) .then(unpackRequest); + const getAllNetworksStats: Api['network']['getAllNetworksStats'] = (params) => + client + .get('/network/stats', { + params, + }) + .then(unpackRequest); + + const getAllGatewaysStatus: Api['network']['getAllGatewaysStatus'] = () => + client.get('/network/gateways').then(unpackRequest); + return { getAppInfo, getNewVersion, @@ -583,6 +593,8 @@ export const buildApi = (client: Axios): Api => { downloadDeviceConfig, }, network: { + getAllNetworksStats, + getAllGatewaysStatus, addNetwork, importNetwork, mapUserDevices: mapUserDevices, diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index e26b3d162d..5a06e3d3f8 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -108,6 +108,7 @@ export interface AddDeviceRequest { export type GatewayStatus = { connected: boolean; network_id: number; + network_name: string; name?: string; hostname: string; uid: string; @@ -498,6 +499,8 @@ export type AclRuleInfo = { protocols: number[]; }; +export type AllGateWaysResponse = Record>; + export type Api = { getAppInfo: () => Promise; getNewVersion: () => Promise; @@ -615,6 +618,8 @@ export type Api = { getNetworkStats: (data: GetNetworkStatsRequest) => Promise; getGatewaysStatus: (networkId: number) => Promise; deleteGateway: (data: DeleteGatewayRequest) => Promise; + getAllNetworksStats: (data: { from?: string }) => Promise; + getAllGatewaysStatus: () => Promise; }; auth: { login: (data: LoginData) => Promise; @@ -1137,9 +1142,11 @@ export interface NetworkUserStats { export interface WireguardNetworkStats { active_users: number; - active_devices: number; + active_user_devices: number; + active_network_devices: number; current_active_users: number; - current_active_devices: number; + current_active_user_devices: number; + current_active_network_devices: number; upload: number; download: number; transfer_series: NetworkSpeedStats[]; From 8266a0185025a47e4f9cce119eb70f150a970302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 14 May 2025 12:53:37 +0200 Subject: [PATCH 06/10] refactor overview state to account for new routing --- web/src/components/App/App.tsx | 4 +- web/src/components/Navigation/Navigation.tsx | 7 -- web/src/i18n/en/index.ts | 5 ++ web/src/i18n/i18n-types.ts | 29 +++++++ web/src/main.tsx | 4 +- .../network/NetworkGateway/NetworkGateway.tsx | 4 +- web/src/pages/network/NetworkPage.tsx | 6 +- .../network/hooks/useNetworkPageStore.ts | 9 ++- .../overview-index/OverviewIndexPage.tsx | 19 +++-- .../EditLocationsSettingsButton.tsx | 41 ++++++++++ .../OverviewNetworkSelection.tsx | 77 ++++++++++++++++++ .../OverviewTimeSelection.tsx | 36 +++++++++ .../hooks/useOverviewTimeSelection.ts | 26 ++++++ web/src/pages/overview-index/style.scss | 18 ++++- .../OverviewHeader/OverviewHeader.tsx | 68 +++------------- web/src/pages/overview/OverviewPage.tsx | 79 +++++++++++-------- .../overview/OverviewStats/OverviewStats.tsx | 3 +- .../pages/overview/OverviewStats/style.scss | 39 +++++---- web/src/pages/overview/style.scss | 77 +++++++----------- .../GatewaysStatusInfo/style.scss | 6 ++ web/src/shared/defguard-ui | 2 +- web/src/shared/hooks/api/api.ts | 27 ++++--- web/src/shared/query-client.ts | 11 +++ web/src/shared/types.ts | 5 +- 24 files changed, 404 insertions(+), 198 deletions(-) create mode 100644 web/src/pages/overview-index/components/EditLocationsSettingsButton/EditLocationsSettingsButton.tsx create mode 100644 web/src/pages/overview-index/components/OverviewNetworkSelection/OverviewNetworkSelection.tsx create mode 100644 web/src/pages/overview-index/components/OverviewTimeSelection/OverviewTimeSelection.tsx create mode 100644 web/src/pages/overview-index/components/hooks/useOverviewTimeSelection.ts create mode 100644 web/src/shared/query-client.ts diff --git a/web/src/components/App/App.tsx b/web/src/components/App/App.tsx index 2fe67d888d..54a7409553 100644 --- a/web/src/components/App/App.tsx +++ b/web/src/components/App/App.tsx @@ -98,7 +98,7 @@ const App = () => { } /> @@ -106,7 +106,7 @@ const App = () => { } /> diff --git a/web/src/components/Navigation/Navigation.tsx b/web/src/components/Navigation/Navigation.tsx index ac56892266..c0459df94f 100644 --- a/web/src/components/Navigation/Navigation.tsx +++ b/web/src/components/Navigation/Navigation.tsx @@ -88,13 +88,6 @@ export const Navigation = () => { }, ]; let middle: NavigationItem[] = [ - { - title: 'Overview Index wip', - linkPath: '/admin/overview-index', - enabled: true, - adminOnly: true, - icon: , - }, { title: LL.navigation.bar.overview(), linkPath: overviewLink, diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 4d370fbb1d..4794dc0a2d 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1816,6 +1816,11 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do }, }, networkOverview: { + networkSelection: { + all: 'All locations summary', + placeholder: 'Select location', + }, + timeRangeSelectionLabel: '{value: number}h period', pageTitle: 'Location overview', controls: { editNetworks: 'Edit Locations settings', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 64e62cf31a..6ccb3998c0 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -4318,6 +4318,21 @@ type RootTranslation = { } } networkOverview: { + networkSelection: { + /** + * A​l​l​ ​l​o​c​a​t​i​o​n​s​ ​s​u​m​m​a​r​y + */ + all: string + /** + * S​e​l​e​c​t​ ​l​o​c​a​t​i​o​n + */ + placeholder: string + } + /** + * {​v​a​l​u​e​}​h​ ​p​e​r​i​o​d + * @param {number} value + */ + timeRangeSelectionLabel: RequiredParams<'value'> /** * L​o​c​a​t​i​o​n​ ​o​v​e​r​v​i​e​w */ @@ -10222,6 +10237,20 @@ export type TranslationFunctions = { } } networkOverview: { + networkSelection: { + /** + * All locations summary + */ + all: () => LocalizedString + /** + * Select location + */ + placeholder: () => LocalizedString + } + /** + * {value}h period + */ + timeRangeSelectionLabel: (arg: { value: number }) => LocalizedString /** * Location overview */ diff --git a/web/src/main.tsx b/web/src/main.tsx index 84c659960b..77f9306f40 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,7 +1,7 @@ import './shared/scss/styles.scss'; import './shared/defguard-ui/scss/index.scss'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import dayjs from 'dayjs'; import LocalizedFormat from 'dayjs/plugin/localizedFormat'; import utc from 'dayjs/plugin/utc'; @@ -11,11 +11,11 @@ import { createRoot } from 'react-dom/client'; import { AppLoader } from './components/AppLoader'; import { I18nProvider } from './components/I18nProvider'; import { ApiProvider } from './shared/hooks/api/provider'; +import queryClient from './shared/query-client'; dayjs.extend(utc); dayjs.extend(LocalizedFormat); -const queryClient = new QueryClient(); const root = createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/web/src/pages/network/NetworkGateway/NetworkGateway.tsx b/web/src/pages/network/NetworkGateway/NetworkGateway.tsx index 9b4db6f57c..de53a4df3a 100644 --- a/web/src/pages/network/NetworkGateway/NetworkGateway.tsx +++ b/web/src/pages/network/NetworkGateway/NetworkGateway.tsx @@ -5,7 +5,7 @@ import { useCallback, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import { useI18nContext } from '../../../i18n/i18n-react'; -import { GatewaysStatus } from '../../../shared/components/network/GatewaysStatus/GatewaysStatus'; +import { NetworkGatewaysStatus } from '../../../shared/components/network/GatewaysStatus/NetworkGatewaysStatus/NetworkGatewaysStatus'; import { ActionButton } from '../../../shared/defguard-ui/components/Layout/ActionButton/ActionButton'; import { ActionButtonVariant } from '../../../shared/defguard-ui/components/Layout/ActionButton/types'; import { Button } from '../../../shared/defguard-ui/components/Layout/Button/Button'; @@ -151,7 +151,7 @@ export const NetworkGatewaySetup = () => { {LL.gatewaySetup.messages.oneLineInstall()} - + ); }; diff --git a/web/src/pages/network/NetworkPage.tsx b/web/src/pages/network/NetworkPage.tsx index 6c8b4fcd1a..b6eb19d254 100644 --- a/web/src/pages/network/NetworkPage.tsx +++ b/web/src/pages/network/NetworkPage.tsx @@ -21,7 +21,7 @@ export const NetworkPage = () => { network: { getNetworks }, } = useApi(); const { LL } = useI18nContext(); - const setPageStore = useNetworkPageStore((state) => state.setState); + const setNetworks = useNetworkPageStore((state) => state.setNetworks); const { breakpoint } = useBreakpoint(deviceBreakpoints); const { data: networksData } = useQuery({ @@ -32,9 +32,9 @@ export const NetworkPage = () => { useEffect(() => { if (networksData) { - setPageStore({ networks: networksData }); + setNetworks(networksData); } - }, [networksData, setPageStore]); + }, [networksData, setNetworks]); return ( diff --git a/web/src/pages/network/hooks/useNetworkPageStore.ts b/web/src/pages/network/hooks/useNetworkPageStore.ts index 7bd06ce4e6..bc5709b925 100644 --- a/web/src/pages/network/hooks/useNetworkPageStore.ts +++ b/web/src/pages/network/hooks/useNetworkPageStore.ts @@ -9,15 +9,22 @@ type NetworkPageStore = { networks: Network[]; selectedNetworkId: number; setState: (data: Partial) => void; + setNetworks: (data: Network[]) => void; }; export const useNetworkPageStore = createWithEqualityFn()( - (set) => ({ + (set, get) => ({ saveSubject: new Subject(), loading: false, networks: [], selectedNetworkId: 1, setState: (newState) => set(() => newState), + setNetworks: (networks) => { + if (get().selectedNetworkId === undefined) { + set({ selectedNetworkId: networks[0]?.id }); + } + set({ networks }); + }, }), Object.is, ); diff --git a/web/src/pages/overview-index/OverviewIndexPage.tsx b/web/src/pages/overview-index/OverviewIndexPage.tsx index 818447fd6b..8f7e2d8a68 100644 --- a/web/src/pages/overview-index/OverviewIndexPage.tsx +++ b/web/src/pages/overview-index/OverviewIndexPage.tsx @@ -2,7 +2,6 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; import { range } from 'lodash-es'; -import { useMemo } from 'react'; import Skeleton from 'react-loading-skeleton'; import { ExpandableSection } from '../../shared/components/Layout/ExpandableSection/ExpandableSection'; @@ -19,10 +18,11 @@ import { isPresent } from '../../shared/defguard-ui/utils/isPresent'; import useApi from '../../shared/hooks/useApi'; import { useToaster } from '../../shared/hooks/useToaster'; import { Network } from '../../shared/types'; -import { getNetworkStatsFilterValue } from '../overview/helpers/stats'; -import { useOverviewStore } from '../overview/hooks/store/useOverviewStore'; import { OverviewStats } from '../overview/OverviewStats/OverviewStats'; -import { OverviewStatsFilterSelect } from '../overview/OverviewStatsFilterSelect/OverviewStatsFilterSelect'; +import { EditLocationsSettingsButton } from './components/EditLocationsSettingsButton/EditLocationsSettingsButton'; +import { useOverviewTimeSelection } from './components/hooks/useOverviewTimeSelection'; +import { OverviewNetworkSelection } from './components/OverviewNetworkSelection/OverviewNetworkSelection'; +import { OverviewTimeSelection } from './components/OverviewTimeSelection/OverviewTimeSelection'; export const OverviewIndexPage = () => { const { @@ -40,8 +40,10 @@ export const OverviewIndexPage = () => {

All locations overview

- + +
+
{ const toaster = useToaster(); - const statsFilter = useOverviewStore((s) => s.statsFilter); - const from = useMemo(() => getNetworkStatsFilterValue(statsFilter), [statsFilter]); + const { from } = useOverviewTimeSelection(); const { network: { getNetworkStats }, @@ -122,9 +123,7 @@ const NetworkSection = ({ network }: NetworkSectionProps) => { }; const SummaryStats = () => { - const statsFilter = useOverviewStore((s) => s.statsFilter); - - const from = useMemo(() => getNetworkStatsFilterValue(statsFilter), [statsFilter]); + const { from } = useOverviewTimeSelection(); const { network: { getAllNetworksStats }, } = useApi(); diff --git a/web/src/pages/overview-index/components/EditLocationsSettingsButton/EditLocationsSettingsButton.tsx b/web/src/pages/overview-index/components/EditLocationsSettingsButton/EditLocationsSettingsButton.tsx new file mode 100644 index 0000000000..c8ea52b0b9 --- /dev/null +++ b/web/src/pages/overview-index/components/EditLocationsSettingsButton/EditLocationsSettingsButton.tsx @@ -0,0 +1,41 @@ +import { useNavigate, useParams } from 'react-router'; + +import { useI18nContext } from '../../../../i18n/i18n-react'; +import IconEditNetwork from '../../../../shared/components/svg/IconEditNetwork'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../shared/defguard-ui/components/Layout/Button/types'; +import { useNetworkPageStore } from '../../../network/hooks/useNetworkPageStore'; + +export const EditLocationsSettingsButton = () => { + const { LL } = useI18nContext(); + const { networkId } = useParams(); + const navigate = useNavigate(); + const selectedNetwork = parseInt(networkId ?? ''); + const setNetworkPageStore = useNetworkPageStore((s) => s.setState); + + const handleClick = () => { + if (!isNaN(selectedNetwork)) { + setNetworkPageStore({ + selectedNetworkId: selectedNetwork, + }); + } + setNetworkPageStore({ + selectedNetworkId: undefined, + }); + navigate('/admin/network'); + }; + + return ( +