Compare commits

...
Author SHA1 Message Date
Viktor Liu 0844507d8e Remove client-side 5m request timeout cap 2026-03-19 16:03:07 +01:00
Viktor Liu 7812f76bcf Address review 2026-03-19 16:02:16 +01:00
Viktor Liu ba31f63400 Remove useMemo from clusterSupportsCustomPorts to fix stale disabled state 2026-03-19 16:02:16 +01:00
Viktor Liu 5cf41dbc76 Add header auth and access restrictions to reverse proxy 2026-03-19 16:02:16 +01:00
Viktor LiuandEduard Gert aff2365ef7 Add layer 4 protocol support to reverse proxy (#579)
* Add layer 4 proto support

* Fix initialResource fallback and UDP session_idle_timeout

* Fix tlsResourceId init for resource-driven create flows, UDP timeout label

* Address PR review: ServiceMode enum, resource init fix, modal title, a11y

* Add L4 protocol values to ReverseProxyTargetProtocol, remove unsafe double cast

* Add aria-labels to L4 port/host inputs

* Unify domain input for all service modes including L4

* Support L4 proxy events

* Fix custom port reset on edit and show port in L4 service link

* Remove redundant listen port from L4 target cell

* Show link only for HTTP/TLS services, copy-on-click for TCP/UDP

* Move mode badge before domain and use fixed width for alignment

* Fix HTTP services to open as link instead of copy

* Hide old proxy clusters from L4 domain selector

* Move service type inside modal

* Update auth cell

* Add target selector component

* Extract into separate components

* hide services types for not supported clusters

* Remove advanced settings tab in http targetmodal and use accordion instead

* Update advanced settings

* Update target device row

* Update text

* Add type cell

* Fix flat target name cell

* Update modal title

* Fix edit target in flat table

* Remove unused proxycluster interface

* Move proxy type icon into type component

* sync cloud

* use emptyrow

* fix l4 type

* fix duplicate error notification

* Set the correct target type

* Fix subnet host editable

* Fix subnet host editable

* hide selector when initial resource or peer

* Rename dropdown

* Update text

* update status cell

* merge cloud

* Update tooltips

* Address coderabbit comments

* Fix skeleton device card

* Update listen port tooltip

* Adjust padding

* update package-lock.json

* bump next to 16.1.7

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-03-18 17:43:00 +01:00
Zoltan PappandEduard Gert bad057d415 [dashboard] feat: add auto_update_always toggle to client settings (#580)
* [dashboard] feat: add auto_update_always toggle to client settings

Add "Always Update" toggle to the Clients settings tab that controls
whether updates are installed automatically in the background or require
user interaction from the UI. Includes a warning icon and caution callout
when enabled to highlight the risk of disrupting active connections.

* [dashboard] fix: improve auto-update UI clarity and toggle label

Clarify that automatic updates require user interaction by updating the
description. Rename "Always Update" to "Force Automatic Updates" for
clarity. Move warning callout below the toggle switch instead of inside it.

* Update src/modules/settings/ClientSettingsTab.tsx

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-03-16 15:35:44 +01:00
Misha Bragin 4d846e2c94 Improve text for optional resource setiings (#584) 2026-03-12 20:48:09 +01:00
Eduard Gert 15fb6e0b05 Refactor resource modal (#582) 2026-03-12 16:30:51 +01:00
Eduard Gert 55c5525626 Fix resource group policy when adding single resource as destination (#581) 2026-03-11 19:23:59 +01:00
Eduard Gert c0c1f4688e Add proxy events sort (#560)
* Add proxy events sort

* Fix coderabbit comment

* Disable local sort when server pagination is used
2026-03-10 10:10:53 +01:00
Eduard Gert b5a8f751ba Create policies inside resources (#568)
* Add acl tooltips

* Adjust resource modal and add tooltips

* Prevent nextjs navigation trigger on tab change

* Update wording

* add acl into resource

* Refactor resource policies

* Add prop to hide group edit and disable redirect

* Add skeleton loader to network page

* Create policy for new resources

* Show existing policies if groups are matching

* Add confirm dialog after creating resource without policy

* Add dialog if user edits policy that is used in multiple resources

* Add callout when selecting resource groups containing policies

* Add dialog if deleting policies containing resources

* Fix stale policies and new group creation in resource modal

* Remove whitespace

* Fix sort

* Cleanup

* Address coderabbit comments

* Fix policy alignment

* Fix initial resource

* disable selector if user did not select  resource groups

* Consider current resource when editing / deleting policy

* Remove unused mutate

* Fix dot position

* Remove ask for policy

* Fix policy index

* Fix multiple resource confirm dialog on policy cell
2026-03-10 10:10:38 +01:00
Eduard Gert 10a8e7b745 Fix stale certificate issued state (#575)
* Fix stale certificate issued state

* fix coderabbit
2026-03-09 10:08:35 +01:00
Viktor Liu 60e8394010 Add per-target options to reverse proxy (#576) 2026-03-06 18:55:28 +01:00
Eduard Gert 9420214059 Bump minimatch and ajv dependencies (#572) 2026-03-02 11:32:52 +01:00
80 changed files with 5696 additions and 1216 deletions

No files matched your search

+54 -54
View File
@@ -60,7 +60,7 @@
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.562.0",
"next": "^16.1.6",
"next": "^16.1.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
@@ -1213,9 +1213,9 @@
}
},
"node_modules/@next/env": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz",
"integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1229,9 +1229,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz",
"integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==",
"cpu": [
"arm64"
],
@@ -1245,9 +1245,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz",
"integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==",
"cpu": [
"x64"
],
@@ -1261,9 +1261,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz",
"integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==",
"cpu": [
"arm64"
],
@@ -1277,9 +1277,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz",
"integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==",
"cpu": [
"arm64"
],
@@ -1293,9 +1293,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
"integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz",
"integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==",
"cpu": [
"x64"
],
@@ -1309,9 +1309,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz",
"integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==",
"cpu": [
"x64"
],
@@ -1325,9 +1325,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz",
"integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==",
"cpu": [
"arm64"
],
@@ -1341,9 +1341,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz",
"integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==",
"cpu": [
"x64"
],
@@ -3255,13 +3255,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -3665,9 +3665,9 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -5768,9 +5768,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"license": "ISC"
},
"node_modules/for-each": {
@@ -6973,9 +6973,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -7067,14 +7067,14 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz",
"integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==",
"license": "MIT",
"dependencies": {
"@next/env": "16.1.6",
"@next/env": "16.1.7",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -7086,14 +7086,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.1.6",
"@next/swc-darwin-x64": "16.1.6",
"@next/swc-linux-arm64-gnu": "16.1.6",
"@next/swc-linux-arm64-musl": "16.1.6",
"@next/swc-linux-x64-gnu": "16.1.6",
"@next/swc-linux-x64-musl": "16.1.6",
"@next/swc-win32-arm64-msvc": "16.1.6",
"@next/swc-win32-x64-msvc": "16.1.6",
"@next/swc-darwin-arm64": "16.1.7",
"@next/swc-darwin-x64": "16.1.7",
"@next/swc-linux-arm64-gnu": "16.1.7",
"@next/swc-linux-arm64-musl": "16.1.7",
"@next/swc-linux-x64-gnu": "16.1.7",
"@next/swc-linux-x64-musl": "16.1.7",
"@next/swc-win32-arm64-msvc": "16.1.7",
"@next/swc-win32-x64-msvc": "16.1.7",
"sharp": "^0.34.4"
},
"peerDependencies": {
+1 -1
View File
@@ -68,7 +68,7 @@
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.562.0",
"next": "^16.1.6",
"next": "^16.1.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
@@ -25,6 +25,8 @@ export default function ProxyEventsPage() {
() => ({
start_date: dayjs().subtract(7, "day").startOf("day").toISOString(),
end_date: dayjs().endOf("day").toISOString(),
sort_by: "timestamp",
sort_order: "desc",
}),
[],
);
+89 -88
View File
@@ -12,7 +12,6 @@ import {
} from "@components/DropdownMenu";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn, singularize } from "@utils/helpers";
@@ -35,6 +34,7 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
import { Network, NetworkResource, NetworkRouter } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
import { NetworkAccessControlProvider } from "@/modules/networks/NetworkAccessControlProvider";
import {
NetworkProvider,
useNetworksContext,
@@ -49,6 +49,7 @@ import ReverseProxiesProvider, {
flattenReverseProxies,
useReverseProxies,
} from "@/contexts/ReverseProxiesProvider";
import { SkeletonNetwork } from "@components/skeletons/SkeletonNetwork";
export default function NetworkDetailPage() {
const queryParameter = useSearchParams();
@@ -65,7 +66,7 @@ export default function NetworkDetailPage() {
<NetworkOverview network={network} />
</ReverseProxiesProvider>
) : (
<FullScreenLoading />
<SkeletonNetwork />
);
}
@@ -96,103 +97,103 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
return (
<PageContainer>
<NetworkProvider network={network}>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={!permission.networks.read}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<NetworkAccessControlProvider>
<NetworkProvider network={network}>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={!permission.networks.read}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div
className={"w-full lg:w-1/2 flex justify-between items-center"}
>
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center w-full",
!network.description && "gap-2",
)}
className={"w-full lg:w-1/2 flex justify-between items-center"}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
</div>
<NetworkProvider network={network}>
<div
className={cn(
"flex items-center w-full",
!network.description && "gap-2",
)}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
</div>
<NetworkActions />
</NetworkProvider>
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<Tabs
defaultValue={tab}
onValueChange={setTab}
value={tab}
className={"pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"resources"}>
<Layers3Icon size={14} />
{singularize("Resources", network?.resources?.length)}
</TabsTrigger>
<TabsTrigger value={"routing-peers"}>
<PeerIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Routing Peers", network?.routing_peers_count)}
</TabsTrigger>
<TabsTrigger value={"services"}>
<ReverseProxyIcon
size={16}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Services", services.length)}
</TabsTrigger>
</TabsList>
<Tabs
defaultValue={tab}
onValueChange={setTab}
value={tab}
className={"pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"resources"}>
<Layers3Icon size={14} />
{singularize("Resources", network?.resources?.length)}
</TabsTrigger>
<TabsTrigger value={"routing-peers"}>
<PeerIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
<TabsContent value={"resources"} className={"pb-8"}>
<ResourcesTabContent
data={resources}
isLoading={isResourcesLoading}
/>
{singularize("Routing Peers", network?.routing_peers_count)}
</TabsTrigger>
<TabsTrigger value={"services"}>
<ReverseProxyIcon
size={16}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
</TabsContent>
<TabsContent value={"routing-peers"} className={"pb-8"}>
<NetworkRoutingPeersTabContent
routers={routers}
isLoading={isRoutersLoading}
/>
{singularize("Services", services.length)}
</TabsTrigger>
</TabsList>
</TabsContent>
<TabsContent value={"resources"} className={"pb-8"}>
<ResourcesTabContent
data={resources}
isLoading={isResourcesLoading}
/>
</TabsContent>
<TabsContent value={"routing-peers"} className={"pb-8"}>
<NetworkRoutingPeersTabContent
routers={routers}
isLoading={isRoutersLoading}
/>
</TabsContent>
<TabsContent value={"services"} className={"pb-8"}>
<ReverseProxyFlatTargetsTabContent
targets={services}
isLoading={isServicesLoading}
/>
</TabsContent>
</Tabs>
</NetworkProvider>
<TabsContent value={"services"} className={"pb-8"}>
<ReverseProxyFlatTargetsTabContent
targets={services}
isLoading={isServicesLoading}
/>
</TabsContent>
</Tabs>
</NetworkProvider>
</NetworkAccessControlProvider>
</PageContainer>
);
}
+5 -1
View File
@@ -8,8 +8,12 @@ export default function ReverseProxyIcon(props: IconProps) {
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
fill={"currentColor"}
>
<path d="M11.4488 2.1499C11.7903 1.95003 12.2097 1.95003 12.5513 2.1499L16.5018 4.46123L12 7.03523L7.49823 4.46123L11.4488 2.1499ZM6.44447 6.46472L6.44444 10.2784L2.93531 12.3315L7.53662 14.8399L10.8889 12.8787V9.00593L6.44447 6.46472ZM2 14.3992V18.7395C2 19.1477 2.21366 19.5247 2.55984 19.7272L6.44446 22V16.8223L2 14.3992ZM8.66668 22L12 20.0497L15.3333 22V16.7994L12 14.8492L8.66668 16.7993V22ZM17.5556 22L21.4401 19.7272C21.7863 19.5247 22 19.1477 22 18.7395V14.3992L17.5556 16.8223V22ZM21.0647 12.3315L17.5556 10.2784V6.46474L13.1111 9.00593V12.8787L16.4634 14.8399L21.0647 12.3315Z" />
<path
fill={"currentColor"}
d="M11.4488 2.1499C11.7903 1.95003 12.2097 1.95003 12.5513 2.1499L16.5018 4.46123L12 7.03523L7.49823 4.46123L11.4488 2.1499ZM6.44447 6.46472L6.44444 10.2784L2.93531 12.3315L7.53662 14.8399L10.8889 12.8787V9.00593L6.44447 6.46472ZM2 14.3992V18.7395C2 19.1477 2.21366 19.5247 2.55984 19.7272L6.44446 22V16.8223L2 14.3992ZM8.66668 22L12 20.0497L15.3333 22V16.7994L12 14.8492L8.66668 16.7993V22ZM17.5556 22L21.4401 19.7272C21.7863 19.5247 22 19.1477 22 18.7395V14.3992L17.5556 16.8223V22ZM21.0647 12.3315L17.5556 10.2784V6.46474L13.1111 9.00593V12.8787L16.4634 14.8399L21.0647 12.3315Z"
/>
</svg>
);
}
+36 -14
View File
@@ -2,6 +2,7 @@
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { cn } from "@utils/helpers";
import { motion } from "framer-motion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
@@ -23,7 +24,7 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center gap-4 font-medium transition-all [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
"flex flex-1 items-center gap-4 font-medium [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
className,
)}
{...props}
@@ -36,20 +37,41 @@ const AccordionTrigger = React.forwardRef<
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className,
)}
{...props}
>
<div className=" pt-0">{children}</div>
</AccordionPrimitive.Content>
));
>(({ className, children }, ref) => {
const wrapperRef = React.useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = React.useState(false);
React.useEffect(() => {
const el = wrapperRef.current?.closest("[data-state]");
if (!el) return;
const update = () => setIsOpen(el.getAttribute("data-state") === "open");
update();
const observer = new MutationObserver(update);
observer.observe(el, { attributes: true, attributeFilter: ["data-state"] });
return () => observer.disconnect();
}, []);
return (
<div ref={wrapperRef}>
<motion.div
ref={ref}
initial={false}
animate={{
height: isOpen ? "auto" : 0,
opacity: isOpen ? 1 : 0,
}}
transition={{ duration: 0.15, ease: "easeOut" }}
className={cn("overflow-hidden text-sm", className)}
>
<div className="pt-0">{children}</div>
</motion.div>
</div>
);
});
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
+9 -7
View File
@@ -80,13 +80,15 @@ export const DeviceCard = ({
hideTooltip={true}
/>
</span>
<span
className={
"text-sm font-normal text-nb-gray-400 relative whitespace-nowrap"
}
>
<TruncatedText text={descriptionText} maxWidth={"160px"} />
</span>
{descriptionText && (
<span
className={
"text-sm font-normal text-nb-gray-400 relative whitespace-nowrap"
}
>
<TruncatedText text={descriptionText} maxWidth={"160px"} />
</span>
)}
</div>
</div>
);
+47 -8
View File
@@ -1,29 +1,68 @@
import * as React from "react";
import FullTooltip from "@components/FullTooltip";
import { HelpCircle } from "lucide-react";
import { cn } from "@utils/helpers";
import { TooltipVariants } from "@components/Tooltip";
type Props = {
content: React.ReactNode;
children: React.ReactNode;
children?: React.ReactNode;
interactive?: boolean;
};
className?: string;
triggerClassName?: string;
align?: "start" | "center" | "end";
side?: "top" | "right" | "bottom" | "left";
alignOffset?: number;
sideOffset?: number;
iconSize?: number;
delayDuration?: number;
} & TooltipVariants;
export const HelpTooltip = ({
content,
children,
interactive = true,
interactive = false,
className,
variant = "default",
triggerClassName,
align = "start",
side = "top",
alignOffset = 0,
sideOffset,
iconSize = 12,
delayDuration = 300,
}: Props) => {
return (
<>
<FullTooltip
interactive={interactive}
side={"top"}
align={"start"}
alignOffset={0}
side={side}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
delayDuration={delayDuration}
variant={variant}
className={
"inline underline decoration-dashed underline-offset-[3px] decoration-nb-gray-300 cursor-help transition-all hover:decoration-white"
}
content={content}
content={
<div className={cn("max-w-xs text-xs", className)}>{content}</div>
}
>
{children}
{children ? (
children
) : (
<span
className={cn(
"p-2 -m-2 inline-flex items-center justify-center relative top-[1px] group/help",
triggerClassName,
)}
>
<HelpCircle
size={iconSize}
className={"text-nb-gray-300 group-hover/help:text-nb-gray-100"}
/>
</span>
)}
</FullTooltip>
</>
);
+17 -6
View File
@@ -18,6 +18,7 @@ export interface NotifyProps<T> {
icon?: React.ReactNode;
backgroundColor?: string;
preventSuccessToast?: boolean;
showOnlyError?: boolean;
errorMessages?: ErrorResponse[];
}
@@ -36,6 +37,7 @@ export default function Notification<T>({
loadingMessage,
duration = 3500,
preventSuccessToast = false,
showOnlyError = false,
errorMessages,
}: NotificationProps<T>) {
const [error, setError] = useState("");
@@ -49,10 +51,13 @@ export default function Notification<T>({
const startTimer = useCallback(() => {
if (timerRef.current) return;
startTimeRef.current = Date.now();
timerRef.current = setTimeout(() => {
timerRef.current = null;
toast.dismiss(toastId);
}, Math.max(0, remainingRef.current));
timerRef.current = setTimeout(
() => {
timerRef.current = null;
toast.dismiss(toastId);
},
Math.max(0, remainingRef.current),
);
}, [toastId]);
const pauseTimer = useCallback(() => {
@@ -88,7 +93,10 @@ export default function Notification<T>({
}
});
observer.observe(toastEl, { attributes: true, attributeFilter: ["data-expanded"] });
observer.observe(toastEl, {
attributes: true,
attributeFilter: ["data-expanded"],
});
// Start immediately if not expanded
const expanded = toastEl.getAttribute("data-expanded") === "true";
@@ -106,7 +114,7 @@ export default function Notification<T>({
promise
.then(() => {
setLoading(false);
if (preventSuccessToast) {
if (showOnlyError || preventSuccessToast) {
toast.dismiss(toastId);
} else {
setReadyToDismiss(true);
@@ -136,6 +144,9 @@ export default function Notification<T>({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hideUntilError = showOnlyError && loading && !error;
if (hideUntilError) return null;
return (
<motion.div
ref={notificationRef}
+72 -8
View File
@@ -30,6 +30,7 @@ import {
MonitorSmartphoneIcon,
NetworkIcon,
SearchIcon,
ShieldCheck,
WorkflowIcon,
} from "lucide-react";
import * as React from "react";
@@ -40,7 +41,7 @@ import { useElementSize } from "@/hooks/useElementSize";
import type { Group, GroupPeer, GroupResource } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer";
import { PolicyRuleResource } from "@/interfaces/Policy";
import { Policy, PolicyRuleResource } from "@/interfaces/Policy";
import { User } from "@/interfaces/User";
import { HorizontalUsersStack } from "@/modules/users/HorizontalUsersStack";
import { PeerOperatingSystemIcon } from "@/modules/peers/PeerOperatingSystemIcon";
@@ -71,6 +72,7 @@ interface MultiSelectProps {
showResourceCounter?: boolean;
showResources?: boolean;
showPeers?: boolean;
showPeerCounter?: boolean;
hideGroupsTab?: boolean;
tabOrder?: ("groups" | "peers" | "resources")[];
closeOnSelect?: boolean;
@@ -83,6 +85,8 @@ interface MultiSelectProps {
users?: User[];
placeholderForSearch?: string;
resourceIds?: string[];
additionalResources?: NetworkResource[];
policies?: Policy[];
}
export function PeerGroupSelector({
onChange,
@@ -101,6 +105,7 @@ export function PeerGroupSelector({
showResourceCounter = true,
showResources = false,
showPeers = false,
showPeerCounter = true,
hideGroupsTab = false,
tabOrder,
closeOnSelect = false,
@@ -113,11 +118,21 @@ export function PeerGroupSelector({
users,
placeholderForSearch = 'Search groups or add new group by pressing "Enter"...',
resourceIds,
additionalResources,
policies,
}: Readonly<MultiSelectProps>) {
const { data: resources, isLoading: isResourcesLoading } = useFetchApi<
const { data: fetchedResources, isLoading: isResourcesLoading } = useFetchApi<
NetworkResource[]
>("/networks/resources");
const resources = useMemo(() => {
if (!additionalResources?.length) return fetchedResources;
const additional = additionalResources.filter(
(ar) => !fetchedResources?.some((r) => r.id === ar.id),
);
return [...(fetchedResources || []), ...additional];
}, [fetchedResources, additionalResources]);
const { data: peers, isLoading: isPeersLoading } =
useFetchApi<Peer[]>("/peers");
@@ -329,7 +344,7 @@ export function PeerGroupSelector({
"min-h-[46px] w-full relative items-center group",
"border border-neutral-200 dark:border-nb-gray-700 justify-between py-2 px-3",
"rounded-md bg-white text-sm dark:bg-nb-gray-900/40 flex dark:text-neutral-400/70 text-neutral-500 cursor-pointer hover:dark:bg-nb-gray-900/50",
"disabled:pointer-events-none disabled:opacity-30 transition-all",
"disabled:pointer-events-none disabled:opacity-60 transition-all",
)}
disabled={disabled}
data-cy={dataCy}
@@ -343,7 +358,14 @@ export function PeerGroupSelector({
{resource && (
<ResourceBadge
className={"py-[3px]"}
resource={resources?.find((r) => r.id === resource.id)}
resource={
resources?.find((r) => r.id === resource.id) ??
({
id: resource.id,
name: resource.id,
type: resource.type,
} as NetworkResource)
}
peer={peers?.find((p) => p.id === resource.id)}
onClick={(e) => {
e.preventDefault();
@@ -569,12 +591,21 @@ export function PeerGroupSelector({
<ResourcesCounter group={option} />
)}
{policies && (
<PolicyCounter
group={option}
policies={policies}
/>
)}
<div className={"flex gap-4 items-center"}>
{!users ? (
<PeerCounter
group={option}
showResourceCounter={showResourceCounter}
/>
showPeerCounter && (
<PeerCounter
group={option}
showResourceCounter={showResourceCounter}
/>
)
) : (
<UsersCounter
group={option}
@@ -790,6 +821,39 @@ const ResourcesCounter = ({ group }: { group: Group }) => {
) : null;
};
const PolicyCounter = ({
group,
policies,
}: {
group: Group;
policies: Policy[];
}) => {
const count = useMemo(() => {
if (!group.id) return 0;
return policies.filter((policy) => {
const destinations = policy.rules?.[0]?.destinations as
| (Group | string)[]
| undefined;
return destinations?.some((d) =>
typeof d === "string" ? d === group.id : d.id === group.id,
);
}).length;
}, [group.id, policies]);
if (count === 0) return null;
return (
<div
className={
"text-nb-gray-300 font-medium flex items-center gap-2 transition-all"
}
>
<ShieldCheck size={14} className={"shrink-0"} />
{count} {count === 1 ? "Policy" : "Policies"}
</div>
);
};
const resourcesSearchPredicate = (item: NetworkResource, query: string) => {
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
+4
View File
@@ -8,6 +8,7 @@ type Props = {
description: ReactNode;
icon?: ReactNode;
className?: string;
disabled?: boolean;
};
export const RadioCard = ({
@@ -16,15 +17,18 @@ export const RadioCard = ({
description,
className,
icon,
disabled,
}: Props) => {
return (
<RadioGroup.Item
value={value}
disabled={disabled}
className={cn(
"peer relative block cursor-pointer rounded-lg border border-nb-gray-900 bg-nb-gray-930/60 px-5 py-3 transition-all focus:outline-none",
"data-[state=checked]:border-nb-gray-400 data-[state=checked]:bg-nb-gray-920",
"outline-none focus:ring-0 focus:bg-nb-gray-930 focus:border-nb-gray-920",
"hover:bg-nb-gray-930",
"disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-nb-gray-930/60",
className,
)}
>
+46 -10
View File
@@ -75,23 +75,59 @@ SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
extra?: React.ReactNode;
icon?: React.ReactNode;
description?: React.ReactNode;
}
>(({ className, children, extra, icon, description, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-nb-gray-900 dark:focus:text-neutral-50 dark:text-gray-400 cursor-pointer",
"relative flex w-full select-none items-center rounded-md py-1.5 text-sm outline-none focus:bg-gray-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-nb-gray-900 dark:focus:text-neutral-50 dark:text-gray-400 cursor-pointer",
icon ? "pl-2 pr-8" : "pl-8 pr-2",
description && "py-2",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
{icon ? (
<>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<div className="flex items-center gap-3">
<span className="flex-shrink-0">{icon}</span>
<div className="flex flex-col">
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
{description && (
<span className="text-xs text-nb-gray-300 font-normal">
{description}
</span>
)}
</div>
</div>
</>
) : (
<>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<div className="flex flex-col">
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
{description && (
<span className="text-xs text-nb-gray-300 font-normal">
{description}
</span>
)}
</div>
</>
)}
{extra}
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
+1
View File
@@ -38,6 +38,7 @@ const ModalOverlay = React.forwardRef<
"bg-black/30 dark:bg-black/40 backdrop-blur-sm",
className,
)}
style={{ scrollbarGutter: "stable both-edges" }}
{...props}
/>
));
@@ -1,15 +1,20 @@
import * as React from "react";
import Skeleton from "react-loading-skeleton";
import { cn } from "@utils/helpers";
export const SkeletonDeviceCard = () => {
type Props = {
className?: string;
};
export const SkeletonDeviceCard = ({ className = "min-h-[59px]" }: Props) => {
return (
<div className={"min-h-[59px] relative -left-2"}>
<div className={"py-2 pr-4 pl-2 flex gap-3"}>
<Skeleton height={36} width={36} />
<div className={"flex flex-col pr-[1.15rem]"}>
<Skeleton height={16} width={70} />
<Skeleton height={16} width={140} />
</div>
<div
className={cn("py-2 pr-4 pl-2 flex gap-3 relative -left-2", className)}
>
<Skeleton height={36} width={36} />
<div className={"flex flex-col pr-[1.15rem]"}>
<Skeleton height={16} width={70} />
<Skeleton height={16} width={140} />
</div>
</div>
);
@@ -0,0 +1,42 @@
import * as React from "react";
import { useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
import SkeletonTable from "@components/skeletons/SkeletonTable";
export const SkeletonNetwork = ({ delay = 400 }: { delay?: number }) => {
const [show, setShow] = useState(delay === 0);
useEffect(() => {
if (delay === 0) return;
const timer = setTimeout(() => setShow(true), delay);
return () => clearTimeout(timer);
}, [delay]);
if (!show) return null;
return (
<div className={"p-default py-6 w-full"}>
<Skeleton height={24} width={240} className={"mb-4"} />
<div className={"mb-8 flex items-center gap-4"}>
<Skeleton height={48} width={48} />
<Skeleton height={20} width={200} />
</div>
<div className={"mb-4"}>
<Skeleton height={106} className={"mb-2 w-full max-w-[574px]"} />
</div>
<div className={"flex items-center gap-4 mb-8"}>
<Skeleton height={24} width={130} />
<Skeleton height={24} width={130} />
<Skeleton height={24} width={130} />
</div>
<div>
<Skeleton height={16} width={530} className={"w-full max-w-[530px]"} />
<Skeleton height={16} width={430} className={"w-full max-w-[430px]"} />
</div>
<div className={"w-full"}>
<SkeletonTable withHeader={false} />
</div>
</div>
);
};
+1
View File
@@ -296,6 +296,7 @@ export function DataTable<TData, TValue>({
autoResetAll: false,
autoResetExpanded: false,
manualPagination: manualPagination,
manualSorting: serverSidePagination,
manualFiltering: manualFiltering || manualColumnFiltering,
pageCount: pageCount,
state: {
+14 -5
View File
@@ -5,6 +5,7 @@ import { IconSortAscending, IconSortDescending } from "@tabler/icons-react";
import type { Column } from "@tanstack/table-core";
import { cn } from "@utils/helpers";
import React from "react";
import { useOptionalServerPagination } from "@/contexts/ServerPaginationProvider";
type Props = {
column: Column<any>;
@@ -13,6 +14,7 @@ type Props = {
center?: boolean;
className?: string;
sorting?: boolean;
name?: string;
};
export default function DataTableHeader({
children,
@@ -21,15 +23,22 @@ export default function DataTableHeader({
center,
className,
sorting = true,
name,
}: Props) {
const serverPagination = useOptionalServerPagination();
const handleSort = () => {
const direction = column.getIsSorted() === "asc" ? "desc" : "asc";
column.toggleSorting(direction === "desc");
if (name && serverPagination?.setSort) {
serverPagination.setSort(name, direction);
}
};
return (
<FullTooltip content={tooltip} disabled={!tooltip}>
<div
onClick={
sorting
? () => column.toggleSorting(column.getIsSorted() === "asc")
: undefined
}
onClick={sorting ? handleSort : undefined}
className={cn(
"flex items-center whitespace-nowrap gap-2 dark:text-gray-400 transition-all select-none text-xs tracking-wide",
sorting &&
+14 -4
View File
@@ -26,6 +26,7 @@ type Props = {
showResources?: boolean;
redirectGroupTab?: string;
showUsers?: boolean;
disableRedirect?: boolean;
};
export default function MultipleGroups({
@@ -37,6 +38,7 @@ export default function MultipleGroups({
showResources = false,
showUsers = false,
redirectGroupTab,
disableRedirect = false,
}: Readonly<Props>) {
const { permission } = usePermissions();
@@ -64,6 +66,7 @@ export default function MultipleGroups({
{firstGroup && (
<GroupBadge
group={firstGroup}
showNewBadge={true}
className={
permission.groups.update ? "group-hover:bg-nb-gray-800" : ""
}
@@ -101,7 +104,7 @@ export default function MultipleGroups({
return (
group && (
<div
key={group.id}
key={group?.id || group?.name}
className={
"flex gap-2 items-center justify-between w-full"
}
@@ -110,16 +113,23 @@ export default function MultipleGroups({
group={group}
className={"py-0"}
textClassName={"py-1.5"}
redirectToGroupPage={true}
showNewBadge={true}
redirectToGroupPage={!disableRedirect}
redirectGroupTab={redirectGroupTab}
></GroupBadge>
<ArrowRightIcon size={14} />
{showResources ? (
<ResourceCountBadge group={group} />
<ResourceCountBadge
group={group}
disableRedirect={disableRedirect}
/>
) : showUsers ? (
<UserCountStack group={group} />
) : (
<PeerCountBadge group={group} />
<PeerCountBadge
group={group}
disableRedirect={disableRedirect}
/>
)}
</div>
)
+5 -2
View File
@@ -10,6 +10,7 @@ import ResourceCountBadge from "@components/ui/ResourceCountBadge";
type Props = {
group?: Group;
disableRedirect?: boolean;
} & React.HTMLAttributes<HTMLDivElement> &
BadgeVariants;
@@ -17,6 +18,7 @@ export default function PeerCountBadge({
group,
variant = "gray",
className,
disableRedirect = false,
}: Props) {
const router = useRouter();
const { dropdownOptions, groups } = useGroups();
@@ -35,7 +37,8 @@ export default function PeerCountBadge({
return peerCount;
}, [currentGroup]);
const canRedirect = !!group?.id && group?.name !== "All";
const canRedirect =
!!group?.id && group?.name !== "All" && !disableRedirect;
const onClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
@@ -46,7 +49,7 @@ export default function PeerCountBadge({
const showResources = resourcesCount > 0 && peerCount === 0;
return showResources ? (
<ResourceCountBadge group={group} />
<ResourceCountBadge group={group} disableRedirect={disableRedirect} />
) : (
<Badge
variant={variant}
+6 -1
View File
@@ -7,15 +7,20 @@ import { Group } from "@/interfaces/Group";
type Props = {
group?: Group;
disableRedirect?: boolean;
} & React.HTMLAttributes<HTMLDivElement> &
BadgeVariants;
export default function ResourceCountBadge({ group }: Props) {
export default function ResourceCountBadge({
group,
disableRedirect = false,
}: Props) {
const router = useRouter();
const hasId = !!group?.id;
const onClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
if (disableRedirect) return;
if (hasId) router.push(`/group?id=${group?.id}&tab=resources`);
};
Loaded 20 of 80 files, more files were not shown because too many files have changed in this diff. Show more