Add improvements to new networks features (#439)
Some checks failed
build and push / build_n_push (push) Has been cancelled

* Fix wrong ui state for routing peer modal in networks

* Add confirmation dialog when blocking users

* Keep peer sort order when switching pages

* Update sidebar navigation order and remove deprecation notice

* Fix issue when hovering over truncated text in a group badge closes the multiple groups popover

* Update group text in network resource modal

* Update networks page text

* Fix line height

* Add search to resource table

* Switch networks flow to create first resources and then add routers

* Add enabled toggle to routing peers

* Add enabled toggle to network resources

* Add resource group modal and adjust tables

* Clarify networks

* Fix not properly aligned horizontal scroll bar

* Add option to install netbird after creating a setup key

* Fix text for install netbird modal

* Show resources count in group settings

* Fix "no results" and "no routing peers" text showing at the same time

* Fix wording

* Fix resource policy count

* Hide resource count when selection source groups

* Extend networks routing peer modal with option to create a setup key and install netbird

* Add option for horizontal stepper

* Generate setup key when installing netbird from routing peer modal

* Add confirm dialog to let the user know a one-off setup-key will be created. This avoids accidental clicking and later confusion on the setup keys page

---------

Co-authored-by: Misha Bragin <bangvalo@gmail.com>
This commit is contained in:
Eduard Gert
2025-01-20 16:18:21 +01:00
committed by GitHub
parent 43e5d5cf53
commit 25be69e7bb
42 changed files with 1629 additions and 413 deletions

347
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -921,6 +922,352 @@
}
}
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.4.tgz",
"integrity": "sha512-QSUUnRA3PQ2UhvoCv3eYvMnCAgGQW+sTu86QPuNb+ZMi+ZENd6UWpiXbcWDQ4AEaKF9KKpCHBeaJz9Rw6lRlaQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.3",
"@radix-ui/react-popper": "1.2.1",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
"integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-portal": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-size": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",

View File

@@ -19,6 +19,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",

View File

@@ -14,7 +14,6 @@ import PeersProvider from "@/contexts/PeersProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route";
import PageContainer from "@/layouts/PageContainer";
import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
const NetworkRoutesTable = lazy(
@@ -40,9 +39,7 @@ export default function NetworkRoutes() {
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>
Network Routes <NetworkRoutesDeprecationInfo size={18} />
</h1>
<h1 ref={headingRef}>Network Routes</h1>
<Paragraph>
Network routes allow you to access other networks like LANs and
VPCs without installing NetBird on every resource.

View File

@@ -31,12 +31,15 @@ export default function Networks() {
</Breadcrumbs>
<h1 ref={headingRef}>Networks</h1>
<Paragraph>
Networks allow you to access other resources like LANs and VPCs
without installing NetBird on every device.
Networks allow you to access internal resources in LANs and VPCs without
installing NetBird on every machine.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink href={"https://docs.netbird.io/how-to/networks"} target={"_blank"}>
<InlineLink
href={"https://docs.netbird.io/how-to/networks"}
target={"_blank"}
>
Networks
<ExternalLinkIcon size={12} />
</InlineLink>

View File

@@ -30,7 +30,7 @@ import * as React from "react";
import { Fragment, useEffect, useMemo, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import { useElementSize } from "@/hooks/useElementSize";
import type {Group, GroupPeer, GroupResource} from "@/interfaces/Group";
import type { Group, GroupPeer, GroupResource } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer";
@@ -48,6 +48,7 @@ interface MultiSelectProps {
showRoutes?: boolean;
disabledGroups?: Group[];
dataCy?: string;
showResourceCounter?: boolean;
}
export function PeerGroupSelector({
onChange,
@@ -63,6 +64,7 @@ export function PeerGroupSelector({
showRoutes = false,
disabledGroups,
dataCy = "group-selector-dropdown",
showResourceCounter = true,
}: Readonly<MultiSelectProps>) {
const { groups, dropdownOptions, setDropdownOptions, addDropdownOptions } =
useGroups();
@@ -105,20 +107,34 @@ export function PeerGroupSelector({
const groupPeers: GroupPeer[] | undefined =
(group?.peers as GroupPeer[]) || [];
const groupResources: GroupResource[] | undefined =
(group?.resources as GroupResource[]) || [];
(group?.resources as GroupResource[]) || [];
if (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
if (!group && !option) {
addDropdownOptions([{ name: name, peers: groupPeers, resources: groupResources }]);
addDropdownOptions([
{ name: name, peers: groupPeers, resources: groupResources },
]);
}
if (max == 1 && values.length == 1) {
onChange([{ name: name, id: group?.id, peers: groupPeers, resources: groupResources }]);
onChange([
{
name: name,
id: group?.id,
peers: groupPeers,
resources: groupResources,
},
]);
} else {
onChange((previous) => [
...previous,
{ name: name, id: group?.id, peers: groupPeers, resources: groupResources },
{
name: name,
id: group?.id,
peers: groupPeers,
resources: groupResources,
},
]);
}
@@ -396,7 +412,9 @@ export function PeerGroupSelector({
<AccessControlGroupCount group_id={option.id} />
)}
<ResourcesCounter group={option} />
{showResourceCounter && (
<ResourcesCounter group={option} />
)}
<div
className={

View File

@@ -166,15 +166,17 @@ export function PeerSelector({
placeholder={"Search for peers by name or ip..."}
/>
{unfilteredItems.length == 0 && (
<DropdownInfoText>
{
"Seems like you don't have any linux peers to assign as a routing peer."
}
</DropdownInfoText>
{unfilteredItems.length == 0 && !search && (
<div className={"max-w-xs mx-auto"}>
<DropdownInfoText>
{
"Seems like you don't have any Linux peers to assign as a routing peer."
}
</DropdownInfoText>
</div>
)}
{filteredItems.length == 0 && (
{filteredItems.length == 0 && search != "" && (
<DropdownInfoText>
There are no peers matching your search.
</DropdownInfoText>

View File

@@ -1,5 +1,3 @@
"use client";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@utils/helpers";
import * as React from "react";
@@ -15,46 +13,31 @@ const ScrollArea = React.forwardRef<
>(({ className, children, withoutViewport = false, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn(
"relative will-change-scroll webkit-scroll",
className,
"overflow-hidden",
)}
className={cn("relative overflow-hidden", className)}
{...props}
>
{withoutViewport ? (
children
) : (
<ScrollAreaViewport disableOverflowY={false}>
{children}
</ScrollAreaViewport>
<ScrollAreaViewport>{children}</ScrollAreaViewport>
)}
<ScrollBar />
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
type AdditionalScrollAreaViewportProps = {
disableOverflowY?: boolean;
};
const ScrollAreaViewport = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport> &
AdditionalScrollAreaViewportProps
>(({ disableOverflowY = true, ...props }, ref) => {
return (
<ScrollAreaPrimitive.Viewport
ref={ref}
className="h-full w-full rounded-[inherit] will-change-scroll webkit-scroll"
{...props}
style={
disableOverflowY ? { overflowY: undefined, ...props.style } : undefined
}
/>
);
});
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<ScrollAreaPrimitive.Viewport
ref={ref}
className={cn("h-full w-full rounded-[inherit]", className)}
{...props}
/>
));
ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName;
const ScrollBar = React.forwardRef<
@@ -63,14 +46,11 @@ const ScrollBar = React.forwardRef<
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
style={{ boxSizing: "unset", overflow: undefined }}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
"flex select-none touch-none transition-colors",
orientation === "vertical" && "h-full w-2.5 p-[1px]",
orientation === "horizontal" && "w-full h-2.5 p-[1px] bottom-0",
className,
)}
{...props}
@@ -79,6 +59,7 @@ const ScrollBar = React.forwardRef<
className={cn(
"relative rounded-full bg-neutral-200 dark:bg-nb-gray-800",
orientation === "vertical" && "flex-1",
orientation === "horizontal" && "h-full",
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>

View File

@@ -60,10 +60,12 @@ export default function SidebarItem({
<li className={"px-4 cursor-pointer"}>
<button
className={classNames(
"rounded-lg text-[.95rem] w-full ",
"rounded-lg text-[.87rem] w-full ",
"font-normal ",
className,
isChild ? "pl-7 pr-2 py-2 mt-1 mb-0.5" : "py-2 px-3",
isChild
? "pl-7 pr-2 py-[.45rem] mt-1 mb-0.5"
: "py-[.45rem] px-3",
isActive
? "text-gray-900 bg-gray-200 dark:text-white dark:bg-nb-gray-900"
: "text-gray-600 hover:bg-gray-200 dark:text-nb-gray-400 dark:hover:bg-nb-gray-900/50",

View File

@@ -4,9 +4,18 @@ import React from "react";
type Props = {
children: React.ReactNode;
className?: string;
horizontal?: boolean;
};
export default function Steps({ children, className }: Props) {
return <div className={cn("pt-4", className)}>{children}</div>;
export default function Steps({
children,
className,
horizontal = false,
}: Readonly<Props>) {
return (
<div className={cn("pt-4", horizontal && "flex", className)}>
{children}
</div>
);
}
type StepProps = {
@@ -14,21 +23,32 @@ type StepProps = {
step: number;
line?: boolean;
center?: boolean;
horizontal?: boolean;
};
const Step = ({ children, step, line = true, center = false }: StepProps) => {
const Step = ({
children,
step,
line = true,
center = false,
horizontal,
}: StepProps) => {
return (
<div
className={cn(
"flex gap-4 items-start min-w-full justify-start relative pb-6 -mx-1.5 group px-[2px]",
"flex gap-4 items-start justify-start relative pb-6 -mx-1.5 group px-[2px]",
center && "items-center",
horizontal ? "flex-col items-center" : "min-w-full",
)}
>
{line && (
<span
className={
"h-full w-[2px] bg-nb-gray-100 dark:bg-nb-gray-800 absolute left-0 ml-[18px] z-0 transition-all"
}
className={cn(
"bg-nb-gray-100 dark:bg-nb-gray-800 z-0 transition-all",
horizontal
? "w-full h-[2px] absolute mt-[16px] transform translate-x-1/2"
: "h-full w-[2px] absolute left-0 ml-[18px]",
)}
></span>
)}

View File

@@ -1,5 +1,6 @@
import Badge from "@components/Badge";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { NewBadge } from "@components/ui/NewBadge";
import TruncatedText from "@components/ui/TruncatedText";
import { cn } from "@utils/helpers";
import { FolderGit2, XIcon } from "lucide-react";
import * as React from "react";
@@ -37,18 +38,9 @@ export default function GroupBadge({
>
<FolderGit2 size={12} className={"shrink-0"} />
<TextWithTooltip text={group?.name || ""} maxChars={20} />
<TruncatedText text={group?.name || ""} maxChars={20} />
{children}
{isNew && showNewBadge && (
<span
className={
"text-[7px] relative top-[.25px] leading-[0] bg-green-900 border border-green-500/20 py-1.5 px-1 rounded-[3px] text-green-400"
}
>
NEW
</span>
)}
{isNew && showNewBadge && <NewBadge />}
{showX && (
<XIcon
size={12}

View File

@@ -0,0 +1,16 @@
import * as React from "react";
type Props = {
text?: string;
};
export const NewBadge = ({ text = "NEW" }: Props) => {
return (
<span
className={
"text-[7px] relative top-[.25px] leading-[0] bg-green-900 border border-green-500/20 py-1.5 px-1 rounded-[3px] text-green-400"
}
>
{text}
</span>
);
};

View File

@@ -0,0 +1,78 @@
import * as HoverCard from "@radix-ui/react-hover-card";
import { cn } from "@utils/helpers";
import React, { useMemo, useState } from "react";
type Props = {
text?: string;
className?: string;
maxChars?: number;
hideTooltip?: boolean;
};
export default function TruncatedText({
text,
className,
maxChars = 40,
hideTooltip = false,
}: Props) {
const charCount = useMemo(() => {
if (!text) return 0;
return text.length;
}, [text]);
const isDisabled = charCount <= maxChars || hideTooltip;
const [open, setOpen] = useState(false);
if (isDisabled) {
return (
<div
className={"w-full min-w-0 inline-block"}
style={{
maxWidth: `${maxChars - 2}ch`,
}}
>
<div className={cn(className, "truncate")}>{text}</div>
</div>
);
}
return (
<HoverCard.Root
openDelay={650}
closeDelay={100}
open={open}
onOpenChange={setOpen}
>
<HoverCard.Trigger asChild={true}>
<div
className={"w-full min-w-0 inline-block"}
style={{
maxWidth: `${maxChars - 2}ch`,
}}
>
<div className={cn(className, "truncate")}>{text}</div>
</div>
</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content
onMouseLeave={() => setOpen(false)}
onMouseEnter={() => setOpen(false)}
alignOffset={20}
sideOffset={4}
className={cn(
"z-[9999] overflow-hidden rounded-md border border-neutral-200 bg-white text-sm text-neutral-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-nb-gray-930 dark:bg-nb-gray-940 dark:text-neutral-50",
className,
"px-3 py-1.5",
)}
>
<div className={"text-neutral-300 flex flex-col gap-1"}>
<div className={"max-w-xs break-all whitespace-normal text-xs"}>
{text}
</div>
</div>
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
}

View File

@@ -16,6 +16,7 @@ export interface NetworkRouter {
peer_groups?: string[];
metric: number;
masquerade: boolean;
enabled: boolean;
}
export interface NetworkResource {
@@ -25,4 +26,5 @@ export interface NetworkResource {
address: string;
groups?: string[] | Group[];
type?: "domain" | "host" | "subnet";
enabled: boolean;
}

View File

@@ -281,6 +281,7 @@ export function AccessControlModalContent({
onChange={setSourceGroups}
values={sourceGroups}
saveGroupAssignments={useSave}
showResourceCounter={false}
/>
</div>
<PolicyDirection
@@ -391,9 +392,7 @@ export function AccessControlModalContent({
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/manage-network-access"
}
href={"https://docs.netbird.io/how-to/manage-network-access"}
target={"_blank"}
>
Access Controls

View File

@@ -10,6 +10,7 @@ import { Network, NetworkResource, NetworkRouter } from "@/interfaces/Network";
import { AccessControlModalContent } from "@/modules/access-control/AccessControlModal";
import NetworkModal from "@/modules/networks/NetworkModal";
import NetworkResourceModal from "@/modules/networks/resources/NetworkResourceModal";
import { ResourceGroupModal } from "@/modules/networks/resources/ResourceGroupModal";
import NetworkRoutingPeerModal from "@/modules/networks/routing-peers/NetworkRoutingPeerModal";
type Props = {
@@ -23,6 +24,10 @@ const NetworksContext = React.createContext(
openEditNetworkModal: (network: Network) => void;
openCreateNetworkModal: () => void;
openResourceModal: (network: Network, resource?: NetworkResource) => void;
openResourceGroupModal: (
network: Network,
resource?: NetworkResource,
) => void;
openPolicyModal: (network?: Network, resource?: NetworkResource) => void;
deleteNetwork: (network: Network) => void;
deleteResource: (network: Network, resource: NetworkResource) => void;
@@ -49,6 +54,7 @@ export const NetworkProvider = ({ children, network }: Props) => {
const [routingPeerModal, setRoutingPeerModal] = useState(false);
const [networkModal, setNetworkModal] = useState(false);
const [resourceModal, setResourceModal] = useState(false);
const [resourceGroupModal, setResourceGroupModal] = useState(false);
const [policyModal, setPolicyModal] = useState(false);
const openAddRoutingPeerModal = (
@@ -76,6 +82,15 @@ export const NetworkProvider = ({ children, network }: Props) => {
setResourceModal(true);
};
const openResourceGroupModal = (
network: Network,
resource?: NetworkResource,
) => {
setCurrentNetwork(network);
resource && setCurrentResource(resource);
setResourceGroupModal(true);
};
const openPolicyModal = (network?: Network, resource?: NetworkResource) => {
setPolicyDefaultSettings({
destinationGroups: resource?.groups,
@@ -217,6 +232,7 @@ export const NetworkProvider = ({ children, network }: Props) => {
openEditNetworkModal,
openCreateNetworkModal,
openResourceModal,
openResourceGroupModal,
openPolicyModal,
deleteNetwork,
deleteResource,
@@ -232,7 +248,7 @@ export const NetworkProvider = ({ children, network }: Props) => {
network={currentNetwork}
onCreated={async (network) => {
mutate("/networks");
await askForRoutingPeer(network);
await askForResource(network);
}}
onUpdated={() => {
mutate("/networks");
@@ -250,13 +266,15 @@ export const NetworkProvider = ({ children, network }: Props) => {
initialDestinationGroups={policyDefaultSettings?.destinationGroups}
initialName={policyDefaultSettings?.name}
initialDescription={policyDefaultSettings?.description}
onSuccess={(p) => {
onSuccess={async (p) => {
setPolicyModal(false);
setPolicyDefaultSettings(undefined);
mutate("/networks");
if (network) {
mutate(`/networks/${network.id}/resources`);
mutate(`/networks/${network.id}`);
} else {
currentNetwork && (await askForRoutingPeer(currentNetwork));
}
}}
/>
@@ -275,8 +293,6 @@ export const NetworkProvider = ({ children, network }: Props) => {
if (network) {
mutate(`/networks/${currentNetwork.id}/routers`);
mutate(`/networks/${network.id}`);
} else {
await askForResource(currentNetwork);
}
}}
onUpdated={async () => {
@@ -294,6 +310,26 @@ export const NetworkProvider = ({ children, network }: Props) => {
setRoutingPeerModal(state);
}}
/>
<ResourceGroupModal
network={currentNetwork}
resource={currentResource}
open={resourceGroupModal}
onOpenChange={(state) => {
setCurrentResource(undefined);
setResourceGroupModal(state);
}}
onUpdated={() => {
setResourceGroupModal(false);
setCurrentResource(undefined);
mutate("/groups");
if (network) {
mutate(`/networks/${network.id}/resources`);
mutate(`/networks/${network.id}`);
}
}}
/>
<NetworkResourceModal
network={currentNetwork}
resource={currentResource}

View File

@@ -60,10 +60,7 @@ export const NetworkInformationSquare = ({
{name}
</p>
<DescriptionWithTooltip
className={cn(
"text-left",
size == "lg" && "text-md leading-none mt-0.5",
)}
className={cn("text-left", size == "lg" && "text-md mt-0.5")}
maxChars={24}
text={description}
/>

View File

@@ -1,27 +1,26 @@
import SidebarItem from "@components/SidebarItem";
import { NewBadge } from "@components/ui/NewBadge";
import * as React from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
export const NetworkNavigation = () => {
return (
<SidebarItem
icon={<NetworkRoutesIcon />}
label="Networks"
collapsible
exactPathMatch={false}
>
<SidebarItem label="Networks" isChild href={"/networks"} />
<>
<SidebarItem
icon={<NetworkRoutesIcon />}
label={
<div className={"flex items-center"}>
Network Routes
<NetworkRoutesDeprecationInfo />
<div className={"flex items-center gap-2"}>
Networks
<NewBadge />
</div>
}
isChild
href={"/network-routes"}
href={"/networks"}
/>
</SidebarItem>
<SidebarItem
icon={<NetworkRoutesIcon />}
href={"/network-routes"}
label={"Network Routes"}
/>
</>
);
};

View File

@@ -1,6 +1,7 @@
"use client";
import Button from "@components/Button";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
@@ -17,7 +18,12 @@ import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import { useApiCall } from "@utils/api";
import { ExternalLinkIcon, PlusCircle, WorkflowIcon } from "lucide-react";
import {
ExternalLinkIcon,
PlusCircle,
Power,
WorkflowIcon,
} from "lucide-react";
import React, { useMemo, useState } from "react";
import { Network, NetworkResource } from "@/interfaces/Network";
import useGroupHelper from "@/modules/groups/useGroupHelper";
@@ -79,6 +85,9 @@ export function ResourceModalContent({
const [groups, setGroups, { save: saveGroups }] = useGroupHelper({
initial: resource?.groups || [],
});
const [enabled, setEnabled] = useState<boolean>(
resource ? resource.enabled : true,
);
const createResource = async () => {
const savedGroups = await saveGroups();
@@ -91,6 +100,7 @@ export function ResourceModalContent({
description,
address,
groups: savedGroups.map((g) => g.id),
enabled,
}).then((r) => {
onCreated?.(r);
}),
@@ -108,6 +118,7 @@ export function ResourceModalContent({
description,
address,
groups: savedGroups.map((g) => g.id),
enabled,
}).then((r) => {
onUpdated?.(r);
}),
@@ -151,17 +162,34 @@ export function ResourceModalContent({
<div>
<Label>Assigned Groups</Label>
<HelpText>
Control access to this resource by assigning it to groups
Add this resource to groups and use them as destinations when
creating policies
</HelpText>
<PeerGroupSelector onChange={setGroups} values={groups} />
</div>
<div className={"mt-3"}>
<FancyToggleSwitch
value={enabled}
onChange={setEnabled}
label={
<>
<Power size={15} />
Enable Resource
</>
}
helpText={"Use this switch to enable or disable the resource."}
/>
</div>
</div>
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink href={"https://docs.netbird.io/how-to/networks#resources"} target={"_blank"}>
<InlineLink
href={"https://docs.netbird.io/how-to/networks#resources"}
target={"_blank"}
>
Resources
<ExternalLinkIcon size={12} />
</InlineLink>

View File

@@ -1,5 +1,11 @@
import Button from "@components/Button";
import { SquarePenIcon, Trash2 } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@components/DropdownMenu";
import { MoreVertical, SquarePenIcon, Trash2 } from "lucide-react";
import * as React from "react";
import { NetworkResource } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
@@ -11,29 +17,45 @@ export const ResourceActionCell = ({ resource }: Props) => {
const { deleteResource, network, openResourceModal } = useNetworksContext();
return (
<div className={"flex justify-end pr-4"}>
<Button
variant={"default-outline"}
size={"sm"}
onClick={() => {
if (!network) return;
openResourceModal(network, resource);
}}
>
<SquarePenIcon size={16} />
Edit
</Button>
<Button
variant={"danger-outline"}
size={"sm"}
onClick={() => {
if (!network) return;
deleteResource(network, resource);
}}
>
<Trash2 size={16} />
Remove
</Button>
<div className={"flex justify-end"}>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
asChild={true}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<Button variant={"secondary"} className={"!px-3"}>
<MoreVertical size={16} className={"shrink-0"} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-auto" align="end">
<DropdownMenuItem
onClick={() => {
if (!network) return;
openResourceModal(network, resource);
}}
>
<div className={"flex gap-3 items-center"}>
<SquarePenIcon size={14} className={"shrink-0"} />
Edit
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (!network) return;
deleteResource(network, resource);
}}
variant={"danger"}
>
<div className={"flex gap-3 items-center"}>
<Trash2 size={14} className={"shrink-0"} />
Remove
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

View File

@@ -0,0 +1,58 @@
import { notify } from "@components/Notification";
import { ToggleSwitch } from "@components/ToggleSwitch";
import { useApiCall } from "@utils/api";
import * as React from "react";
import { useMemo } from "react";
import { useSWRConfig } from "swr";
import { Group } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
type Props = {
resource: NetworkResource;
};
export const ResourceEnabledCell = ({ resource }: Props) => {
const { mutate } = useSWRConfig();
const { network } = useNetworksContext();
const update = useApiCall<NetworkResource>(
`/networks/${network?.id}/resources/${resource?.id}`,
).put;
const toggle = async (enabled: boolean) => {
notify({
title: `Update Resource`,
description: `'${resource?.name}' is now ${
enabled ? "enabled" : "disabled"
}`,
loadingMessage: "Updating resource...",
duration: 1200,
promise: update({
...resource,
groups: resource.groups
?.map((g) => {
let group = g as Group;
return group.id;
})
.filter((g) => g !== undefined),
enabled,
}).then(() => {
mutate(`/networks/${network?.id}/resources`);
}),
});
};
const isChecked = useMemo(() => {
return resource.enabled;
}, [resource]);
return (
<div className={"flex"}>
<ToggleSwitch
checked={isChecked}
size={"small"}
onClick={() => toggle(!isChecked)}
/>
</div>
);
};

View File

@@ -2,14 +2,23 @@ import MultipleGroups from "@components/ui/MultipleGroups";
import * as React from "react";
import { Group } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
type Props = {
resource?: NetworkResource;
};
export const ResourceGroupCell = ({ resource }: Props) => {
const { network, openResourceGroupModal } = useNetworksContext();
return (
<div className={"flex"}>
<button
className={"flex cursor-pointer"}
onClick={() => {
if (!network) return;
openResourceGroupModal(network, resource);
}}
>
<MultipleGroups groups={resource?.groups as Group[]} />
</div>
</button>
);
};

View File

@@ -0,0 +1,121 @@
import Button from "@components/Button";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
} from "@components/modal/Modal";
import ModalHeader from "@components/modal/ModalHeader";
import { notify } from "@components/Notification";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import { useApiCall } from "@utils/api";
import { FolderGit2 } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
import { Network, NetworkResource } from "@/interfaces/Network";
import useGroupHelper from "@/modules/groups/useGroupHelper";
type ResourceGroupModalProps = {
resource?: NetworkResource;
network?: Network;
open: boolean;
onOpenChange: (open: boolean) => void;
onUpdated?: (r: NetworkResource) => void;
};
export const ResourceGroupModal = ({
resource,
network,
open,
onOpenChange,
onUpdated,
}: ResourceGroupModalProps) => {
return (
<Modal open={open} onOpenChange={onOpenChange}>
{network && resource && (
<ResourceGroupModalContent
network={network}
resource={resource}
onUpdated={onUpdated}
key={open ? "1" : "0"}
/>
)}
</Modal>
);
};
type ModalProps = {
onUpdated?: (r: NetworkResource) => void;
network?: Network;
resource?: NetworkResource;
};
const ResourceGroupModalContent = ({
resource,
network,
onUpdated,
}: ModalProps) => {
const update = useApiCall<NetworkResource>(
`/networks/${network?.id}/resources/${resource?.id}`,
).put;
const [groups, setGroups, { save: saveGroups }] = useGroupHelper({
initial: resource?.groups || [],
});
const updateResource = async () => {
const savedGroups = await saveGroups();
notify({
title: "Update Resource",
description: `'${resource?.name}' groups updated`,
loadingMessage: "Updating resource groups...",
promise: update({
...resource,
groups: savedGroups.map((g) => g.id),
}).then((r) => {
onUpdated?.(r);
}),
});
};
const canSave = useMemo(() => {
return groups.length > 0;
}, [groups]);
return (
<ModalContent maxWidthClass={"max-w-xl"}>
<ModalHeader
icon={<FolderGit2 size={18} />}
title={"Assigned Groups"}
description={
"Add this resource to groups and use them as destinations when creating policies"
}
color={"blue"}
/>
<Separator />
<div className={"px-8 py-6 flex flex-col gap-8"}>
<div>
<PeerGroupSelector onChange={setGroups} values={groups} />
</div>
</div>
<ModalFooter className={"items-center"}>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
<Button
variant={"primary"}
onClick={updateResource}
disabled={!canSave}
>
Save Groups
</Button>
</div>
</ModalFooter>
</ModalContent>
);
};

View File

@@ -1,32 +1,52 @@
import DescriptionWithTooltip from "@components/ui/DescriptionWithTooltip";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { cn } from "@utils/helpers";
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
import React from "react";
import { NetworkResource } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
type Props = {
resource: NetworkResource;
};
export default function ResourceNameCell({ resource }: Readonly<Props>) {
const { network, openResourceModal } = useNetworksContext();
return (
<div className={"flex gap-4 items-center"}>
<button
className={"flex gap-4 items-center group"}
onClick={() => {
if (!network) return;
openResourceModal(network, resource);
}}
>
<div
className={cn(
"flex items-center justify-center rounded-md h-9 w-9 shrink-0 bg-nb-gray-900 transition-all",
"group-hover:bg-nb-gray-800",
)}
>
{resource.type === "host" && <WorkflowIcon size={15} />}
{resource.type === "domain" && <GlobeIcon size={15} />}
{resource.type === "subnet" && <NetworkIcon size={15} />}
</div>
<div className="flex flex-col gap-0 dark:text-neutral-300 text-neutral-500 font-light truncate">
<span className={"font-normal truncate"}>{resource.name}</span>
<div
className={cn(
"flex flex-col gap-0 text-neutral-300 font-light truncate",
"group-hover:text-neutral-100",
)}
>
<TextWithTooltip
text={resource.name}
maxChars={25}
className={"font-normal"}
/>
<DescriptionWithTooltip
className={cn("font-normal mt-0.5 ")}
text={resource.description}
/>
</div>
</div>
</button>
);
}

View File

@@ -22,13 +22,10 @@ export const ResourcePolicyCell = ({ resource }: Props) => {
const resourceGroups = resource?.groups as Group[];
return policies?.filter((policy) => {
if (!policy.enabled) return false;
const sourcePolicyGroups = policy.rules
?.map((rule) => rule?.sources)
.flat() as Group[];
const destinationPolicyGroups = policy.rules
?.map((rule) => rule?.destinations)
.flat() as Group[];
const policyGroups = [...sourcePolicyGroups, ...destinationPolicyGroups];
const policyGroups = [...destinationPolicyGroups];
return resourceGroups.some((resourceGroup) =>
policyGroups.some((policyGroup) => policyGroup.id === resourceGroup.id),
);
@@ -89,7 +86,7 @@ export const ResourcePolicyCell = ({ resource }: Props) => {
<Button
size={"xs"}
variant={"secondary"}
className={"min-w-[110px]"}
className={"min-w-[100px]"}
onClick={() => openPolicyModal(network, resource)}
>
<PlusCircle size={12} />

View File

@@ -1,15 +1,12 @@
import Button from "@components/Button";
import Paragraph from "@components/Paragraph";
import SkeletonTable, {
SkeletonTableHeader,
} from "@components/skeletons/SkeletonTable";
import { usePortalElement } from "@hooks/usePortalElement";
import { IconCirclePlus } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import * as React from "react";
import { Suspense } from "react";
import { Network, NetworkResource } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
import ResourcesTable from "@/modules/networks/resources/ResourcesTable";
type ResourcesSectionProps = {
@@ -23,27 +20,14 @@ export const ResourcesSection = ({ network }: ResourcesSectionProps) => {
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
const { openResourceModal } = useNetworksContext();
return (
<div className={"py-7 px-8"}>
<div className={"max-w-6xl"}>
<div className={"flex justify-between items-center"}>
<div className={"flex justify-between items-center mb-6"}>
<div>
<h2 ref={headingRef}>Resources</h2>
<Paragraph>Add and manage resources for this network.</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div>
<Button
variant={"primary"}
onClick={() => openResourceModal(network)}
>
<IconCirclePlus size={16} />
Add Resource
</Button>
</div>
</div>
</div>
<Suspense

View File

@@ -1,14 +1,20 @@
import Button from "@components/Button";
import Card from "@components/Card";
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import NoResults from "@components/ui/NoResults";
import { IconCirclePlus } from "@tabler/icons-react";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import { Layers3Icon } from "lucide-react";
import * as React from "react";
import { useState } from "react";
import { Group } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
import { ResourceActionCell } from "@/modules/networks/resources/ResourceActionCell";
import ResourceAddressCell from "@/modules/networks/resources/ResourceAddressCell";
import { ResourceEnabledCell } from "@/modules/networks/resources/ResourceEnabledCell";
import { ResourceGroupCell } from "@/modules/networks/resources/ResourceGroupCell";
import ResourceNameCell from "@/modules/networks/resources/ResourceNameCell";
import { ResourcePolicyCell } from "@/modules/networks/resources/ResourcePolicyCell";
@@ -22,7 +28,7 @@ type Props = {
const NetworkResourceColumns: ColumnDef<NetworkResource>[] = [
{
id: "id",
accessorKey: "id",
accessorKey: "name",
header: ({ column }) => {
return <DataTableHeader column={column}>Resource</DataTableHeader>;
},
@@ -40,9 +46,20 @@ const NetworkResourceColumns: ColumnDef<NetworkResource>[] = [
return <ResourceAddressCell resource={row.original} />;
},
},
{
id: "enabled",
accessorKey: "enabled",
header: ({ column }) => {
return <DataTableHeader column={column}>Active</DataTableHeader>;
},
cell: ({ row }) => <ResourceEnabledCell resource={row.original} />,
},
{
id: "groups",
accessorKey: "id",
accessorFn: (resource) => {
let groups = resource?.groups as Group[];
return groups.map((group) => group.name).join(", ");
},
header: ({ column }) => {
return <DataTableHeader column={column}>Groups</DataTableHeader>;
},
@@ -74,40 +91,56 @@ export default function ResourcesTable({
resources,
isLoading,
headingTarget,
}: Props) {
}: Readonly<Props>) {
const [sorting, setSorting] = useState<SortingState>([]);
const { openResourceModal, network } = useNetworksContext();
return (
<>
<DataTable
wrapperComponent={Card}
wrapperProps={{ className: "mt-6 w-full" }}
headingTarget={headingTarget}
sorting={sorting}
setSorting={setSorting}
minimal={true}
showSearchAndFilters={false}
inset={false}
tableClassName={"mt-0"}
text={"Peers"}
columns={NetworkResourceColumns}
keepStateInLocalStorage={false}
data={resources}
searchPlaceholder={"Search by name, IP, owner or group..."}
isLoading={isLoading}
getStartedCard={
<NoResults
className={"py-4"}
title={"This network has no resources"}
description={
"Add resources to this network to control what peers can access. Resources can be anything from a single IP, a subnet, or a domain."
}
icon={<Layers3Icon size={20} />}
/>
}
columnVisibility={{}}
paginationPaddingClassName={"px-0 pt-8"}
/>
</>
<DataTable
wrapperComponent={Card}
wrapperProps={{ className: "mt-6 pb-2 w-full" }}
headingTarget={headingTarget}
sorting={sorting}
setSorting={setSorting}
minimal={true}
showSearchAndFilters={true}
inset={false}
tableClassName={"mt-0"}
text={"Resources"}
columns={NetworkResourceColumns}
keepStateInLocalStorage={false}
data={resources}
searchPlaceholder={"Search by name, address or group..."}
isLoading={isLoading}
getStartedCard={
<NoResults
className={"py-4"}
title={"This network has no resources"}
description={
"Add resources to this network to control what peers can access. Resources can be anything from a single IP, a subnet, or a domain."
}
icon={<Layers3Icon size={20} />}
/>
}
columnVisibility={{}}
paginationPaddingClassName={"px-0 pt-8"}
rightSide={() => (
<Button
variant={"primary"}
className={"ml-auto"}
onClick={() => network && openResourceModal(network)}
>
<IconCirclePlus size={16} />
Add Resource
</Button>
)}
>
{(table) => (
<DataTableRowsPerPage
table={table}
disabled={!resources || resources?.length == 0}
/>
)}
</DataTable>
);
}

View File

@@ -24,18 +24,25 @@ import { cn } from "@utils/helpers";
import { uniqBy } from "lodash";
import {
ArrowDownWideNarrow,
DownloadIcon,
ExternalLinkIcon,
FolderGit2,
Loader2,
MonitorSmartphoneIcon,
PlusCircle,
Power,
Settings2,
Share2Icon,
VenetianMask,
} from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import { useDialog } from "@/contexts/DialogProvider";
import { Network, NetworkRouter } from "@/interfaces/Network";
import { Peer } from "@/interfaces/Peer";
import { SetupKey } from "@/interfaces/SetupKey";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
type Props = {
network: Network;
@@ -112,6 +119,9 @@ function RoutingPeerModalContent({
const [masquerade, setMasquerade] = useState<boolean>(
router ? router.masquerade : true,
);
const [enabled, setEnabled] = useState<boolean>(
router ? router.enabled : true,
);
const [metric, setMetric] = useState(
router?.metric ? router.metric.toString() : "9999",
);
@@ -137,6 +147,7 @@ function RoutingPeerModalContent({
? createdGroups.map((g) => g.id)
: undefined,
metric: parseInt(metric),
enabled,
masquerade,
}).then((r) => {
onCreated?.(r);
@@ -165,6 +176,7 @@ function RoutingPeerModalContent({
? createdGroups.map((g) => g.id)
: undefined,
metric: parseInt(metric),
enabled,
masquerade,
}).then((r) => {
onUpdated?.(r);
@@ -172,6 +184,10 @@ function RoutingPeerModalContent({
});
};
const [setupKeyModal, setSetupKeyModal] = useState(false);
const canContinue = routingPeer !== undefined || routingPeerGroups.length > 0;
return (
<ModalContent maxWidthClass={"max-w-xl"}>
<ModalHeader
@@ -190,7 +206,7 @@ function RoutingPeerModalContent({
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Routers
Routing Peers
</TabsTrigger>
<TabsTrigger value={"settings"} className={"ml-auto"}>
@@ -203,48 +219,86 @@ function RoutingPeerModalContent({
Advanced Settings
</TabsTrigger>
</TabsList>
<TabsContent value={"router"} className={"pb-8"}>
<div className={"flex flex-col gap-4 px-8 "}>
<SegmentedTabs value={type} onChange={setType}>
<SegmentedTabs.List>
<SegmentedTabs.Trigger value={"peer"}>
<MonitorSmartphoneIcon size={16} />
Routing Peers
</SegmentedTabs.Trigger>
<TabsContent value={"router"} className={"pb-6"}>
<div className={"flex flex-col gap-4 px-8"}>
<div className={"relative "}>
<SegmentedTabs
value={type}
onChange={(state) => {
setType(state);
setRoutingPeer(undefined);
setRoutingPeerGroups([]);
}}
>
<SegmentedTabs.List>
<SegmentedTabs.Trigger value={"peer"}>
<MonitorSmartphoneIcon size={16} />
Routing Peers
</SegmentedTabs.Trigger>
<SegmentedTabs.Trigger value={"group"}>
<FolderGit2 size={16} />
Peer Group
</SegmentedTabs.Trigger>
</SegmentedTabs.List>
<SegmentedTabs.Content value={"peer"}>
<div>
<HelpText>
Assign a single or multiple peers as a routing peers for the
network.
</HelpText>
<PeerSelector onChange={setRoutingPeer} value={routingPeer} />
</div>
</SegmentedTabs.Content>
<SegmentedTabs.Content value={"group"}>
<div>
<HelpText>
Assign a peer group with Linux machines to be used as
routing peers.
</HelpText>
<PeerGroupSelector
max={1}
onChange={setRoutingPeerGroups}
values={routingPeerGroups}
/>
</div>
</SegmentedTabs.Content>
</SegmentedTabs>
<SegmentedTabs.Trigger value={"group"}>
<FolderGit2 size={16} />
Peer Group
</SegmentedTabs.Trigger>
</SegmentedTabs.List>
<SegmentedTabs.Content value={"peer"}>
<div>
<HelpText>
Assign a single or multiple Linux peers as routing peers
for the network.
</HelpText>
<PeerSelector
onChange={setRoutingPeer}
value={routingPeer}
/>
</div>
</SegmentedTabs.Content>
<SegmentedTabs.Content value={"group"}>
<div>
<HelpText>
Assign a peer group with Linux machines to be used as
routing peers.
</HelpText>
<PeerGroupSelector
max={1}
onChange={setRoutingPeerGroups}
values={routingPeerGroups}
/>
</div>
</SegmentedTabs.Content>
</SegmentedTabs>
</div>
<div className={cn("flex justify-between items-center mt-3")}>
<div>
<Label>{"Don't have a routing peer?"}</Label>
<HelpText className={""}>
You can install NetBird with a setup key on one or more Linux
machines to act as routing peers.
</HelpText>
</div>
<InstallNetBirdWithSetupKeyButton
name={`Routing Peer (${network.name})`}
/>
</div>
</div>
</TabsContent>
<TabsContent value={"settings"} className={"pb-4"}>
<div className={"px-8 flex flex-col gap-6"}>
<FancyToggleSwitch
value={enabled}
onChange={setEnabled}
label={
<>
<Power size={15} />
Enable Routing Peer
</>
}
helpText={
"Use this switch to enable or disable the routing peer."
}
/>
<FancyToggleSwitch
value={masquerade}
onChange={setMasquerade}
@@ -302,34 +356,128 @@ function RoutingPeerModalContent({
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
{tab == "router" && (
<Button variant={"primary"} onClick={() => setTab("settings")}>
Continue
</Button>
<>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
<Button
variant={"primary"}
onClick={() => setTab("settings")}
disabled={!canContinue}
>
Continue
</Button>
</>
)}
{tab == "settings" && (
<Button
variant={"primary"}
disabled={
routingPeer == undefined && routingPeerGroups.length <= 0
}
onClick={router ? updateRouter : addRouter}
>
{router ? (
<>Save Changes</>
) : (
<>
<PlusCircle size={16} />
Add Routing Peer
</>
)}
</Button>
<>
<Button variant={"secondary"} onClick={() => setTab("router")}>
Back
</Button>
<Button
variant={"primary"}
disabled={
routingPeer == undefined && routingPeerGroups.length <= 0
}
onClick={router ? updateRouter : addRouter}
>
{router ? (
<>Save Changes</>
) : (
<>
<PlusCircle size={16} />
Add Routing Peer
</>
)}
</Button>
</>
)}
</div>
</ModalFooter>
</ModalContent>
);
}
type InstallNetBirdWithSetupKeyButtonProps = {
name?: string;
};
const InstallNetBirdWithSetupKeyButton = ({
name,
}: InstallNetBirdWithSetupKeyButtonProps) => {
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
const { mutate } = useSWRConfig();
const { confirm } = useDialog();
const [installModal, setInstallModal] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>();
const [isLoading, setIsLoading] = useState(false);
const createSetupKey = async () => {
const choice = await confirm({
title: `Create a Setup Key?`,
description:
"If you continue, a one-off setup key will be automatically created and you will be able to install NetBird on a Linux machine.",
confirmText: "Continue",
cancelText: "Cancel",
type: "default",
});
if (!choice) return;
const loadingTimeout = setTimeout(() => setIsLoading(true), 1000);
await setupKeyRequest
.post({
name,
type: "one-off",
expires_in: 24 * 60 * 60, // 1 day expiration
revoked: false,
auto_groups: [],
usage_limit: 1,
ephemeral: false,
})
.then((setupKey) => {
setInstallModal(true);
setSetupKey(setupKey);
mutate("/setup-keys");
})
.finally(() => {
setIsLoading(false);
clearTimeout(loadingTimeout);
});
};
return (
<>
<Button
variant={"secondary"}
size={"xs"}
className={"ml-8"}
onClick={createSetupKey}
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={14} className={"animate-spin delay-1000"} />
) : (
<DownloadIcon size={14} />
)}
Install NetBird
</Button>
{setupKey && (
<Modal
open={installModal}
onOpenChange={setInstallModal}
key={setupKey.key}
>
<SetupModal
showClose={true}
setupKey={setupKey.key}
showOnlyRoutingPeerOS={true}
/>
</Modal>
)}
</>
);
};

View File

@@ -44,13 +44,11 @@ export const NetworkRoutingPeerName = ({ router }: Props) => {
if (routingPeerGroup) {
return (
<>
<div className={"flex items-center gap-2 max-w-[295px] min-w-[295px]"}>
<GroupBadge group={routingPeerGroup} />
<ArrowRightIcon size={14} className={"shrink-0"} />
<PeerBadge> {routingPeerGroup.peers_count} Peer(s)</PeerBadge>
</div>
</>
<div className={"flex items-center gap-2 max-w-[295px] min-w-[295px]"}>
<GroupBadge group={routingPeerGroup} />
<ArrowRightIcon size={14} className={"shrink-0"} />
<PeerBadge> {routingPeerGroup.peers_count} Peer(s)</PeerBadge>
</div>
);
}
};

View File

@@ -1,14 +1,19 @@
import Button from "@components/Button";
import Card from "@components/Card";
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import NoResults from "@components/ui/NoResults";
import { IconCirclePlus } from "@tabler/icons-react";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import * as React from "react";
import { useState } from "react";
import PeerIcon from "@/assets/icons/PeerIcon";
import { NetworkRouter } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
import { NetworkRoutingPeerName } from "@/modules/networks/routing-peers/NetworkRoutingPeerName";
import { RoutingPeersActionCell } from "@/modules/networks/routing-peers/RoutingPeersActionCell";
import { RoutingPeersEnabledCell } from "@/modules/networks/routing-peers/RoutingPeersEnabledCell";
import { RoutingPeersMasqueradeCell } from "@/modules/networks/routing-peers/RoutingPeersMasqueradeCell";
import RouteMetricCell from "@/modules/routes/RouteMetricCell";
@@ -28,13 +33,23 @@ const NetworkRouterColumns: ColumnDef<NetworkRouter>[] = [
sortingFn: "text",
cell: ({ row }) => <NetworkRoutingPeerName router={row.original} />,
},
{
id: "enabled",
accessorKey: "enabled",
header: ({ column }) => {
return <DataTableHeader column={column}>Active</DataTableHeader>;
},
cell: ({ row }) => <RoutingPeersEnabledCell router={row.original} />,
},
{
id: "metric",
accessorKey: "metric",
header: ({ column }) => {
return <DataTableHeader column={column}>Metric</DataTableHeader>;
},
cell: ({ row }) => <RouteMetricCell metric={row.original.metric} />,
cell: ({ row }) => (
<RouteMetricCell metric={row.original.metric} useHoverStyle={false} />
),
},
{
id: "masquerade",
@@ -58,7 +73,9 @@ export default function NetworkRoutingPeersTable({
routers,
isLoading,
headingTarget,
}: Props) {
}: Readonly<Props>) {
const { openAddRoutingPeerModal, network } = useNetworksContext();
const [sorting, setSorting] = useState<SortingState>([
{
id: "metric",
@@ -69,7 +86,7 @@ export default function NetworkRoutingPeersTable({
return (
<DataTable
wrapperComponent={Card}
wrapperProps={{ className: "mt-6 w-full" }}
wrapperProps={{ className: "mt-6 pb-2 w-full" }}
headingTarget={headingTarget}
sorting={sorting}
setSorting={setSorting}
@@ -77,11 +94,11 @@ export default function NetworkRoutingPeersTable({
showSearchAndFilters={false}
inset={false}
tableClassName={"mt-0"}
text={"Peers"}
text={"Routing Peers"}
columns={NetworkRouterColumns}
keepStateInLocalStorage={false}
data={routers}
searchPlaceholder={"Search by name, IP, owner or group..."}
searchPlaceholder={"Search by name..."}
isLoading={isLoading}
getStartedCard={
<NoResults
@@ -95,6 +112,23 @@ export default function NetworkRoutingPeersTable({
}
columnVisibility={{}}
paginationPaddingClassName={"px-0 pt-8"}
/>
rightSide={() => (
<Button
variant={"primary"}
className={"ml-auto"}
onClick={() => network && openAddRoutingPeerModal(network)}
>
<IconCirclePlus size={16} />
Add Routing Peer
</Button>
)}
>
{(table) => (
<DataTableRowsPerPage
table={table}
disabled={!routers || routers?.length == 0}
/>
)}
</DataTable>
);
}

View File

@@ -1,5 +1,11 @@
import Button from "@components/Button";
import { SquarePenIcon, Trash2 } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@components/DropdownMenu";
import { MoreVertical, SquarePenIcon, Trash2 } from "lucide-react";
import * as React from "react";
import { NetworkRouter } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
@@ -12,29 +18,45 @@ export const RoutingPeersActionCell = ({ router }: Props) => {
useNetworksContext();
return (
<div className={"flex justify-end pr-4"}>
<Button
variant={"default-outline"}
size={"sm"}
onClick={() => {
if (!network) return;
openAddRoutingPeerModal(network, router);
}}
>
<SquarePenIcon size={16} />
Edit
</Button>
<Button
variant={"danger-outline"}
size={"sm"}
onClick={() => {
if (!network) return;
deleteRouter(network, router);
}}
>
<Trash2 size={16} />
Remove
</Button>
<div className={"flex justify-end"}>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
asChild={true}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<Button variant={"secondary"} className={"!px-3"}>
<MoreVertical size={16} className={"shrink-0"} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-auto" align="end">
<DropdownMenuItem
onClick={() => {
if (!network) return;
openAddRoutingPeerModal(network, router);
}}
>
<div className={"flex gap-3 items-center"}>
<SquarePenIcon size={14} className={"shrink-0"} />
Edit
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (!network) return;
deleteRouter(network, router);
}}
variant={"danger"}
>
<div className={"flex gap-3 items-center"}>
<Trash2 size={14} className={"shrink-0"} />
Remove
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

View File

@@ -0,0 +1,48 @@
import { notify } from "@components/Notification";
import { ToggleSwitch } from "@components/ToggleSwitch";
import { useApiCall } from "@utils/api";
import * as React from "react";
import { useMemo } from "react";
import { useSWRConfig } from "swr";
import { NetworkRouter } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
type Props = {
router: NetworkRouter;
};
export const RoutingPeersEnabledCell = ({ router }: Props) => {
const { mutate } = useSWRConfig();
const { network } = useNetworksContext();
const update = useApiCall<NetworkRouter>(
`/networks/${network?.id}/routers/${router?.id}`,
).put;
const toggle = async (enabled: boolean) => {
notify({
title: "Network Routing Peer",
description: `Routing peer is now ${enabled ? "enabled" : "disabled"}`,
loadingMessage: "Updating routing peer...",
promise: update({
...router,
enabled,
}).then(() => {
mutate(`/networks/${network?.id}/routers`);
}),
});
};
const isChecked = useMemo(() => {
return router.enabled;
}, [router]);
return (
<div className={"flex"}>
<ToggleSwitch
checked={isChecked}
size={"small"}
onClick={() => toggle(!isChecked)}
/>
</div>
);
};

View File

@@ -13,7 +13,6 @@ import { usePathname } from "next/navigation";
import React from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { useDialog } from "@/contexts/DialogProvider";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { Network } from "@/interfaces/Network";
import {
@@ -38,14 +37,6 @@ export const NetworkTableColumns: ColumnDef<Network>[] = [
{
accessorKey: "description",
},
{
accessorKey: "routers",
accessorFn: (network) => network?.routers?.length,
header: ({ column }) => {
return <DataTableHeader column={column}>Routing Peers</DataTableHeader>;
},
cell: ({ row }) => <NetworkRoutingPeerCell network={row.original} />,
},
{
accessorKey: "resources",
accessorFn: (network) => network?.resources?.length,
@@ -62,6 +53,14 @@ export const NetworkTableColumns: ColumnDef<Network>[] = [
},
cell: ({ row }) => <NetworkPolicyCell network={row.original} />,
},
{
accessorKey: "routers",
accessorFn: (network) => network?.routers?.length,
header: ({ column }) => {
return <DataTableHeader column={column}>Routing Peers</DataTableHeader>;
},
cell: ({ row }) => <NetworkRoutingPeerCell network={row.original} />,
},
{
accessorKey: "id",
header: "",
@@ -94,20 +93,6 @@ export default function NetworksTable({
],
);
const { confirm } = useDialog();
const showConfirm = async () => {
const choice = await confirm({
title: `Do you want to add a resource to 'Office Network' now?`,
description:
"Peers will be able to access your network resources once you add them.",
confirmText: "Add Resource",
cancelText: "Later",
type: "default",
});
if (!choice) return;
};
return (
<NetworkProvider>
<DataTable
@@ -146,9 +131,7 @@ export default function NetworksTable({
<>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/networks"
}
href={"https://docs.netbird.io/how-to/networks"}
target={"_blank"}
>
Networks

View File

@@ -63,7 +63,8 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
enableHiding: false,
},
{
accessorKey: "name",
id: "name",
accessorFn: (peer) => `${peer?.name}${peer?.dns_label}`,
header: ({ column }) => {
return <DataTableHeader column={column}>Name</DataTableHeader>;
},
@@ -94,6 +95,7 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
accessorFn: (peer) => (peer.user ? peer.user?.email : "Unknown"),
},
{
id: "dns_label",
accessorKey: "dns_label",
header: ({ column }) => {
return <DataTableHeader column={column}>Address</DataTableHeader>;
@@ -193,6 +195,10 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
id: "connected",
desc: true,
},
{
id: "name",
desc: false,
},
{
id: "last_seen",
desc: true,

View File

@@ -3,11 +3,15 @@ import { ArrowUpDown, InfoIcon } from "lucide-react";
type Props = {
metric?: number;
useHoverStyle?: boolean;
};
export default function RouteMetricCell({ metric }: Props) {
export default function RouteMetricCell({
metric,
useHoverStyle = true,
}: Readonly<Props>) {
return (
<FullTooltip
hoverButton={true}
hoverButton={useHoverStyle}
isAction={true}
content={
<div className={"text-xs max-w-xs flex gap-2 items-center"}>

View File

@@ -4,7 +4,7 @@ import DataTableHeader from "@components/table/DataTableHeader";
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import NoResults from "@components/ui/NoResults";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import { FolderGit2Icon } from "lucide-react";
import { FolderGit2Icon, Layers3Icon } from "lucide-react";
import { usePathname } from "next/navigation";
import React from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
@@ -138,6 +138,29 @@ export const GroupsTableColumns: ColumnDef<GroupUsage>[] = [
/>
),
},
{
accessorKey: "resources_count",
header: ({ column }) => {
return (
<DataTableHeader
column={column}
tooltip={
<div className={"text-sm normal-case"}>Network Resources</div>
}
>
<Layers3Icon size={12} />
</DataTableHeader>
);
},
cell: ({ row }) => (
<GroupsCountCell
icon={<Layers3Icon size={10} />}
groupName={row.original.name}
text={"Network Resource(s)"}
count={row.original.resources_count}
/>
),
},
{
accessorKey: "users_count",
header: ({ column }) => {
@@ -172,7 +195,8 @@ export const GroupsTableColumns: ColumnDef<GroupUsage>[] = [
row.policies_count > 0 ||
row.routes_count > 0 ||
row.setup_keys_count > 0 ||
row.users_count > 0
row.users_count > 0 ||
row.resources_count > 0
);
},
},
@@ -189,7 +213,7 @@ type Props = {
headingTarget?: HTMLHeadingElement | null;
};
export default function GroupsTable({ headingTarget }: Props) {
export default function GroupsTable({ headingTarget }: Readonly<Props>) {
const groups = useGroupsUsage();
const path = usePathname();

View File

@@ -16,6 +16,7 @@ export interface GroupUsage {
routes_count: number;
setup_keys_count: number;
users_count: number;
resources_count: number;
}
export default function useGroupsUsage() {
@@ -126,6 +127,7 @@ export default function useGroupsUsage() {
id: group.id,
name: group.name,
peers_count: group.peers_count,
resources_count: group.resources_count,
policies_count: policyCount,
nameservers_count: nameserverCount,
routes_count: routeCount,

View File

@@ -23,7 +23,7 @@ import { cn } from "@utils/helpers";
import { trim } from "lodash";
import {
AlarmClock,
CopyIcon,
DownloadIcon,
ExternalLinkIcon,
MonitorSmartphoneIcon,
PlusCircle,
@@ -32,25 +32,28 @@ import {
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import useCopyToClipboard from "@/hooks/useCopyToClipboard";
import { SetupKey } from "@/interfaces/SetupKey";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
type Props = {
children?: React.ReactNode;
open: boolean;
setOpen: (open: boolean) => void;
name?: string;
showOnlyRoutingPeerOS?: boolean;
};
const copyMessage = "Setup-Key was copied to your clipboard!";
export default function SetupKeyModal({
children,
open,
setOpen,
name,
showOnlyRoutingPeerOS,
}: Readonly<Props>) {
const [successModal, setSuccessModal] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>();
const [, copy] = useCopyToClipboard(setupKey?.key);
const [installModal, setInstallModal] = useState(false);
const handleSuccess = (setupKey: SetupKey) => {
setSetupKey(setupKey);
setSuccessModal(true);
@@ -60,8 +63,24 @@ export default function SetupKeyModal({
<>
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
{children && <ModalTrigger asChild>{children}</ModalTrigger>}
<SetupKeyModalContent onSuccess={handleSuccess} />
<SetupKeyModalContent onSuccess={handleSuccess} predefinedName={name} />
</Modal>
<Modal
open={installModal}
onOpenChange={(state) => {
setInstallModal(state);
setOpen(false);
}}
key={installModal ? 2 : 3}
>
<SetupModal
showClose={true}
setupKey={setupKey?.key}
showOnlyRoutingPeerOS={showOnlyRoutingPeerOS}
/>
</Modal>
<Modal
open={successModal}
onOpenChange={(open) => {
@@ -118,10 +137,10 @@ export default function SetupKeyModal({
variant={"primary"}
className={"w-full"}
data-cy={"setup-key-copy"}
onClick={() => copy(copyMessage)}
onClick={() => setInstallModal(true)}
>
<CopyIcon size={14} />
Copy to clipboard
<DownloadIcon size={14} />
Install NetBird
</Button>
</div>
</ModalFooter>
@@ -133,13 +152,17 @@ export default function SetupKeyModal({
type ModalProps = {
onSuccess?: (setupKey: SetupKey) => void;
predefinedName?: string;
};
export function SetupKeyModalContent({ onSuccess }: Readonly<ModalProps>) {
export function SetupKeyModalContent({
onSuccess,
predefinedName = "",
}: Readonly<ModalProps>) {
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
const { mutate } = useSWRConfig();
const [name, setName] = useState("");
const [name, setName] = useState(predefinedName);
const [reusable, setReusable] = useState(false);
const [usageLimit, setUsageLimit] = useState("");
const [expiresIn, setExpiresIn] = useState("7");

View File

@@ -9,8 +9,17 @@ import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { RoutingPeerSetupKeyInfo } from "@/modules/setup-netbird-modal/SetupModal";
export default function DockerTab() {
type Props = {
setupKey?: string;
showSetupKeyInfo?: boolean;
};
export default function DockerTab({
setupKey,
showSetupKeyInfo = false,
}: Readonly<Props>) {
return (
<TabsContent value={String(OperatingSystem.DOCKER)}>
<TabsContentPadding>
@@ -35,14 +44,20 @@ export default function DockerTab() {
</div>
</Steps.Step>
<Steps.Step step={2}>
<p>Run NetBird container</p>
<p>
Run NetBird container
{showSetupKeyInfo && <RoutingPeerSetupKeyInfo />}
</p>
<Code>
<Code.Line>docker run --rm -d \</Code.Line>
<Code.Line> --cap-add=NET_ADMIN \</Code.Line>
<Code.Line>
{" "}
-e NB_SETUP_KEY=
<span className={"text-netbird"}>SETUP_KEY</span> \
<span className={"text-netbird"}>
{setupKey ?? "SETUP_KEY"}
</span>{" "}
\
</Code.Line>
<Code.Line> -v netbird-client:/etc/netbird \</Code.Line>
{GRPC_API_ORIGIN && (

View File

@@ -13,8 +13,20 @@ import { getNetBirdUpCommand } from "@utils/netbird";
import { TerminalSquareIcon } from "lucide-react";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import {
RoutingPeerSetupKeyInfo,
SetupKeyParameter,
} from "@/modules/setup-netbird-modal/SetupModal";
export default function LinuxTab() {
type Props = {
setupKey?: string;
showSetupKeyInfo?: boolean;
};
export default function LinuxTab({
setupKey,
showSetupKeyInfo = false,
}: Readonly<Props>) {
return (
<TabsContent value={String(OperatingSystem.LINUX)}>
<TabsContentPadding>
@@ -27,9 +39,15 @@ export default function LinuxTab() {
<Code>curl -fsSL https://pkgs.netbird.io/install.sh | sh</Code>
</Steps.Step>
<Steps.Step step={2} line={false}>
<p>Run NetBird and log in the browser</p>
<p>
Run NetBird {!setupKey && "and log in the browser"}
{showSetupKeyInfo && <RoutingPeerSetupKeyInfo />}
</p>
<Code>
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
<Code.Line>
{getNetBirdUpCommand()}
<SetupKeyParameter setupKey={setupKey} />
</Code.Line>
</Code>
</Steps.Step>
</Steps>
@@ -78,9 +96,15 @@ export default function LinuxTab() {
</Code>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p>Run NetBird and log in the browser</p>
<p>
Run NetBird {!setupKey && "and log in the browser"}
{showSetupKeyInfo && <RoutingPeerSetupKeyInfo />}
</p>
<Code>
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
<Code.Line>
{getNetBirdUpCommand()}
<SetupKeyParameter setupKey={setupKey} />
</Code.Line>
</Code>
</Steps.Step>
</Steps>

View File

@@ -23,8 +23,12 @@ import {
import Link from "next/link";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { SetupKeyParameter } from "@/modules/setup-netbird-modal/SetupModal";
export default function MacOSTab() {
type Props = {
setupKey?: string;
};
export default function MacOSTab({ setupKey }: Readonly<Props>) {
return (
<TabsContent value={String(OperatingSystem.APPLE)}>
<TabsContentPadding>
@@ -98,15 +102,29 @@ export default function MacOSTab() {
</Steps.Step>
)}
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
<p>
{/* eslint-disable-next-line react/no-unescaped-entities */}
Click on "Connect" from the NetBird icon in your system tray
</p>
</Steps.Step>
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
<p>Sign up using your email address</p>
</Steps.Step>
{setupKey ? (
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2} line={false}>
<p>Open Terminal and run NetBird</p>
<Code>
<Code.Line>
{getNetBirdUpCommand()}
<SetupKeyParameter setupKey={setupKey} />
</Code.Line>
</Code>
</Steps.Step>
) : (
<>
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
<p>
{/* eslint-disable-next-line react/no-unescaped-entities */}
Click on "Connect" from the NetBird icon in your system tray
</p>
</Steps.Step>
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
<p>Sign up using your email address</p>
</Steps.Step>
</>
)}
</Steps>
</TabsContentPadding>
<Separator />
@@ -125,9 +143,12 @@ export default function MacOSTab() {
</Code>
</Steps.Step>
<Steps.Step step={2} line={false}>
<p>Run NetBird and log in the browser</p>
<p>Run NetBird {!setupKey && "and log in the browser"}</p>
<Code>
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
<Code.Line>
{getNetBirdUpCommand()}
<SetupKeyParameter setupKey={setupKey} />
</Code.Line>
</Code>
</Steps.Step>
</Steps>
@@ -179,9 +200,12 @@ export default function MacOSTab() {
</Code>
</Steps.Step>
<Steps.Step step={4} line={false}>
<p>Run NetBird and log in the browser</p>
<p>Run NetBird {!setupKey && "and log in the browser"}</p>
<Code>
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
<Code.Line>
{getNetBirdUpCommand()}
<SetupKeyParameter setupKey={setupKey} />
</Code.Line>
</Code>
</Steps.Step>
</Steps>

View File

@@ -5,6 +5,7 @@ import { ModalContent, ModalFooter } from "@components/modal/Modal";
import Paragraph from "@components/Paragraph";
import SmallParagraph from "@components/SmallParagraph";
import { Tabs, TabsList, TabsTrigger } from "@components/Tabs";
import { cn } from "@utils/helpers";
import { ExternalLinkIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import React from "react";
@@ -31,27 +32,44 @@ type OidcUserInfo = {
type Props = {
showClose?: boolean;
user?: OidcUserInfo;
setupKey?: string;
showOnlyRoutingPeerOS?: boolean;
};
export default function SetupModal({ showClose = true, user }: Props) {
export default function SetupModal({
showClose = true,
user,
setupKey,
showOnlyRoutingPeerOS = false,
}: Readonly<Props>) {
return (
<ModalContent showClose={showClose}>
<SetupModalContent user={user} />
<SetupModalContent
user={user}
setupKey={setupKey}
showOnlyRoutingPeerOS={showOnlyRoutingPeerOS}
/>
</ModalContent>
);
}
type SetupModalContentProps = {
user?: OidcUserInfo;
header?: boolean;
footer?: boolean;
tabAlignment?: "center" | "start" | "end";
setupKey?: string;
showOnlyRoutingPeerOS?: boolean;
};
export function SetupModalContent({
user,
header = true,
footer = true,
tabAlignment = "center",
}: {
user?: OidcUserInfo;
header?: boolean;
footer?: boolean;
tabAlignment?: "center" | "start" | "end";
}) {
setupKey,
showOnlyRoutingPeerOS,
}: Readonly<SetupModalContentProps>) {
const os = useOperatingSystem();
const [isFirstRun] = useLocalStorage<boolean>("netbird-first-run", true);
const pathname = usePathname();
@@ -60,24 +78,33 @@ export function SetupModalContent({
return (
<>
{header && (
<div className={"text-center pb-8 pt-4 px-8"}>
<h2 className={"text-3xl max-w-lg mx-auto"}>
<div className={"text-center pb-5 pt-4 px-8"}>
<h2
className={cn(
"max-w-lg mx-auto",
setupKey ? "text-2xl" : "text-3xl",
)}
>
{isFirstRun && !isInstallPage ? (
<>
Hello {user?.given_name || "there"}! 👋 <br />
{`It's time to add your first device.`}
</>
) : (
<>Install NetBird</>
<>Install NetBird{setupKey && " with Setup Key"}</>
)}
</h2>
<Paragraph className={"max-w-xs mx-auto mt-3"}>
To get started, install NetBird and log in with your email account.
<Paragraph
className={cn("mx-auto mt-3", setupKey ? "max-w-sm" : "max-w-xs")}
>
{setupKey
? "To get started, install and run NetBird with the setup key as a parameter."
: "To get started, install NetBird and log in with your email account."}
</Paragraph>
</div>
)}
<Tabs defaultValue={String(os)}>
<Tabs defaultValue={String(setupKey ? OperatingSystem.LINUX : os)}>
<TabsList justify={tabAlignment} className={"pt-2 px-3"}>
<TabsTrigger value={String(OperatingSystem.LINUX)}>
<ShellIcon
@@ -87,38 +114,49 @@ export function SetupModalContent({
/>
Linux
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.WINDOWS)}>
<WindowsIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Windows
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.APPLE)}>
<AppleIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
macOS
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.IOS)}>
<IOSIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
iOS
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.ANDROID)}>
<AndroidIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Android
</TabsTrigger>
{!showOnlyRoutingPeerOS && (
<>
<TabsTrigger value={String(OperatingSystem.WINDOWS)}>
<WindowsIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Windows
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.APPLE)}>
<AppleIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
macOS
</TabsTrigger>
</>
)}
{!setupKey && (
<>
<TabsTrigger value={String(OperatingSystem.IOS)}>
<IOSIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
iOS
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.ANDROID)}>
<AndroidIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Android
</TabsTrigger>
</>
)}
<TabsTrigger value={String(OperatingSystem.DOCKER)}>
<DockerIcon
className={
@@ -128,12 +166,25 @@ export function SetupModalContent({
Docker
</TabsTrigger>
</TabsList>
<LinuxTab />
<WindowsTab />
<MacOSTab />
<AndroidTab />
<IOSTab />
<DockerTab />
<LinuxTab
setupKey={setupKey}
showSetupKeyInfo={showOnlyRoutingPeerOS}
/>
<WindowsTab setupKey={setupKey} />
<MacOSTab setupKey={setupKey} />
{!setupKey && (
<>
<AndroidTab />
<IOSTab />
</>
)}
<DockerTab
setupKey={setupKey}
showSetupKeyInfo={showOnlyRoutingPeerOS}
/>
</Tabs>
{footer && (
<ModalFooter variant={"setup"}>
@@ -158,3 +209,32 @@ export function SetupModalContent({
</>
);
}
type SetupKeyParameterProps = {
setupKey?: string;
};
export const SetupKeyParameter = ({ setupKey }: SetupKeyParameterProps) => {
return (
setupKey && (
<>
{" "}
--setup-key <span className={"text-netbird"}>{setupKey}</span>
</>
)
);
};
export const RoutingPeerSetupKeyInfo = () => {
return (
<div
className={
"flex gap-2 mt-1 items-center text-xs text-nb-gray-300 font-normal mb-1"
}
>
This setup key can be used only once within the next 24 hours.
<br />
When expired, the same key can not be used again.
</div>
);
};

View File

@@ -2,13 +2,18 @@ import Button from "@components/Button";
import Code from "@components/Code";
import Steps from "@components/Steps";
import TabsContentPadding, { TabsContent } from "@components/Tabs";
import { GRPC_API_ORIGIN } from "@utils/netbird";
import { getNetBirdUpCommand, GRPC_API_ORIGIN } from "@utils/netbird";
import { DownloadIcon, PackageOpenIcon } from "lucide-react";
import Link from "next/link";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { SetupKeyParameter } from "@/modules/setup-netbird-modal/SetupModal";
export default function WindowsTab() {
type Props = {
setupKey?: string;
};
export default function WindowsTab({ setupKey }: Readonly<Props>) {
return (
<TabsContent value={String(OperatingSystem.WINDOWS)}>
<TabsContentPadding>
@@ -44,15 +49,29 @@ export default function WindowsTab() {
</Steps.Step>
)}
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
<p>
{/* eslint-disable-next-line react/no-unescaped-entities */}
Click on "Connect" from the NetBird icon in your system tray
</p>
</Steps.Step>
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
<p>Sign up using your email address</p>
</Steps.Step>
{setupKey ? (
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2} line={false}>
<p>Open Command-line and run NetBird</p>
<Code>
<Code.Line>
{getNetBirdUpCommand()}
<SetupKeyParameter setupKey={setupKey} />
</Code.Line>
</Code>
</Steps.Step>
) : (
<>
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
<p>
{/* eslint-disable-next-line react/no-unescaped-entities */}
Click on "Connect" from the NetBird icon in your system tray
</p>
</Steps.Step>
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
<p>Sign up using your email address</p>
</Steps.Step>
</>
)}
</Steps>
</TabsContentPadding>
</TabsContent>