Compare commits

...
24 Commits
Author SHA1 Message Date
Eduard Gert 3affa8908f Redirect /setup to /peers if no setup is required (#526)
* Redirect /setup to /peers if not setup is required

* Fix bad state while redirect

* Prevent redirect to /setup if already on /setup

* Fix loading state
2026-01-08 15:01:45 +01:00
Eduard Gert 52fd984912 Add user view to control center (#525) 2026-01-07 17:53:55 +01:00
Misha BraginandEduard Gert 83e3159ee4 Configure Identity Providers in the UI (#523)
* Add user creation with password copy

* Add initial identity provider view

* Add IdP logos

* Add IdP id to user

* Add IdP logo to user obj

* Fix okta icon

* Return callback URL when creating an IdP

* Create user for self-hosted

* Clear up password from the state

* Show IdPs and create user when enabled

* Fetch IdPs only when embedded idp is enabled

* Update src/app/(dashboard)/settings/page.tsx

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

* Update src/app/(dashboard)/settings/page.tsx

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

* Update src/modules/settings/IdentityProvidersTab.tsx

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

* Update src/modules/settings/IdentityProviderModal.tsx

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

* Update src/modules/settings/IdentityProvidersTab.tsx

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

* Update src/modules/settings/IdentityProviderModal.tsx

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

* Rename IdentityProvider to SSOIdentityProvider

* Fix build and extract icons

* Fix initial onboarding

* Add icons

* Move name to the top

* Fix setup wizard background color

* Update instance setup ui

* Update instance setup ui

* Use input component

* Move idp label and icons

* Fix setup wizard width

* Add authentik and keycloak

* Add idp hints

* Handle idp permissions

* Consider selfhosted instances when checking if netbird is hosted

* Update redirect

* Add max retries to redirect

* Require new secret when clientid changed

* Add callback URL on the idp creation step

* Add idp activity events

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-01-07 14:43:30 +01:00
Eduard Gert bf81aeb02d Add fine-grained ssh policy (#522)
* Add fine-grained ssh policy

* Update version text

* Fix coderabbit comment
2025-12-30 09:27:17 +01:00
Eduard Gert b058e66e32 Add auto update setting (#519) 2025-12-29 12:38:50 +01:00
Eduard Gert 8d6b617cbd Update NextJS to 14.2.35 (#518) 2025-12-22 11:02:29 +01:00
Eduard Gert 47db655e9f Update eslint and tailwind (#515) 2025-11-27 17:38:18 +01:00
dependabot[bot] 0661cbf9f4 Bump js-yaml from 4.1.0 to 4.1.1 (#509)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 15:25:50 +01:00
Eduard Gert 240a96fa8b Add onboarding for new accounts (#514) 2025-11-27 14:49:58 +01:00
Eduard Gert 43bc069a49 Increase ssh detection timeout (#512) 2025-11-21 10:32:50 +01:00
Eduard Gert 936de0f4f3 Add ssh policy info for peers (#511) 2025-11-20 14:29:14 +01:00
Eduard Gert d81b75a946 Bump browser ssh versions for ssh rewrite (#510)
* Bump browser ssh versions for ssh rewrite

* Remove cypress temporary
2025-11-18 17:07:58 +01:00
Eduard Gert a632eeeef0 Remove dns0eu (#508) 2025-11-10 14:21:58 +01:00
Eduard Gert e2219aeea0 Add group update activity event (#504) 2025-11-10 10:50:04 +01:00
Eduard Gert 63f4c69eb4 Fix native ssh detection (#505) 2025-11-07 09:33:58 +01:00
Eduard Gert b1af256296 Add wasm client version (#503) 2025-11-06 10:59:41 +01:00
Eduard Gertandaliamerj 4027894a2e Feature/groups page (#498)
* move our group membership from the settings menu, into the Team menu

* add action to the table and new group page

* update group page and return group settings to settings menu

* new update

* fix bug

* group action: add peer to group

* group action: add user to group

* Update wording, redirect to group page after creation

* Add better table loading skeleton

* Adjust group name cell

* Update wording

* Update sort order

* Refactor

* Merge main

* Fix button height

* Fix resources table

* Adjust table loading skeleton

* Adjust table loading skeleton

* Add loading to tab triggers

* Update meta

* Update group location

* Fix rename

* Refactor group details

* Fix linked peers

* Fix group usage

* Fix incrementing peer count

* Prevent renaming to already existing group

* Fix group name click

* Update group nav

* Make group table cells clickable

* Fix breadcrumbs

* Update wording

* Add confirmation before removing users from group

* Add permissions

* Add initial group for network routes

* Add acl and routing peer groups

---------

Co-authored-by: aliamerj <aliamer19ali@gmail.com>
2025-11-05 12:08:49 +01:00
Yanis64andEduard Gert af90792595 Add multi-group support for JWT allow groups with tag system (#500)
* fix: add multi-group support for JWT allow groups with tag system

* Update src/modules/settings/GroupsTab.tsx to use the Badge component

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

* chore(GroupsTab): import Badge components

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2025-11-03 16:09:15 +01:00
Eduard Gert 9a401733b3 Fix toggle for p2p policies (#501) 2025-10-31 13:21:23 +01:00
Eduard Gert 07b6895380 Sync SSH & RDP changes (#495) 2025-10-16 14:44:26 +02:00
Eduard Gert 9e2e38764e Add control center (#494)
* Add control center

* Update rdp doc link
2025-10-09 11:26:21 +02:00
Maycon Santos d9fb379abf Enable connect buttons (#493) 2025-10-06 16:23:00 -03:00
Eduard Gert 831673d0d6 Sync with cloud (#491)
implements a "Sync with cloud" functionality that includes various UI improvements, code refactoring, and component extractions. The changes focus on enhancing the user interface, improving code organization, and adding new features for remote access and activity tracking.

- Refactors inline components into reusable shared components
- Adds new activity tracking for group operations
- Updates remote access configuration and UI components
- Enhances styling and layout for better user experience
2025-10-03 14:37:11 +02:00
Maycon Santos bc4aac10aa Add browser client support (#490)
* Sync wasm rdp and ssh client

* sync package-lock

* remove msp ref

* add ephemeral info
2025-10-02 00:41:08 +02:00
270 changed files with 25928 additions and 4913 deletions

No files matched your search

+28 -3
View File
@@ -2,7 +2,6 @@ name: build and push
on:
push:
branches:
- "feature/**"
- main
tags:
- "**"
@@ -28,6 +27,33 @@ jobs:
- run: echo '{}' > .local-config.json
- name: Download IronRDP release TS files
uses: robinraju/release-downloader@v1.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: netbirdio/IronRDP
latest: true
fileName: "*.ts"
out-file-path: 'public/ironrdp-pkg'
- name: Download IronRDP release JS files
uses: robinraju/release-downloader@v1.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: netbirdio/IronRDP
latest: true
fileName: "*.js"
out-file-path: 'public/ironrdp-pkg'
- name: Download IronRDP release WASM file
uses: robinraju/release-downloader@v1.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: netbirdio/IronRDP
latest: true
fileName: "ironrdp_web_bg.wasm"
out-file-path: 'public/ironrdp-pkg'
- name: Build
run: npm run build
-
@@ -44,7 +70,6 @@ jobs:
images: ${{ env.IMAGE_NAME }}
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.NB_DOCKER_USER }}
@@ -55,7 +80,7 @@ jobs:
with:
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
push: true
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+5
View File
@@ -41,3 +41,8 @@ next-env.d.ts
.configs/.staging-config.json
.configs/.temp-config.json
.configs
/public/ironrdp-pkg/
/public/netbird.wasm
.idea
src/.local-config*
+1
View File
@@ -26,6 +26,7 @@ The dashboard makes it possible to:
- NextJS
- ReactJS
- Tailwind CSS
- [React Flow](https://reactflow.dev/) for the Control Center
- Auth0
- Nginx
- Docker
+2 -1
View File
@@ -13,5 +13,6 @@
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID"
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
"wasmPath": "$NETBIRD_WASM_PATH"
}
+8
View File
@@ -3,6 +3,14 @@ server {
listen [::]:80 default_server;
root /usr/share/nginx/html;
location = /netbird.wasm {
root /usr/share/nginx/html;
default_type application/wasm;
}
location = /ironrdp-pkg/ironrdp_web_bg.wasm {
root /usr/share/nginx/html;
default_type application/wasm;
}
location / {
try_files $uri $uri.html $uri/ =404;
+2 -1
View File
@@ -61,11 +61,12 @@ export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
+1
View File
@@ -101,6 +101,7 @@ http {
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/wasm
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
+3265 -2844
View File
File diff suppressed because it is too large. Load diff
+12 -6
View File
@@ -14,6 +14,7 @@
},
"dependencies": {
"@axa-fr/react-oidc": "^7.22.18",
"@dagrejs/dagre": "^1.1.5",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
@@ -35,11 +36,15 @@
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.7",
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/lodash": "^4.14.200",
"@types/node": "20.10.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-window": "^1.8.8",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"@xyflow/react": "^12.8.4",
"autoprefixer": "^10",
"chart.js": "^4.4.8",
"chroma-js": "^3.1.2",
@@ -47,9 +52,10 @@
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"eslint": "^8",
"elkjs": "^0.10.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"flowbite": "^1.8.1",
@@ -58,8 +64,8 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.481.0",
"next": "^14.2.28",
"lucide-react": "^0.539.0",
"next": "^14.2.35",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18.3.1",
@@ -83,10 +89,10 @@
"@faker-js/faker": "^9.5.1",
"@types/chroma-js": "^3.1.1",
"@types/js-cookie": "^3.0.6",
"eslint-config-next": "^14.2.28",
"cypress": "^13.13.0",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.5",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
"tailwindcss": "^3.4.17"
}
}
+575
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -33,7 +33,7 @@ export default function AccessControlPage() {
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/policies"}
href={"/access-control"}
label={"Access Control"}
icon={<AccessControlIcon size={14} />}
/>
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Control Center - ${globalMetaTitle}`,
};
export default BlankLayout;
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -32,7 +32,7 @@ export default function NameServers() {
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/dns"}
href={"/dns/nameservers"}
label={"DNS"}
icon={<DNSIcon size={13} />}
/>
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Group - ${globalMetaTitle}`,
};
export default BlankLayout;
+315
View File
@@ -0,0 +1,315 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import FullTooltip from "@components/FullTooltip";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
import { PageNotFound } from "@components/ui/PageNotFound";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn, singularize } from "@utils/helpers";
import { FolderGit2Icon, Layers3Icon, PencilIcon } from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useState } from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import DNSIcon from "@/assets/icons/DNSIcon";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import TeamIcon from "@/assets/icons/TeamIcon";
import { GroupProvider, useGroupContext } from "@/contexts/GroupProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { Group, GROUP_TOOLTIP_TEXT } from "@/interfaces/Group";
import PageContainer from "@/layouts/PageContainer";
import { GroupNameserversSection } from "@/modules/groups/details/GroupNameserversSection";
import { GroupNetworkRoutesSection } from "@/modules/groups/details/GroupNetworkRoutesSection";
import { GroupPeersSection } from "@/modules/groups/details/GroupPeersSection";
import { GroupPoliciesSection } from "@/modules/groups/details/GroupPoliciesSection";
import { GroupResourcesSection } from "@/modules/groups/details/GroupResourcesSection";
import { GroupSetupKeysSection } from "@/modules/groups/details/GroupSetupKeysSection";
import { GroupUsersSection } from "@/modules/groups/details/GroupUsersSection";
import useGroupDetails from "@/modules/groups/details/useGroupDetails";
export default function GroupPage() {
const queryParameter = useSearchParams();
const { isRestricted } = usePermissions();
const groupId = queryParameter.get("id");
const {
data: group,
isLoading,
error,
} = useFetchApi<Group>(`/groups/${groupId}`, true);
useRedirect("/groups", false, !groupId || isRestricted);
if (isRestricted) {
return (
<PageContainer>
<RestrictedAccess page={"Group Information"} />
</PageContainer>
);
}
if (error)
return (
<PageNotFound
title={error?.message}
description={
"The group you are attempting to access cannot be found. It may have been deleted, or you may not have permission to view it. Please verify the URL or return to the dashboard."
}
/>
);
return group && !isLoading ? (
<PageContainer>
<RoutesProvider>
<GroupProvider group={group} isDetailPage={true}>
<div className={"p-default py-6 pb-0 w-full mb-[6px]"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/groups"}
label={"Groups"}
icon={<FolderGit2Icon size={14} />}
/>
<Breadcrumbs.Item label={group.name} active />
</Breadcrumbs>
<GroupDetailsName />
</div>
<GroupOverviewTabs group={group} />
</GroupProvider>
</RoutesProvider>
</PageContainer>
) : (
<FullScreenLoading />
);
}
const GroupDetailsName = () => {
const { group, isJWTGroup, isAllowedToRename, openGroupRenameModal } =
useGroupContext();
const { permission } = usePermissions();
return (
<div className={"w-full"}>
<h1 className={"flex items-center gap-3 w-full whitespace-nowrap"}>
<GroupBadgeIcon id={group?.id} issued={group?.issued} size={20} />
{group.name}
{group.name !== "All" && permission?.groups?.update && (
<div>
<FullTooltip
content={
<div className={"text-xs max-w-xs"}>
{isJWTGroup
? GROUP_TOOLTIP_TEXT.RENAME.JWT
: GROUP_TOOLTIP_TEXT.RENAME.INTEGRATION}
</div>
}
interactive={false}
disabled={isAllowedToRename}
className={"w-full block"}
>
<div
className={cn(
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
!isAllowedToRename &&
"opacity-40 cursor-not-allowed pointer-events-none",
)}
onClick={openGroupRenameModal}
>
<PencilIcon size={16} />
</div>
</FullTooltip>
</div>
)}
</h1>
</div>
);
};
const validAllGroupTabs = [
"policies",
"resources",
"network-routes",
"nameservers",
];
const validOtherGroupTabs = ["users", "peers", "setup-keys"];
const GroupOverviewTabs = ({ group }: { group: Group }) => {
const searchParams = useSearchParams();
const getInitialTab = () => {
const isAllGroup = group.name === "All";
const tabParam = searchParams.get("tab");
const validTabs = isAllGroup
? validAllGroupTabs
: [...validAllGroupTabs, ...validOtherGroupTabs];
if (tabParam === null) return isAllGroup ? "policies" : "users";
if (isAllGroup) {
return validTabs.includes(tabParam) ? tabParam : "policies";
}
return validTabs.includes(tabParam) ? tabParam : "users";
};
const [tab, setTab] = useState(getInitialTab());
const { groupDetails, isLoading } = useGroupDetails(group?.id || "");
const peersCount = groupDetails?.peers_count || 0;
const usersCount = groupDetails?.users?.length || 0;
const policiesCount = groupDetails?.policies?.length || 0;
const resourcesCount = groupDetails?.resources_count || 0;
const routesCount = groupDetails?.routes?.length || 0;
const nameserversCount = groupDetails?.nameservers?.length || 0;
const setupKeysCount = groupDetails?.setupKeys?.length || 0;
return (
<Tabs
defaultValue={tab}
onValueChange={(v) => setTab(v)}
value={tab}
className={"pt-2 pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
{group.name !== "All" && (
<TabsTrigger
value={"users"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<TeamIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Users", usersCount)}
</TabsTrigger>
)}
{group.name !== "All" && (
<TabsTrigger
value={"peers"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<PeerIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Peers", peersCount)}
</TabsTrigger>
)}
<TabsTrigger
value={"policies"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<AccessControlIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Policies", policiesCount)}
</TabsTrigger>
<TabsTrigger
value={"resources"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<Layers3Icon size={14} />
{singularize("Resources", resourcesCount)}
</TabsTrigger>
<TabsTrigger
value={"network-routes"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<NetworkRoutesIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Network Routes", routesCount)}
</TabsTrigger>
<TabsTrigger
value={"nameservers"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<DNSIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Nameservers", nameserversCount)}
</TabsTrigger>
{group.name !== "All" && (
<TabsTrigger
value={"setup-keys"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<SetupKeysIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Setup Keys", setupKeysCount)}
</TabsTrigger>
)}
</TabsList>
<TabsContent value={"users"} className={"pb-8"}>
<GroupUsersSection users={groupDetails?.users} isLoading={isLoading} />
</TabsContent>
<TabsContent value={"peers"} className={"pb-8"}>
<GroupPeersSection
peers={groupDetails?.peersOfGroup}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"policies"} className={"pb-8"}>
<GroupPoliciesSection
policies={groupDetails?.policies}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"resources"} className={"pb-8"}>
<GroupResourcesSection
resources={groupDetails?.networkResources}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"network-routes"} className={"pb-8"}>
<GroupNetworkRoutesSection
routes={groupDetails?.routes}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"nameservers"} className={"pb-8"}>
<GroupNameserversSection
nameserverGroups={groupDetails?.nameservers}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"setup-keys"} className={"pb-8"}>
<GroupSetupKeysSection
setupKeys={groupDetails?.setupKeys}
isLoading={isLoading}
/>
</TabsContent>
</Tabs>
);
};
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Groups - ${globalMetaTitle}`,
};
export default BlankLayout;
+56
View File
@@ -0,0 +1,56 @@
"use client";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import { ExternalLinkIcon, FolderGit2Icon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import Breadcrumbs from "@/components/Breadcrumbs";
import InlineLink from "@/components/InlineLink";
import { usePermissions } from "@/contexts/PermissionsProvider";
import PageContainer from "@/layouts/PageContainer";
const GroupsTable = lazy(() => import("@/modules/groups/table/GroupsTable"));
export default function GroupsPage() {
const { permission } = usePermissions();
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/groups"}
label={"Groups"}
icon={<FolderGit2Icon size={14} />}
active
/>
</Breadcrumbs>
<h1 ref={headingRef}>Groups</h1>
<Paragraph>
Here is the overview of the groups of your organization. You can
delete the unused ones.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/manage-network-access"}
target={"_blank"}
>
Groups
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess hasAccess={permission.groups.read} page={"Groups"}>
<Suspense fallback={<SkeletonTable />}>
<GroupsTable headingTarget={portalTarget} />
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+13 -1
View File
@@ -7,7 +7,7 @@ import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import { ArrowUpRightIcon, ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeersProvider from "@/contexts/PeersProvider";
@@ -16,6 +16,7 @@ import RoutesProvider from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route";
import PageContainer from "@/layouts/PageContainer";
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
import { Callout } from "@components/Callout";
const NetworkRoutesTable = lazy(
() => import("@/modules/route-group/NetworkRoutesTable"),
@@ -59,6 +60,17 @@ export default function NetworkRoutes() {
</InlineLink>
in our documentation.
</Paragraph>
<Callout className={"max-w-xl mt-3"} variant={"warning"}>
<span>
We recommend using the new Networks concept to easier visualise
and manage access to your resources.{" "}
<InlineLink href={"/networks"}>
Go to Networks
<ArrowUpRightIcon size={14} />
</InlineLink>
</span>
</Callout>
</div>
<RestrictedAccess hasAccess={permission.routes.read}>
+83 -32
View File
@@ -1,7 +1,15 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import Card from "@components/Card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/DropdownMenu";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import Separator from "@components/Separator";
@@ -12,12 +20,14 @@ import { cn } from "@utils/helpers";
import {
ArrowUpRightIcon,
HelpCircle,
MoreVertical,
PencilLineIcon,
ServerIcon,
ShieldCheckIcon,
ShieldXIcon,
Trash2,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
@@ -25,8 +35,10 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
import NetworkModal from "@/modules/networks/NetworkModal";
import { NetworkProvider } from "@/modules/networks/NetworkProvider";
import {
NetworkProvider,
useNetworksContext,
} from "@/modules/networks/NetworkProvider";
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
@@ -77,35 +89,24 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center",
!network.description && "gap-2",
)}
className={"w-full lg:w-1/2 flex justify-between items-center"}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
{permission.networks.update && (
<button
className={
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
}
onClick={() => setNetworkModal(true)}
>
<PencilLineIcon size={18} />
</button>
)}
<NetworkModal
open={networkModal}
setOpen={setNetworkModal}
onUpdated={() => {
mutate(`/networks/${network.id}`);
}}
network={network}
/>
<div
className={cn(
"flex items-center w-full",
!network.description && "gap-2",
)}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
</div>
<NetworkProvider network={network}>
<NetworkActions />
</NetworkProvider>
</div>
</div>
@@ -124,6 +125,56 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
);
}
function NetworkActions() {
const { permission } = usePermissions();
const { deleteNetwork, openEditNetworkModal, network } = useNetworksContext();
const router = useRouter();
if (!network) return;
return (
<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={() => openEditNetworkModal(network)}
disabled={!permission.networks.update}
>
<div className={"flex gap-3 items-center"}>
<PencilLineIcon size={14} className={"shrink-0"} />
Rename
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
deleteNetwork(network).then(() => router.push("/networks"))
}
variant={"danger"}
disabled={!permission.networks.delete}
>
<div className={"flex gap-3 items-center"}>
<Trash2 size={14} className={"shrink-0"} />
Delete
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const isHighlyAvailable = !!(
network?.routing_peers_count && network?.routing_peers_count >= 2
@@ -154,7 +205,7 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const policyCount = network.policies?.length ?? 0;
return (
<Card>
<Card className={"w-full lg:w-1/2"}>
<Card.List>
<Card.ListItem
tooltip={false}
Loaded 20 of 270 files, more files were not shown because too many files have changed in this diff. Show more