Compare commits

...
Author SHA1 Message Date
Eduard Gert 41dfccca76 Add description for network resource 2025-01-22 12:12:09 +01:00
Eduard GertandMisha Bragin 25be69e7bb Add improvements to new networks features (#439)
* Fix wrong ui state for routing peer modal in networks

* Add confirmation dialog when blocking users

* Keep peer sort order when switching pages

* Update sidebar navigation order and remove deprecation notice

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

* Update group text in network resource modal

* Update networks page text

* Fix line height

* Add search to resource table

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

* Add enabled toggle to routing peers

* Add enabled toggle to network resources

* Add resource group modal and adjust tables

* Clarify networks

* Fix not properly aligned horizontal scroll bar

* Add option to install netbird after creating a setup key

* Fix text for install netbird modal

* Show resources count in group settings

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

* Fix wording

* Fix resource policy count

* Hide resource count when selection source groups

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

* Add option for horizontal stepper

* Generate setup key when installing netbird from routing peer modal

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

---------

Co-authored-by: Misha Bragin <bangvalo@gmail.com>
2025-01-20 16:18:21 +01:00
Eduard Gert 43e5d5cf53 Fix activity search and allow searching for meta fields (#440) 2025-01-15 16:41:55 +01:00
Eduard Gert 18819d6fdf Add confirmation dialog when blocking users (#437) 2025-01-15 16:29:05 +01:00
Eduard Gert 158804c1ac Fix wrong ui state for routing peer modal in networks (#436) 2025-01-15 16:28:50 +01:00
Misha Bragin 14d2d68819 Update links to networks doc (#435) 2024-12-27 21:48:36 +01:00
Pascal Fischer 40902b3629 add resources to groups update operation (#434) 2024-12-27 14:20:11 +01:00
Pascal Fischer fa9bcea4ab Update links for networks concept (#433) 2024-12-23 18:08:06 +01:00
Eduard Gert 3ba7acdecf Add new networks feature (#427) 2024-12-23 13:20:01 +03:00
Eduard Gert c7775ade8c Hide groups for regular users (#423) 2024-11-20 17:50:28 +01:00
Maycon Santos cd3e75b640 Add setup-key improvements (#420)
- Add support to key deletion
- Add custom and unlimited expiration
2024-11-01 16:04:43 +01:00
Jon "The Nice Guy" Spriggs f8281c8057 Typo (#418)
Protocol appears to include the : delimiter
2024-10-22 11:19:16 +02:00
Eduard Gert c1fcadaefe Fix resetting acl groups on switching active toggle (#417) 2024-10-07 17:31:42 +02:00
Jon "The Nice Guy" SpriggsandEduard Gert a0c4520f4b Add admin-url to the add-peer dialogue (#416)
* Add admin-url to the add-peer dialogue

* Missed "let" from defining the variable

* Update netbird.ts

Fix isNetBirdHosted check

---------

Co-authored-by: Eduard Gert <eduard@netbird.io>
2024-10-07 17:25:03 +02:00
Eduard Gert 76ef50a886 Add Access Control Groups & various UI / UX improvements (#415)
* Update codespell

* Add access control group, add various ui / ux improvements
2024-10-04 19:54:49 +02:00
Maycon Santos 58cec8fcd1 ignore mappin spelling (#408) 2024-08-13 15:59:38 +02:00
Eduard Gert d34ae9beb2 Sync changes with netbird cloud (#407)
* Update axa oidc library and package.json

* Update ACL port state to show correct value

* Filter user groups by unique groups only

* Add peer multiselect, optimize dropdown performance for peer selection, remove 'all' group from some dropdowns, various ui / ux optimizations

* Add peer multiselect, optimize dropdown performance for peer selection, remove 'all' group from some dropdowns, various ui / ux optimizations
2024-08-13 15:51:22 +02:00
Eduard Gert 650496f670 Include all settings in put request to prevent overwrite (#405) 2024-07-31 18:48:59 +02:00
Tom Hubrecht 121778c4a6 Fix package-lock.json (#401) 2024-07-12 10:35:31 +02:00
juliaroesschen d4102c5d04 fix typo in route update modal (#397) 2024-07-04 15:05:57 +02:00
pascal-fischer e78c35bdbe Fix DNS modal to allow one char domains (#393)
* update regex to allow one char domains in DNS routing modal

* update regex
2024-07-04 10:50:37 +02:00
juliaroesschen 6ebee98695 Fix typo in Network Routes dialogue (#395) 2024-07-04 10:48:49 +02:00
juliaroesschen f4b28d5f40 Fix typo in routes modal 2024-06-28 11:38:39 +02:00
Eduard Gert b4b6d9295b Add DNS routes (#390) 2024-06-17 09:32:55 +02:00
Maycon Santos 4898742ee9 Fix http://localhost:3000/ url validation case (#388)
* Fix http://localhost:3000/ url validation case

* adjust min regex occurrences
2024-06-12 18:18:14 +02:00
Eduard Gert 79164e9dd5 Add process posture check (#378)
* Add process posture check

* Add support for separate linux and mac paths
2024-06-12 16:32:10 +02:00
Eduard Gert 5caeab118b UX changes for modals and refactoring (#380) 2024-05-08 14:42:04 +02:00
Eduard Gert 3f943bb7d4 Use next/font/local instead of next/font/google (#376) 2024-04-19 17:12:56 +02:00
Eduard Gert 96b939e6cc Add changes from cloud repo to public one (#377)
* Remove unused files

* Update activity descriptions

* Update SelectDropdown

* Update redirect logic for / page

* Update HelpText.tsx

* Update wording for exit nodes
2024-04-19 17:12:37 +02:00
Eduard Gert 5e13548b81 Add better input validation for setup-keys, nameserver and routes (#373)
* Return the correct promise for errors

* Update icon

* Add better validation for routes

* Add better validation for DNS

* Add better validation for setup keys

* Merge exit nodes to input validation
2024-04-17 15:27:21 +02:00
Eduard Gert 2272a1d2a4 Add Exit Nodes (#374)
* Add exit node feature

* Fix spelling

* Hide masquerade for exit nodes

* Add exit node information to peers list

* Change exit node button, add indicator to peers table

* Add steps to route modal

* Add hook to check if peer has exit nodes

* Hide exit node indicator for regular users

* Add documentation links
2024-04-17 13:11:38 +02:00
Eduard Gert fc3da50346 Add fallbacks for setup key name & setup key group names (#370)
* Add try catch block for global search

* Add fallback for group name

* Add fallback for setup key name

* Do not load setup key modal if it's not open

* Check if auto_groups actually exists for the setup keys

* Add fallback for group names in setup keys table

* Add fallback for group names in peers table
2024-04-11 16:42:27 +02:00
253 changed files with 13356 additions and 2784 deletions

No files matched your search

+2 -1
View File
@@ -12,4 +12,5 @@ jobs:
uses: codespell-project/actions-codespell@v2 uses: codespell-project/actions-codespell@v2
with: with:
only_warn: 1 only_warn: 1
skip: package-lock.json,*.svg skip: package-lock.json,*.svg
ignore_words_list: mappin, allTime
+3
View File
@@ -5,6 +5,9 @@ const nextConfig = {
unoptimized: true, unoptimized: true,
}, },
reactStrictMode: false, reactStrictMode: false,
env: {
APP_ENV: process.env.APP_ENV || "production",
},
}; };
module.exports = nextConfig; module.exports = nextConfig;
+1154 -286
View File
File diff suppressed because it is too large. Load diff
+9 -5
View File
@@ -13,17 +13,19 @@
"cypress:open": "cypress open" "cypress:open": "cypress open"
}, },
"dependencies": { "dependencies": {
"@axa-fr/react-oidc": "^5.14.0", "@axa-fr/react-oidc": "^7.22.18",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@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-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
@@ -37,6 +39,7 @@
"@types/node": "20.10.6", "@types/node": "20.10.6",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-window": "^1.8.8",
"autoprefixer": "^10", "autoprefixer": "^10",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
@@ -53,8 +56,8 @@
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0", "ip-cidr": "^3.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.287.0", "lucide-react": "^0.460.0",
"next": "13.5.5", "next": "13.5.7",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"punycode": "^2.3.1", "punycode": "^2.3.1",
"react": "^18", "react": "^18",
@@ -67,13 +70,14 @@
"react-jwt": "^1.2.0", "react-jwt": "^1.2.0",
"react-loading-skeleton": "^3.3.1", "react-loading-skeleton": "^3.3.1",
"react-responsive": "^9.0.2", "react-responsive": "^9.0.2",
"react-virtuoso": "^4.9.0",
"swr": "^2.2.4", "swr": "^2.2.4",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5" "typescript": "^5"
}, },
"devDependencies": { "devDependencies": {
"cypress": "^13.3.3", "cypress": "^13.13.0",
"postcss": "^8", "postcss": "^8",
"prettier": "3.0.3", "prettier": "3.0.3",
"tailwindcss": "^3" "tailwindcss": "^3"
+10 -7
View File
@@ -5,6 +5,7 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable"; import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess"; import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense } from "react";
@@ -20,6 +21,9 @@ const AccessControlTable = lazy(
export default function AccessControlPage() { export default function AccessControlPage() {
const { data: policies, isLoading } = useFetchApi<Policy[]>("/policies"); const { data: policies, isLoading } = useFetchApi<Policy[]>("/policies");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return ( return (
<PageContainer> <PageContainer>
<GroupsProvider> <GroupsProvider>
@@ -31,12 +35,7 @@ export default function AccessControlPage() {
icon={<AccessControlIcon size={14} />} icon={<AccessControlIcon size={14} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1 ref={headingRef}>Access Control Policies</h1>
<h1>
{policies && policies.length > 1
? `${policies.length} Access Control Policies`
: "Access Control Policies"}
</h1>
<Paragraph> <Paragraph>
Create rules to manage access in your network and define what peers Create rules to manage access in your network and define what peers
can connect. can connect.
@@ -57,7 +56,11 @@ export default function AccessControlPage() {
<RestrictedAccess page={"Access Control"}> <RestrictedAccess page={"Access Control"}>
<PoliciesProvider> <PoliciesProvider>
<Suspense fallback={<SkeletonTable />}> <Suspense fallback={<SkeletonTable />}>
<AccessControlTable isLoading={isLoading} policies={policies} /> <AccessControlTable
isLoading={isLoading}
policies={policies}
headingTarget={portalTarget}
/>
</Suspense> </Suspense>
</PoliciesProvider> </PoliciesProvider>
</RestrictedAccess> </RestrictedAccess>
+10 -6
View File
@@ -4,6 +4,7 @@ import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink"; import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import { RestrictedAccess } from "@components/ui/RestrictedAccess"; import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import React from "react"; import React from "react";
@@ -15,6 +16,9 @@ import ActivityTable from "@/modules/activity/ActivityTable";
export default function Activity() { export default function Activity() {
const { data: events, isLoading } = useFetchApi<ActivityEvent[]>("/events"); const { data: events, isLoading } = useFetchApi<ActivityEvent[]>("/events");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return ( return (
<PageContainer> <PageContainer>
<div className={"p-default py-6"}> <div className={"p-default py-6"}>
@@ -25,11 +29,7 @@ export default function Activity() {
icon={<ActivityIcon size={13} />} icon={<ActivityIcon size={13} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1> <h1 ref={headingRef}>Activity Events</h1>
{events && events.length > 1
? `${events.length} Activity Events`
: "Activity Events"}
</h1>
<Paragraph> <Paragraph>
Here you can see all the account and network activity events. Here you can see all the account and network activity events.
</Paragraph> </Paragraph>
@@ -48,7 +48,11 @@ export default function Activity() {
</Paragraph> </Paragraph>
</div> </div>
<RestrictedAccess page={"Activity"}> <RestrictedAccess page={"Activity"}>
<ActivityTable events={events} isLoading={isLoading} /> <ActivityTable
events={events}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</RestrictedAccess> </RestrictedAccess>
</PageContainer> </PageContainer>
); );
+6 -5
View File
@@ -5,6 +5,7 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable"; import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess"; import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import { ExternalLinkIcon, ServerIcon } from "lucide-react"; import { ExternalLinkIcon, ServerIcon } from "lucide-react";
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense } from "react";
@@ -20,6 +21,9 @@ export default function NameServers() {
const { data: nameserverGroups, isLoading } = const { data: nameserverGroups, isLoading } =
useFetchApi<NameserverGroup[]>("/dns/nameservers"); useFetchApi<NameserverGroup[]>("/dns/nameservers");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return ( return (
<PageContainer> <PageContainer>
<div className={"p-default py-6"}> <div className={"p-default py-6"}>
@@ -36,11 +40,7 @@ export default function NameServers() {
icon={<ServerIcon size={13} />} icon={<ServerIcon size={13} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1> <h1 ref={headingRef}>Nameservers</h1>
{nameserverGroups && nameserverGroups.length > 1
? `${nameserverGroups.length} Nameservers`
: "Nameservers"}
</h1>
<Paragraph> <Paragraph>
Add nameservers for domain name resolution in your NetBird network. Add nameservers for domain name resolution in your NetBird network.
</Paragraph> </Paragraph>
@@ -62,6 +62,7 @@ export default function NameServers() {
<NameserverGroupTable <NameserverGroupTable
nameserverGroups={nameserverGroups} nameserverGroups={nameserverGroups}
isLoading={isLoading} isLoading={isLoading}
headingTarget={portalTarget}
/> />
</Suspense> </Suspense>
</RestrictedAccess> </RestrictedAccess>
+22 -7
View File
@@ -14,17 +14,24 @@ import { IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api"; import useFetchApi, { useApiCall } from "@utils/api";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import React from "react"; import React from "react";
import Skeleton from "react-loading-skeleton";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import DNSIcon from "@/assets/icons/DNSIcon"; import DNSIcon from "@/assets/icons/DNSIcon";
import { useHasChanges } from "@/hooks/useHasChanges"; import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { NameserverSettings } from "@/interfaces/NameserverSettings"; import { NameserverSettings } from "@/interfaces/NameserverSettings";
import PageContainer from "@/layouts/PageContainer"; import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper"; import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
export default function NameServerSettings() { export default function NameServerSettings() {
const { data: settings, isLoading } = const { data: settings, isLoading } =
useFetchApi<NameserverSettings>("/dns/settings"); useFetchApi<NameserverSettings>("/dns/settings");
const initialDNSGroups = useGroupIdsToGroups(
settings?.disabled_management_groups,
);
return ( return (
<PageContainer> <PageContainer>
<div className={"p-default py-6"}> <div className={"p-default py-6"}>
@@ -55,10 +62,16 @@ export default function NameServerSettings() {
in our documentation. in our documentation.
</Paragraph> </Paragraph>
<RestrictedAccess page={"DNS Settings"}> <RestrictedAccess page={"DNS Settings"}>
{!isLoading && ( {!isLoading && initialDNSGroups !== undefined ? (
<SettingDisabledManagementGroups <SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
initial={settings?.disabled_management_groups} ) : (
/> <div>
<Skeleton
width={"100%"}
className={"mt-8 max-w-xl"}
height={240}
/>
</div>
)} )}
</RestrictedAccess> </RestrictedAccess>
</div> </div>
@@ -67,16 +80,16 @@ export default function NameServerSettings() {
} }
const SettingDisabledManagementGroups = ({ const SettingDisabledManagementGroups = ({
initial, initialGroups,
}: { }: {
initial: string[] | undefined; initialGroups: Group[];
}) => { }) => {
const settingRequest = useApiCall<NameserverSettings>("/dns/settings"); const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [selectedGroups, setSelectedGroups, { save: saveGroups }] = const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({ useGroupHelper({
initial: initial || [], initial: initialGroups,
}); });
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([ const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
@@ -108,6 +121,7 @@ const SettingDisabledManagementGroups = ({
Peers in these groups will require manual domain name resolution Peers in these groups will require manual domain name resolution
</HelpText> </HelpText>
<PeerGroupSelector <PeerGroupSelector
dataCy={"dns-groups-selector"}
onChange={setSelectedGroups} onChange={setSelectedGroups}
values={selectedGroups} values={selectedGroups}
/> />
@@ -122,6 +136,7 @@ const SettingDisabledManagementGroups = ({
size={"sm"} size={"sm"}
onClick={saveSettings} onClick={saveSettings}
disabled={!hasChanges} disabled={!hasChanges}
data-cy={"save-changes"}
> >
Save Changes Save Changes
</Button> </Button>
+6 -5
View File
@@ -5,6 +5,7 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable"; import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess"; import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense } from "react";
@@ -23,6 +24,9 @@ export default function NetworkRoutes() {
const { data: routes, isLoading } = useFetchApi<Route[]>("/routes"); const { data: routes, isLoading } = useFetchApi<Route[]>("/routes");
const groupedRoutes = useGroupedRoutes({ routes }); const groupedRoutes = useGroupedRoutes({ routes });
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return ( return (
<PageContainer> <PageContainer>
<RoutesProvider> <RoutesProvider>
@@ -35,11 +39,7 @@ export default function NetworkRoutes() {
icon={<NetworkRoutesIcon size={13} />} icon={<NetworkRoutesIcon size={13} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1> <h1 ref={headingRef}>Network Routes</h1>
{groupedRoutes && groupedRoutes.length > 1
? `${groupedRoutes.length} Network Routes`
: "Network Routes"}
</h1>
<Paragraph> <Paragraph>
Network routes allow you to access other networks like LANs and Network routes allow you to access other networks like LANs and
VPCs without installing NetBird on every resource. VPCs without installing NetBird on every resource.
@@ -65,6 +65,7 @@ export default function NetworkRoutes() {
isLoading={isLoading} isLoading={isLoading}
groupedRoutes={groupedRoutes} groupedRoutes={groupedRoutes}
routes={routes} routes={routes}
headingTarget={portalTarget}
/> />
</Suspense> </Suspense>
</RestrictedAccess> </RestrictedAccess>
+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: `Network - Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+229
View File
@@ -0,0 +1,229 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Card from "@components/Card";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import {
ArrowUpRightIcon,
HelpCircle,
PencilLineIcon,
ServerIcon,
ShieldCheckIcon,
ShieldXIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
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 { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
export default function NetworkDetailPage() {
const queryParameter = useSearchParams();
const networkId = queryParameter.get("id");
const { data: network, isLoading } = useFetchApi<Network>(
`/networks/${networkId}`,
true,
);
useRedirect("/networks", false, !networkId);
return network && !isLoading ? (
<NetworkOverview network={network} />
) : (
<FullScreenLoading />
);
}
function NetworkOverview({ network }: Readonly<{ network: Network }>) {
const { isUser } = useLoggedInUser();
const [networkModal, setNetworkModal] = useState(false);
const { mutate } = useSWRConfig();
const isActive = !!(
network?.routing_peers_count && network.routing_peers_count > 0
);
return (
<PageContainer>
<NetworkProvider network={network}>
<div className={"p-default py-6 mb-4"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={isUser}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center",
!network.description && "gap-2",
)}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
<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>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<Separator />
<ResourcesSection network={network} />
<div className={"h-3"} />
<Separator />
<NetworkRoutingPeersSection network={network} />
</NetworkProvider>
</PageContainer>
);
}
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const isHighlyAvailable = !!(
network?.routing_peers_count && network?.routing_peers_count >= 2
);
const disabledText = useMemo(
() => (
<>
High availability is currently{" "}
<span className={"text-yellow-400 font-medium"}>inactive</span> for this
network.
</>
),
[],
);
const enabledText = useMemo(
() => (
<>
High availability is{" "}
<span className={"text-green-500 font-medium"}>active</span> for this
network.
</>
),
[],
);
const policyCount = network.policies?.length ?? 0;
return (
<Card>
<Card.List>
<Card.ListItem
tooltip={false}
label={
<>
<ServerIcon size={16} />
High Availability
</>
}
value={
<FullTooltip
interactive={false}
content={
<div className={"max-w-xs text-xs"}>
{isHighlyAvailable ? enabledText : disabledText}
{isHighlyAvailable ? (
<div className={"inline-flex mt-2"}>
You can add more routing peers to increase the
availability of this network.
</div>
) : (
<div className={"inline-flex mt-2"}>
Go ahead and add more routing peers or groups with routing
peers to enable high availability for this network.
</div>
)}
</div>
}
>
<div
className={cn(
"flex gap-2.5 items-center text-nb-gray-300 text-sm cursor-help",
)}
>
<span
className={cn(
"h-2 w-2 rounded-full",
!isHighlyAvailable ? "bg-yellow-400" : "bg-green-500",
)}
></span>
{isHighlyAvailable ? "Active" : "Inactive"}
<HelpCircle size={12} />
</div>
</FullTooltip>
}
/>
<Card.ListItem
tooltip={false}
label={
policyCount > 0 ? (
<>
<ShieldCheckIcon size={16} className={"text-green-500"} />
{policyCount}{" "}
{policyCount === 1 ? "Active Policy" : "Active Policies"}
</>
) : (
<>
<ShieldXIcon size={16} className={"text-red-500"} />
No Active Policies
</>
)
}
value={
policyCount > 0 ? (
<InlineLink href={"/access-control"}>
Go to Policies
<ArrowUpRightIcon size={14} />
</InlineLink>
) : null
}
/>
</Card.List>
</Card>
);
}
+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: `Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+61
View File
@@ -0,0 +1,61 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
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 React, { Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import NetworksTable from "@/modules/networks/table/NetworksTable";
export default function Networks() {
const { data: networks, isLoading } = useFetchApi<Network[]>("/networks");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Networks</h1>
<Paragraph>
Networks allow you to access internal resources in LANs and VPCs without
installing NetBird on every machine.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/networks"}
target={"_blank"}
>
Networks
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess>
<Suspense fallback={<SkeletonTable />}>
<NetworksTable
data={networks}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+30 -52
View File
@@ -23,6 +23,8 @@ import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading"; import FullScreenLoading from "@components/ui/FullScreenLoading";
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge"; import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
import TextWithTooltip from "@components/ui/TextWithTooltip"; import TextWithTooltip from "@components/ui/TextWithTooltip";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import useRedirect from "@hooks/useRedirect";
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react"; import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -53,20 +55,22 @@ import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
import RoutesProvider from "@/contexts/RoutesProvider"; import RoutesProvider from "@/contexts/RoutesProvider";
import { useLoggedInUser } from "@/contexts/UsersProvider"; import { useLoggedInUser } from "@/contexts/UsersProvider";
import { useHasChanges } from "@/hooks/useHasChanges"; import { useHasChanges } from "@/hooks/useHasChanges";
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
import { OperatingSystem } from "@/interfaces/OperatingSystem"; import { OperatingSystem } from "@/interfaces/OperatingSystem";
import type { Peer } from "@/interfaces/Peer"; import type { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer"; import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper"; import useGroupHelper from "@/modules/groups/useGroupHelper";
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton"; import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable"; import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
export default function PeerPage() { export default function PeerPage() {
const queryParameter = useSearchParams(); const queryParameter = useSearchParams();
const peerId = queryParameter.get("id"); const peerId = queryParameter.get("id");
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId); const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);
return peer ? (
<PeerProvider peer={peer}> useRedirect("/peers", false, !peerId);
return peer && !isLoading ? (
<PeerProvider peer={peer} key={peerId}>
<PeerOverview /> <PeerOverview />
</PeerProvider> </PeerProvider>
) : ( ) : (
@@ -293,64 +297,38 @@ function PeerOverview() {
/> />
</FullTooltip> </FullTooltip>
<div> {!isUser && (
<Label>Assigned Groups</Label> <div>
<HelpText> <Label>Assigned Groups</Label>
Use groups to control what this peer can access. <HelpText>
</HelpText> Use groups to control what this peer can access.
<FullTooltip </HelpText>
content={
<div
className={
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
<LockIcon size={14} />
<span>
{`You don't have the required permissions to update this
setting.`}
</span>
</div>
}
interactive={false}
className={"w-full block"}
disabled={!isUser}
>
<PeerGroupSelector <PeerGroupSelector
disabled={isUser} disabled={isUser}
onChange={setSelectedGroups} onChange={setSelectedGroups}
values={selectedGroups} values={selectedGroups}
hideAllGroup={true}
peer={peer} peer={peer}
/> />
</FullTooltip> </div>
</div> )}
</div> </div>
</div> </div>
</div> </div>
<Separator />
{isLinux && !isUser ? ( {isLinux && !isUser ? (
<div className={"px-8 py-6"}> <>
<div className={"max-w-6xl"}> <Separator />
<div className={"flex justify-between items-center"}> <PeerNetworkRoutesSection peer={peer} />
<div> </>
<h2>Network Routes</h2>
<Paragraph>
Access other networks without installing NetBird on every
resource.
</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div>
<AddRouteDropdownButton />
</div>
</div>
</div>
<PeerRoutesTable peer={peer} />
</div>
</div>
) : null} ) : null}
{peer?.id && (
<>
<Separator />
<AccessiblePeersSection peerID={peer.id} />
</>
)}
</RoutesProvider> </RoutesProvider>
</PageContainer> </PageContainer>
); );
+16 -15
View File
@@ -4,13 +4,12 @@ import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink"; import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable"; import SkeletonTable from "@components/skeletons/SkeletonTable";
import useFetchApi from "@utils/api"; import { usePortalElement } from "@hooks/usePortalElement";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense, useEffect } from "react"; import React, { lazy, Suspense } from "react";
import PeerIcon from "@/assets/icons/PeerIcon"; import PeerIcon from "@/assets/icons/PeerIcon";
import { useGroups } from "@/contexts/GroupsProvider"; import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
import { useLoggedInUser, useUsers } from "@/contexts/UsersProvider"; import { useLoggedInUser, useUsers } from "@/contexts/UsersProvider";
import { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer"; import PageContainer from "@/layouts/PageContainer";
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal"; import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
@@ -21,24 +20,22 @@ export default function Peers() {
return ( return (
<PageContainer> <PageContainer>
{permission?.dashboard_view === "blocked" ? ( {permission.dashboard_view === "blocked" ? (
<PeersBlockedView /> <PeersBlockedView />
) : ( ) : (
<PeersView /> <PeersProvider>
<PeersView />
</PeersProvider>
)} )}
</PageContainer> </PageContainer>
); );
} }
function PeersView() { function PeersView() {
const { data: peers, isLoading } = useFetchApi<Peer[]>("/peers"); const { peers, isLoading } = usePeers();
const { users } = useUsers(); const { users } = useUsers();
const { refresh } = useGroups(); const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
useEffect(() => {
refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const peersWithUser = peers?.map((peer) => { const peersWithUser = peers?.map((peer) => {
if (!users) return peer; if (!users) return peer;
@@ -58,7 +55,7 @@ function PeersView() {
icon={<PeerIcon size={13} />} icon={<PeerIcon size={13} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1>{peers && peers.length > 1 ? `${peers.length} Peers` : "Peers"}</h1> <h1 ref={headingRef}>Peers</h1>
<Paragraph> <Paragraph>
A list of all machines and devices connected to your private network. A list of all machines and devices connected to your private network.
Use this view to manage peers. Use this view to manage peers.
@@ -76,7 +73,11 @@ function PeersView() {
</Paragraph> </Paragraph>
</div> </div>
<Suspense fallback={<SkeletonTable />}> <Suspense fallback={<SkeletonTable />}>
<PeersTable isLoading={isLoading} peers={peersWithUser} /> <PeersTable
isLoading={isLoading}
peers={peersWithUser}
headingTarget={portalTarget}
/>
</Suspense> </Suspense>
</> </>
); );
+10 -6
View File
@@ -5,6 +5,7 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable"; import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess"; import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import { ExternalLinkIcon, ShieldCheck } from "lucide-react"; import { ExternalLinkIcon, ShieldCheck } from "lucide-react";
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense } from "react";
@@ -21,6 +22,9 @@ export default function PostureChecksPage() {
const { data: postureChecks, isLoading } = const { data: postureChecks, isLoading } =
useFetchApi<PostureCheck[]>("/posture-checks"); useFetchApi<PostureCheck[]>("/posture-checks");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return ( return (
<PageContainer> <PageContainer>
<GroupsProvider> <GroupsProvider>
@@ -38,17 +42,16 @@ export default function PostureChecksPage() {
icon={<ShieldCheck size={15} />} icon={<ShieldCheck size={15} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1> <h1 ref={headingRef}>Posture Checks</h1>
{postureChecks && postureChecks.length > 1
? `${postureChecks.length} Posture Checks`
: "Posture Checks"}
</h1>
<Paragraph> <Paragraph>
Use posture checks to further restrict access in your network. Use posture checks to further restrict access in your network.
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
Learn more about Learn more about
<InlineLink href={"https://docs.netbird.io/how-to/manage-posture-checks"} target={"_blank"}> <InlineLink
href={"https://docs.netbird.io/how-to/manage-posture-checks"}
target={"_blank"}
>
Posture Checks Posture Checks
<ExternalLinkIcon size={12} /> <ExternalLinkIcon size={12} />
</InlineLink> </InlineLink>
@@ -60,6 +63,7 @@ export default function PostureChecksPage() {
<PoliciesProvider> <PoliciesProvider>
<Suspense fallback={<SkeletonTable />}> <Suspense fallback={<SkeletonTable />}>
<PostureCheckTable <PostureCheckTable
headingTarget={portalTarget}
isLoading={isLoading} isLoading={isLoading}
postureChecks={postureChecks} postureChecks={postureChecks}
/> />
+7
View File
@@ -6,6 +6,7 @@ import {
AlertOctagonIcon, AlertOctagonIcon,
FolderGit2Icon, FolderGit2Icon,
LockIcon, LockIcon,
NetworkIcon,
ShieldIcon, ShieldIcon,
} from "lucide-react"; } from "lucide-react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
@@ -16,6 +17,7 @@ import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab"; import AuthenticationTab from "@/modules/settings/AuthenticationTab";
import DangerZoneTab from "@/modules/settings/DangerZoneTab"; import DangerZoneTab from "@/modules/settings/DangerZoneTab";
import GroupsTab from "@/modules/settings/GroupsTab"; import GroupsTab from "@/modules/settings/GroupsTab";
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab"; import PermissionsTab from "@/modules/settings/PermissionsTab";
export default function NetBirdSettings() { export default function NetBirdSettings() {
@@ -47,6 +49,10 @@ export default function NetBirdSettings() {
<LockIcon size={14} /> <LockIcon size={14} />
Permissions Permissions
</VerticalTabs.Trigger> </VerticalTabs.Trigger>
<VerticalTabs.Trigger value="networks">
<NetworkIcon size={14} />
Networks
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}> <VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
<AlertOctagonIcon size={14} /> <AlertOctagonIcon size={14} />
Danger zone Danger zone
@@ -57,6 +63,7 @@ export default function NetBirdSettings() {
{account && <AuthenticationTab account={account} />} {account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />} {account && <PermissionsTab account={account} />}
{account && <GroupsTab account={account} />} {account && <GroupsTab account={account} />}
{account && <NetworkSettingsTab account={account} />}
{account && <DangerZoneTab account={account} />} {account && <DangerZoneTab account={account} />}
</div> </div>
</RestrictedAccess> </RestrictedAccess>
+22 -16
View File
@@ -5,9 +5,10 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable"; import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess"; import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense, useMemo } from "react";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon"; import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import { useGroups } from "@/contexts/GroupsProvider"; import { useGroups } from "@/contexts/GroupsProvider";
import { Group } from "@/interfaces/Group"; import { Group } from "@/interfaces/Group";
@@ -22,16 +23,24 @@ export default function SetupKeys() {
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys"); const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
const { groups } = useGroups(); const { groups } = useGroups();
const setupKeysWithGroups = setupKeys?.map((setupKey) => { const setupKeysWithGroups = useMemo(() => {
if (!setupKey.auto_groups) return setupKey; if (!setupKeys) return [];
if (!groups) return setupKey; return setupKeys?.map((setupKey) => {
return { if (!setupKey.auto_groups) return setupKey;
...setupKey, if (!groups) return setupKey;
groups: setupKey.auto_groups.map((group) => { return {
return groups.find((g) => g.id === group) || undefined; ...setupKey,
}) as Group[] | undefined, groups: setupKey.auto_groups
}; ?.map((group) => {
}); return groups.find((g) => g.id === group) || undefined;
})
.filter((group) => group !== undefined) as Group[],
};
});
}, [setupKeys, groups]);
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return ( return (
<PageContainer> <PageContainer>
@@ -43,11 +52,7 @@ export default function SetupKeys() {
icon={<SetupKeysIcon size={13} />} icon={<SetupKeysIcon size={13} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1> <h1 ref={headingRef}>Setup Keys</h1>
{setupKeys && setupKeys.length > 1
? `${setupKeys.length} Setup Keys`
: "Setup Keys"}
</h1>
<Paragraph> <Paragraph>
Setup keys are pre-authentication keys that allow to register new Setup keys are pre-authentication keys that allow to register new
machines in your network. machines in your network.
@@ -69,6 +74,7 @@ export default function SetupKeys() {
<RestrictedAccess page={"Setup Keys"}> <RestrictedAccess page={"Setup Keys"}>
<Suspense fallback={<SkeletonTable />}> <Suspense fallback={<SkeletonTable />}>
<SetupKeysTable <SetupKeysTable
headingTarget={portalTarget}
setupKeys={setupKeysWithGroups} setupKeys={setupKeysWithGroups}
isLoading={isLoading} isLoading={isLoading}
/> />
@@ -5,6 +5,7 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable"; import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess"; import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import { IconSettings2 } from "@tabler/icons-react"; import { IconSettings2 } from "@tabler/icons-react";
import useFetchApi from "@utils/api"; import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
@@ -22,6 +23,9 @@ export default function ServiceUsers() {
"/users?service_user=true", "/users?service_user=true",
); );
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return ( return (
<PageContainer> <PageContainer>
<div className={"p-default py-6"}> <div className={"p-default py-6"}>
@@ -38,11 +42,7 @@ export default function ServiceUsers() {
icon={<IconSettings2 size={17} />} icon={<IconSettings2 size={17} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1> <h1 ref={headingRef}>Service Users</h1>
{users && users.length > 1
? `${users.length} Service Users`
: "Service Users"}
</h1>
<Paragraph> <Paragraph>
Use service users to create API tokens and avoid losing automated Use service users to create API tokens and avoid losing automated
access. access.
@@ -61,7 +61,11 @@ export default function ServiceUsers() {
</div> </div>
<RestrictedAccess page={"Service Users"}> <RestrictedAccess page={"Service Users"}>
<Suspense fallback={<SkeletonTable />}> <Suspense fallback={<SkeletonTable />}>
<ServiceUsersTable users={users} isLoading={isLoading} /> <ServiceUsersTable
users={users}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</Suspense> </Suspense>
</RestrictedAccess> </RestrictedAccess>
</PageContainer> </PageContainer>
+35 -15
View File
@@ -10,6 +10,7 @@ import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector"; import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator"; import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading"; import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react"; import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api"; import useFetchApi, { useApiCall } from "@utils/api";
import { generateColorFromString } from "@utils/helpers"; import { generateColorFromString } from "@utils/helpers";
@@ -21,11 +22,13 @@ import { useSWRConfig } from "swr";
import TeamIcon from "@/assets/icons/TeamIcon"; import TeamIcon from "@/assets/icons/TeamIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider"; import { useLoggedInUser } from "@/contexts/UsersProvider";
import { useHasChanges } from "@/hooks/useHasChanges"; import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { Role, User } from "@/interfaces/User"; import { Role, User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer"; import PageContainer from "@/layouts/PageContainer";
import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable"; import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable";
import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal"; import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal";
import useGroupHelper from "@/modules/groups/useGroupHelper"; import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell"; import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell"; import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector"; import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
@@ -42,8 +45,12 @@ export default function UserPage() {
return users?.find((u) => u.id === userId); return users?.find((u) => u.id === userId);
}, [users, userId]); }, [users, userId]);
return !isLoading && user ? ( useRedirect("/team/users", false, !userId);
<UserOverview user={user} />
const userGroups = useGroupIdsToGroups(user?.auto_groups);
return !isLoading && user && userGroups !== undefined ? (
<UserOverview user={user} initialGroups={userGroups} />
) : ( ) : (
<FullScreenLoading /> <FullScreenLoading />
); );
@@ -51,16 +58,16 @@ export default function UserPage() {
type Props = { type Props = {
user: User; user: User;
initialGroups: Group[];
}; };
function UserOverview({ user }: Props) { function UserOverview({ user, initialGroups }: Readonly<Props>) {
const router = useRouter(); const router = useRouter();
const userRequest = useApiCall<User>("/users"); const userRequest = useApiCall<User>("/users");
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser(); const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false; const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
const initialGroups = user.auto_groups;
const [selectedGroups, setSelectedGroups, { save: saveGroups }] = const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({ useGroupHelper({
initial: initialGroups, initial: initialGroups,
@@ -177,6 +184,7 @@ function UserOverview({ user }: Props) {
className={"w-full"} className={"w-full"}
disabled={!hasChanges} disabled={!hasChanges}
onClick={save} onClick={save}
data-cy={"save-changes"}
> >
Save Changes Save Changes
</Button> </Button>
@@ -184,7 +192,7 @@ function UserOverview({ user }: Props) {
)} )}
</div> </div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl"}> <div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<UserInformationCard user={user} /> <UserInformationCard user={user} />
<div className={"flex flex-col gap-8 w-1/2 "}> <div className={"flex flex-col gap-8 w-1/2 "}>
{!user.is_service_user && ( {!user.is_service_user && (
@@ -197,6 +205,8 @@ function UserOverview({ user }: Props) {
disabled={isUser} disabled={isUser}
onChange={setSelectedGroups} onChange={setSelectedGroups}
values={selectedGroups} values={selectedGroups}
hideAllGroup={true}
dataCy={"user-group-selector"}
/> />
</div> </div>
)} )}
@@ -211,6 +221,8 @@ function UserOverview({ user }: Props) {
<UserRoleSelector <UserRoleSelector
value={role} value={role}
onChange={setRole} onChange={setRole}
hideOwner={user.is_service_user}
currentUser={user}
disabled={ disabled={
isLoggedInUser || isLoggedInUser ||
!isOwnerOrAdmin || !isOwnerOrAdmin ||
@@ -238,7 +250,10 @@ function UserOverview({ user }: Props) {
<div className={"inline-flex gap-4 justify-end"}> <div className={"inline-flex gap-4 justify-end"}>
<div> <div>
<CreateAccessTokenModal user={user}> <CreateAccessTokenModal user={user}>
<Button variant={"primary"}> <Button
variant={"primary"}
data-cy={"access-token-open-modal"}
>
<IconCirclePlus size={16} /> <IconCirclePlus size={16} />
Create Access Token Create Access Token
</Button> </Button>
@@ -287,6 +302,7 @@ function UserInformationCard({ user }: { user: User }) {
)} )}
<Card.ListItem <Card.ListItem
tooltip={false}
label={ label={
<> <>
<GalleryHorizontalEnd size={16} /> <GalleryHorizontalEnd size={16} />
@@ -298,15 +314,19 @@ function UserInformationCard({ user }: { user: User }) {
{!isServiceUser && ( {!isServiceUser && (
<> <>
<Card.ListItem {!user.is_current && user.role != Role.Owner && (
label={ <Card.ListItem
<> tooltip={false}
<Ban size={16} /> label={
Block User <>
</> <Ban size={16} />
} Block User
value={<UserBlockCell user={user} isUserPage={true} />} </>
/> }
value={<UserBlockCell user={user} isUserPage={true} />}
/>
)}
<Card.ListItem <Card.ListItem
label={ label={
<> <>
Loaded 20 of 253 files, more files were not shown because too many files have changed in this diff. Show more