diff --git a/package-lock.json b/package-lock.json index 6781ccea..75aed861 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5dc826a4..b63bac9c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/(dashboard)/network-routes/page.tsx b/src/app/(dashboard)/network-routes/page.tsx index c4646cc8..e5b00981 100644 --- a/src/app/(dashboard)/network-routes/page.tsx +++ b/src/app/(dashboard)/network-routes/page.tsx @@ -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={} /> -

- Network Routes -

+

Network Routes

Network routes allow you to access other networks like LANs and VPCs without installing NetBird on every resource. diff --git a/src/app/(dashboard)/networks/page.tsx b/src/app/(dashboard)/networks/page.tsx index d3b274f5..07683348 100644 --- a/src/app/(dashboard)/networks/page.tsx +++ b/src/app/(dashboard)/networks/page.tsx @@ -31,12 +31,15 @@ export default function Networks() {

Networks

- 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. Learn more about - + Networks diff --git a/src/components/PeerGroupSelector.tsx b/src/components/PeerGroupSelector.tsx index 904ed397..dfaf6a9b 100644 --- a/src/components/PeerGroupSelector.tsx +++ b/src/components/PeerGroupSelector.tsx @@ -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) { 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({ )} - + {showResourceCounter && ( + + )}
- {unfilteredItems.length == 0 && ( - - { - "Seems like you don't have any linux peers to assign as a routing peer." - } - + {unfilteredItems.length == 0 && !search && ( +
+ + { + "Seems like you don't have any Linux peers to assign as a routing peer." + } + +
)} - {filteredItems.length == 0 && ( + {filteredItems.length == 0 && search != "" && ( There are no peers matching your search. diff --git a/src/components/ScrollArea.tsx b/src/components/ScrollArea.tsx index 1f5551aa..4f8c6925 100644 --- a/src/components/ScrollArea.tsx +++ b/src/components/ScrollArea.tsx @@ -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) => ( {withoutViewport ? ( children ) : ( - - {children} - + {children} )} - + + )); ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; -type AdditionalScrollAreaViewportProps = { - disableOverflowY?: boolean; -}; - const ScrollAreaViewport = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & - AdditionalScrollAreaViewportProps ->(({ disableOverflowY = true, ...props }, ref) => { - return ( - - ); -}); + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName; const ScrollBar = React.forwardRef< @@ -63,14 +46,11 @@ const ScrollBar = React.forwardRef< >(({ className, orientation = "vertical", ...props }, ref) => ( diff --git a/src/components/SidebarItem.tsx b/src/components/SidebarItem.tsx index 1836b61a..067d40c2 100644 --- a/src/components/SidebarItem.tsx +++ b/src/components/SidebarItem.tsx @@ -60,10 +60,12 @@ export default function SidebarItem({
  • ; +export default function Steps({ + children, + className, + horizontal = false, +}: Readonly) { + return ( +
    + {children} +
    + ); } 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 (
    {line && ( )} diff --git a/src/components/ui/GroupBadge.tsx b/src/components/ui/GroupBadge.tsx index 641fdc10..93e6f9b1 100644 --- a/src/components/ui/GroupBadge.tsx +++ b/src/components/ui/GroupBadge.tsx @@ -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({ > - + {children} - {isNew && showNewBadge && ( - - NEW - - )} - + {isNew && showNewBadge && } {showX && ( { + return ( + + {text} + + ); +}; diff --git a/src/components/ui/TruncatedText.tsx b/src/components/ui/TruncatedText.tsx new file mode 100644 index 00000000..66a36c82 --- /dev/null +++ b/src/components/ui/TruncatedText.tsx @@ -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 ( +
    +
    {text}
    +
    + ); + } + + return ( + + +
    +
    {text}
    +
    +
    + + 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", + )} + > +
    +
    + {text} +
    +
    +
    +
    +
    + ); +} diff --git a/src/interfaces/Network.ts b/src/interfaces/Network.ts index 1e9f37aa..e7cd5601 100644 --- a/src/interfaces/Network.ts +++ b/src/interfaces/Network.ts @@ -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; } diff --git a/src/modules/access-control/AccessControlModal.tsx b/src/modules/access-control/AccessControlModal.tsx index 5bc6d573..246b12cc 100644 --- a/src/modules/access-control/AccessControlModal.tsx +++ b/src/modules/access-control/AccessControlModal.tsx @@ -281,6 +281,7 @@ export function AccessControlModalContent({ onChange={setSourceGroups} values={sourceGroups} saveGroupAssignments={useSave} + showResourceCounter={false} />
    Learn more about Access Controls diff --git a/src/modules/networks/NetworkProvider.tsx b/src/modules/networks/NetworkProvider.tsx index d214d05f..171f5a33 100644 --- a/src/modules/networks/NetworkProvider.tsx +++ b/src/modules/networks/NetworkProvider.tsx @@ -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); }} /> + + { + setCurrentResource(undefined); + setResourceGroupModal(state); + }} + onUpdated={() => { + setResourceGroupModal(false); + setCurrentResource(undefined); + mutate("/groups"); + if (network) { + mutate(`/networks/${network.id}/resources`); + mutate(`/networks/${network.id}`); + } + }} + /> + diff --git a/src/modules/networks/misc/NetworkNavigation.tsx b/src/modules/networks/misc/NetworkNavigation.tsx index 590da45a..9d7ec8dd 100644 --- a/src/modules/networks/misc/NetworkNavigation.tsx +++ b/src/modules/networks/misc/NetworkNavigation.tsx @@ -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 ( - } - label="Networks" - collapsible - exactPathMatch={false} - > - + <> } label={ -
    - Network Routes - +
    + Networks +
    } - isChild + href={"/networks"} + /> + } href={"/network-routes"} + label={"Network Routes"} /> - + ); }; diff --git a/src/modules/networks/resources/NetworkResourceModal.tsx b/src/modules/networks/resources/NetworkResourceModal.tsx index da2d1b81..08c86f9b 100644 --- a/src/modules/networks/resources/NetworkResourceModal.tsx +++ b/src/modules/networks/resources/NetworkResourceModal.tsx @@ -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( + 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({
    - Control access to this resource by assigning it to groups + Add this resource to groups and use them as destinations when + creating policies
    +
    + + + Enable Resource + + } + helpText={"Use this switch to enable or disable the resource."} + /> +
    Learn more about - + Resources diff --git a/src/modules/networks/resources/ResourceActionCell.tsx b/src/modules/networks/resources/ResourceActionCell.tsx index 39b5f944..aa1f93cf 100644 --- a/src/modules/networks/resources/ResourceActionCell.tsx +++ b/src/modules/networks/resources/ResourceActionCell.tsx @@ -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 ( -
    - - +
    + + { + e.stopPropagation(); + e.preventDefault(); + }} + > + + + + { + if (!network) return; + openResourceModal(network, resource); + }} + > +
    + + Edit +
    +
    + { + if (!network) return; + deleteResource(network, resource); + }} + variant={"danger"} + > +
    + + Remove +
    +
    +
    +
    ); }; diff --git a/src/modules/networks/resources/ResourceEnabledCell.tsx b/src/modules/networks/resources/ResourceEnabledCell.tsx new file mode 100644 index 00000000..dd24c119 --- /dev/null +++ b/src/modules/networks/resources/ResourceEnabledCell.tsx @@ -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( + `/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 ( +
    + toggle(!isChecked)} + /> +
    + ); +}; diff --git a/src/modules/networks/resources/ResourceGroupCell.tsx b/src/modules/networks/resources/ResourceGroupCell.tsx index 1b6c1d8b..752cbc24 100644 --- a/src/modules/networks/resources/ResourceGroupCell.tsx +++ b/src/modules/networks/resources/ResourceGroupCell.tsx @@ -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 ( -
    +
    + ); }; diff --git a/src/modules/networks/resources/ResourceGroupModal.tsx b/src/modules/networks/resources/ResourceGroupModal.tsx new file mode 100644 index 00000000..2c44b3b8 --- /dev/null +++ b/src/modules/networks/resources/ResourceGroupModal.tsx @@ -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 ( + + {network && resource && ( + + )} + + ); +}; + +type ModalProps = { + onUpdated?: (r: NetworkResource) => void; + network?: Network; + resource?: NetworkResource; +}; + +const ResourceGroupModalContent = ({ + resource, + network, + onUpdated, +}: ModalProps) => { + const update = useApiCall( + `/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 ( + + } + title={"Assigned Groups"} + description={ + "Add this resource to groups and use them as destinations when creating policies" + } + color={"blue"} + /> + + + +
    +
    + +
    +
    + + +
    + + + + + +
    +
    +
    + ); +}; diff --git a/src/modules/networks/resources/ResourceNameCell.tsx b/src/modules/networks/resources/ResourceNameCell.tsx index 17f2e8e7..90286b76 100644 --- a/src/modules/networks/resources/ResourceNameCell.tsx +++ b/src/modules/networks/resources/ResourceNameCell.tsx @@ -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) { + const { network, openResourceModal } = useNetworksContext(); + return ( -
    + ); } diff --git a/src/modules/networks/resources/ResourcePolicyCell.tsx b/src/modules/networks/resources/ResourcePolicyCell.tsx index f0f0d2ff..e48c8cfa 100644 --- a/src/modules/networks/resources/ResourcePolicyCell.tsx +++ b/src/modules/networks/resources/ResourcePolicyCell.tsx @@ -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) => { -
    -
    [] = [ { id: "id", - accessorKey: "id", + accessorKey: "name", header: ({ column }) => { return Resource; }, @@ -40,9 +46,20 @@ const NetworkResourceColumns: ColumnDef[] = [ return ; }, }, + { + id: "enabled", + accessorKey: "enabled", + header: ({ column }) => { + return Active; + }, + cell: ({ row }) => , + }, { id: "groups", - accessorKey: "id", + accessorFn: (resource) => { + let groups = resource?.groups as Group[]; + return groups.map((group) => group.name).join(", "); + }, header: ({ column }) => { return Groups; }, @@ -74,40 +91,56 @@ export default function ResourcesTable({ resources, isLoading, headingTarget, -}: Props) { +}: Readonly) { const [sorting, setSorting] = useState([]); + const { openResourceModal, network } = useNetworksContext(); return ( - <> - } - /> - } - columnVisibility={{}} - paginationPaddingClassName={"px-0 pt-8"} - /> - + } + /> + } + columnVisibility={{}} + paginationPaddingClassName={"px-0 pt-8"} + rightSide={() => ( + + )} + > + {(table) => ( + + )} + ); } diff --git a/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx b/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx index 94dda20a..16651981 100644 --- a/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx +++ b/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx @@ -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( router ? router.masquerade : true, ); + const [enabled, setEnabled] = useState( + 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 ( - Routers + Routing Peers @@ -203,48 +219,86 @@ function RoutingPeerModalContent({ Advanced Settings - -
    - - - - - Routing Peers - - - - - Peer Group - - - -
    - - Assign a single or multiple peers as a routing peers for the - network. - - -
    -
    - -
    - - Assign a peer group with Linux machines to be used as - routing peers. - - -
    -
    -
    + +
    +
    + { + setType(state); + setRoutingPeer(undefined); + setRoutingPeerGroups([]); + }} + > + + + + Routing Peers + + + + + Peer Group + + + +
    + + Assign a single or multiple Linux peers as routing peers + for the network. + + +
    +
    + +
    + + Assign a peer group with Linux machines to be used as + routing peers. + + +
    +
    +
    +
    + +
    +
    + + + You can install NetBird with a setup key on one or more Linux + machines to act as routing peers. + +
    + +
    + + + Enable Routing Peer + + } + helpText={ + "Use this switch to enable or disable the routing peer." + } + />
    - - - {tab == "router" && ( - + <> + + + + + )} {tab == "settings" && ( - + <> + + + + )}
    ); } + +type InstallNetBirdWithSetupKeyButtonProps = { + name?: string; +}; + +const InstallNetBirdWithSetupKeyButton = ({ + name, +}: InstallNetBirdWithSetupKeyButtonProps) => { + const setupKeyRequest = useApiCall("/setup-keys", true); + const { mutate } = useSWRConfig(); + const { confirm } = useDialog(); + + const [installModal, setInstallModal] = useState(false); + const [setupKey, setSetupKey] = useState(); + 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 ( + <> + + {setupKey && ( + + + + )} + + ); +}; diff --git a/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx b/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx index 03511e2c..092f5064 100644 --- a/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx +++ b/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx @@ -44,13 +44,11 @@ export const NetworkRoutingPeerName = ({ router }: Props) => { if (routingPeerGroup) { return ( - <> -
    - - - {routingPeerGroup.peers_count} Peer(s) -
    - +
    + + + {routingPeerGroup.peers_count} Peer(s) +
    ); } }; diff --git a/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx b/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx index fc4ca60d..9e801ad9 100644 --- a/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx +++ b/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx @@ -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[] = [ sortingFn: "text", cell: ({ row }) => , }, + { + id: "enabled", + accessorKey: "enabled", + header: ({ column }) => { + return Active; + }, + cell: ({ row }) => , + }, { id: "metric", accessorKey: "metric", header: ({ column }) => { return Metric; }, - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { id: "masquerade", @@ -58,7 +73,9 @@ export default function NetworkRoutingPeersTable({ routers, isLoading, headingTarget, -}: Props) { +}: Readonly) { + const { openAddRoutingPeerModal, network } = useNetworksContext(); + const [sorting, setSorting] = useState([ { id: "metric", @@ -69,7 +86,7 @@ export default function NetworkRoutingPeersTable({ return ( + rightSide={() => ( + + )} + > + {(table) => ( + + )} + ); } diff --git a/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx b/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx index 91b9e001..3c64415c 100644 --- a/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx +++ b/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx @@ -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 ( -
    - - +
    + + { + e.stopPropagation(); + e.preventDefault(); + }} + > + + + + { + if (!network) return; + openAddRoutingPeerModal(network, router); + }} + > +
    + + Edit +
    +
    + { + if (!network) return; + deleteRouter(network, router); + }} + variant={"danger"} + > +
    + + Remove +
    +
    +
    +
    ); }; diff --git a/src/modules/networks/routing-peers/RoutingPeersEnabledCell.tsx b/src/modules/networks/routing-peers/RoutingPeersEnabledCell.tsx new file mode 100644 index 00000000..debd5925 --- /dev/null +++ b/src/modules/networks/routing-peers/RoutingPeersEnabledCell.tsx @@ -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( + `/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 ( +
    + toggle(!isChecked)} + /> +
    + ); +}; diff --git a/src/modules/networks/table/NetworksTable.tsx b/src/modules/networks/table/NetworksTable.tsx index dda7f662..f3177a65 100644 --- a/src/modules/networks/table/NetworksTable.tsx +++ b/src/modules/networks/table/NetworksTable.tsx @@ -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[] = [ { accessorKey: "description", }, - { - accessorKey: "routers", - accessorFn: (network) => network?.routers?.length, - header: ({ column }) => { - return Routing Peers; - }, - cell: ({ row }) => , - }, { accessorKey: "resources", accessorFn: (network) => network?.resources?.length, @@ -62,6 +53,14 @@ export const NetworkTableColumns: ColumnDef[] = [ }, cell: ({ row }) => , }, + { + accessorKey: "routers", + accessorFn: (network) => network?.routers?.length, + header: ({ column }) => { + return Routing Peers; + }, + cell: ({ row }) => , + }, { 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 ( Learn more about Networks diff --git a/src/modules/peers/PeersTable.tsx b/src/modules/peers/PeersTable.tsx index 3f98c381..29f8a45e 100644 --- a/src/modules/peers/PeersTable.tsx +++ b/src/modules/peers/PeersTable.tsx @@ -63,7 +63,8 @@ const PeersTableColumns: ColumnDef[] = [ enableHiding: false, }, { - accessorKey: "name", + id: "name", + accessorFn: (peer) => `${peer?.name}${peer?.dns_label}`, header: ({ column }) => { return Name; }, @@ -94,6 +95,7 @@ const PeersTableColumns: ColumnDef[] = [ accessorFn: (peer) => (peer.user ? peer.user?.email : "Unknown"), }, { + id: "dns_label", accessorKey: "dns_label", header: ({ column }) => { return Address; @@ -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, diff --git a/src/modules/routes/RouteMetricCell.tsx b/src/modules/routes/RouteMetricCell.tsx index 6b72677c..a805de93 100644 --- a/src/modules/routes/RouteMetricCell.tsx +++ b/src/modules/routes/RouteMetricCell.tsx @@ -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) { return ( diff --git a/src/modules/settings/GroupsTable.tsx b/src/modules/settings/GroupsTable.tsx index d58ab8c2..e95c0742 100644 --- a/src/modules/settings/GroupsTable.tsx +++ b/src/modules/settings/GroupsTable.tsx @@ -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[] = [ /> ), }, + { + accessorKey: "resources_count", + header: ({ column }) => { + return ( + Network Resources
    + } + > + + + ); + }, + cell: ({ row }) => ( + } + 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[] = [ 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) { const groups = useGroupsUsage(); const path = usePathname(); diff --git a/src/modules/settings/useGroupsUsage.tsx b/src/modules/settings/useGroupsUsage.tsx index 6e085d56..0551addf 100644 --- a/src/modules/settings/useGroupsUsage.tsx +++ b/src/modules/settings/useGroupsUsage.tsx @@ -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, diff --git a/src/modules/setup-keys/SetupKeyModal.tsx b/src/modules/setup-keys/SetupKeyModal.tsx index 3192cff1..97291d4a 100644 --- a/src/modules/setup-keys/SetupKeyModal.tsx +++ b/src/modules/setup-keys/SetupKeyModal.tsx @@ -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) { const [successModal, setSuccessModal] = useState(false); const [setupKey, setSetupKey] = useState(); - 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({ <> {children && {children}} - + + + { + setInstallModal(state); + setOpen(false); + }} + key={installModal ? 2 : 3} + > + + + { @@ -118,10 +137,10 @@ export default function SetupKeyModal({ variant={"primary"} className={"w-full"} data-cy={"setup-key-copy"} - onClick={() => copy(copyMessage)} + onClick={() => setInstallModal(true)} > - - Copy to clipboard + + Install NetBird
    @@ -133,13 +152,17 @@ export default function SetupKeyModal({ type ModalProps = { onSuccess?: (setupKey: SetupKey) => void; + predefinedName?: string; }; -export function SetupKeyModalContent({ onSuccess }: Readonly) { +export function SetupKeyModalContent({ + onSuccess, + predefinedName = "", +}: Readonly) { const setupKeyRequest = useApiCall("/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"); diff --git a/src/modules/setup-netbird-modal/DockerTab.tsx b/src/modules/setup-netbird-modal/DockerTab.tsx index eb35b000..e6cd059d 100644 --- a/src/modules/setup-netbird-modal/DockerTab.tsx +++ b/src/modules/setup-netbird-modal/DockerTab.tsx @@ -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) { return ( @@ -35,14 +44,20 @@ export default function DockerTab() { -

    Run NetBird container

    +

    + Run NetBird container + {showSetupKeyInfo && } +

    docker run --rm -d \ --cap-add=NET_ADMIN \ {" "} -e NB_SETUP_KEY= - SETUP_KEY \ + + {setupKey ?? "SETUP_KEY"} + {" "} + \ -v netbird-client:/etc/netbird \ {GRPC_API_ORIGIN && ( diff --git a/src/modules/setup-netbird-modal/LinuxTab.tsx b/src/modules/setup-netbird-modal/LinuxTab.tsx index 9fcadaae..581e82d5 100644 --- a/src/modules/setup-netbird-modal/LinuxTab.tsx +++ b/src/modules/setup-netbird-modal/LinuxTab.tsx @@ -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"; + +type Props = { + setupKey?: string; + showSetupKeyInfo?: boolean; +}; -export default function LinuxTab() { +export default function LinuxTab({ + setupKey, + showSetupKeyInfo = false, +}: Readonly) { return ( @@ -27,9 +39,15 @@ export default function LinuxTab() { curl -fsSL https://pkgs.netbird.io/install.sh | sh
    -

    Run NetBird and log in the browser

    +

    + Run NetBird {!setupKey && "and log in the browser"} + {showSetupKeyInfo && } +

    - {getNetBirdUpCommand()} + + {getNetBirdUpCommand()} + +
    @@ -78,9 +96,15 @@ export default function LinuxTab() {
    -

    Run NetBird and log in the browser

    +

    + Run NetBird {!setupKey && "and log in the browser"} + {showSetupKeyInfo && } +

    - {getNetBirdUpCommand()} + + {getNetBirdUpCommand()} + +
    diff --git a/src/modules/setup-netbird-modal/MacOSTab.tsx b/src/modules/setup-netbird-modal/MacOSTab.tsx index a9b5964c..7e4f1525 100644 --- a/src/modules/setup-netbird-modal/MacOSTab.tsx +++ b/src/modules/setup-netbird-modal/MacOSTab.tsx @@ -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) { return ( @@ -98,15 +102,29 @@ export default function MacOSTab() { )} - -

    - {/* eslint-disable-next-line react/no-unescaped-entities */} - Click on "Connect" from the NetBird icon in your system tray -

    -
    - -

    Sign up using your email address

    -
    + {setupKey ? ( + +

    Open Terminal and run NetBird

    + + + {getNetBirdUpCommand()} + + + +
    + ) : ( + <> + +

    + {/* eslint-disable-next-line react/no-unescaped-entities */} + Click on "Connect" from the NetBird icon in your system tray +

    +
    + +

    Sign up using your email address

    +
    + + )}
    @@ -125,9 +143,12 @@ export default function MacOSTab() { -

    Run NetBird and log in the browser

    +

    Run NetBird {!setupKey && "and log in the browser"}

    - {getNetBirdUpCommand()} + + {getNetBirdUpCommand()} + +
    @@ -179,9 +200,12 @@ export default function MacOSTab() { -

    Run NetBird and log in the browser

    +

    Run NetBird {!setupKey && "and log in the browser"}

    - {getNetBirdUpCommand()} + + {getNetBirdUpCommand()} + +
    diff --git a/src/modules/setup-netbird-modal/SetupModal.tsx b/src/modules/setup-netbird-modal/SetupModal.tsx index e7a4d591..99923104 100644 --- a/src/modules/setup-netbird-modal/SetupModal.tsx +++ b/src/modules/setup-netbird-modal/SetupModal.tsx @@ -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) { return ( - + ); } +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) { const os = useOperatingSystem(); const [isFirstRun] = useLocalStorage("netbird-first-run", true); const pathname = usePathname(); @@ -60,24 +78,33 @@ export function SetupModalContent({ return ( <> {header && ( -
    -

    +
    +

    {isFirstRun && !isInstallPage ? ( <> Hello {user?.given_name || "there"}! 👋
    {`It's time to add your first device.`} ) : ( - <>Install NetBird + <>Install NetBird{setupKey && " with Setup Key"} )}

    - - To get started, install NetBird and log in with your email account. + + {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."}
    )} - + Linux - - - Windows - - - - macOS - - - - iOS - - - - Android - + + {!showOnlyRoutingPeerOS && ( + <> + + + Windows + + + + macOS + + + )} + + {!setupKey && ( + <> + + + iOS + + + + Android + + + )} + - - - - - - + + + + + + {!setupKey && ( + <> + + + + )} + + {footer && ( @@ -158,3 +209,32 @@ export function SetupModalContent({ ); } + +type SetupKeyParameterProps = { + setupKey?: string; +}; + +export const SetupKeyParameter = ({ setupKey }: SetupKeyParameterProps) => { + return ( + setupKey && ( + <> + {" "} + --setup-key {setupKey} + + ) + ); +}; + +export const RoutingPeerSetupKeyInfo = () => { + return ( +
    + This setup key can be used only once within the next 24 hours. +
    + When expired, the same key can not be used again. +
    + ); +}; diff --git a/src/modules/setup-netbird-modal/WindowsTab.tsx b/src/modules/setup-netbird-modal/WindowsTab.tsx index 49bc7d0a..1a6a01ba 100644 --- a/src/modules/setup-netbird-modal/WindowsTab.tsx +++ b/src/modules/setup-netbird-modal/WindowsTab.tsx @@ -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) { return ( @@ -44,15 +49,29 @@ export default function WindowsTab() { )} - -

    - {/* eslint-disable-next-line react/no-unescaped-entities */} - Click on "Connect" from the NetBird icon in your system tray -

    -
    - -

    Sign up using your email address

    -
    + {setupKey ? ( + +

    Open Command-line and run NetBird

    + + + {getNetBirdUpCommand()} + + + +
    + ) : ( + <> + +

    + {/* eslint-disable-next-line react/no-unescaped-entities */} + Click on "Connect" from the NetBird icon in your system tray +

    +
    + +

    Sign up using your email address

    +
    + + )}