mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e553f3c83 |
No files matched your search
@@ -12,5 +12,4 @@ 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
|
|
||||||
Generated
+286
-807
File diff suppressed because it is too large.
Load diff
+5
-8
@@ -13,7 +13,7 @@
|
|||||||
"cypress:open": "cypress open"
|
"cypress:open": "cypress open"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@axa-fr/react-oidc": "^7.22.18",
|
"@axa-fr/react-oidc": "^5.14.0",
|
||||||
"@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",
|
||||||
@@ -22,9 +22,8 @@
|
|||||||
"@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.1.0",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@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",
|
||||||
@@ -38,7 +37,6 @@
|
|||||||
"@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",
|
||||||
@@ -55,8 +53,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.460.0",
|
"lucide-react": "^0.287.0",
|
||||||
"next": "13.5.7",
|
"next": "13.5.5",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"punycode": "^2.3.1",
|
"punycode": "^2.3.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
@@ -69,14 +67,13 @@
|
|||||||
"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.13.0",
|
"cypress": "^13.3.3",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"tailwindcss": "^3"
|
"tailwindcss": "^3"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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";
|
||||||
@@ -21,9 +20,6 @@ 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>
|
||||||
@@ -35,7 +31,12 @@ 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.
|
||||||
@@ -56,11 +57,7 @@ export default function AccessControlPage() {
|
|||||||
<RestrictedAccess page={"Access Control"}>
|
<RestrictedAccess page={"Access Control"}>
|
||||||
<PoliciesProvider>
|
<PoliciesProvider>
|
||||||
<Suspense fallback={<SkeletonTable />}>
|
<Suspense fallback={<SkeletonTable />}>
|
||||||
<AccessControlTable
|
<AccessControlTable isLoading={isLoading} policies={policies} />
|
||||||
isLoading={isLoading}
|
|
||||||
policies={policies}
|
|
||||||
headingTarget={portalTarget}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</PoliciesProvider>
|
</PoliciesProvider>
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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";
|
||||||
@@ -16,9 +15,6 @@ 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"}>
|
||||||
@@ -29,7 +25,11 @@ export default function Activity() {
|
|||||||
icon={<ActivityIcon size={13} />}
|
icon={<ActivityIcon size={13} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Activity Events</h1>
|
<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,11 +48,7 @@ export default function Activity() {
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
<RestrictedAccess page={"Activity"}>
|
<RestrictedAccess page={"Activity"}>
|
||||||
<ActivityTable
|
<ActivityTable events={events} isLoading={isLoading} />
|
||||||
events={events}
|
|
||||||
isLoading={isLoading}
|
|
||||||
headingTarget={portalTarget}
|
|
||||||
/>
|
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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";
|
||||||
@@ -21,9 +20,6 @@ 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"}>
|
||||||
@@ -40,7 +36,11 @@ export default function NameServers() {
|
|||||||
icon={<ServerIcon size={13} />}
|
icon={<ServerIcon size={13} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Nameservers</h1>
|
<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,7 +62,6 @@ export default function NameServers() {
|
|||||||
<NameserverGroupTable
|
<NameserverGroupTable
|
||||||
nameserverGroups={nameserverGroups}
|
nameserverGroups={nameserverGroups}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
headingTarget={portalTarget}
|
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
|
|||||||
@@ -14,24 +14,17 @@ 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"}>
|
||||||
@@ -62,16 +55,10 @@ export default function NameServerSettings() {
|
|||||||
in our documentation.
|
in our documentation.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<RestrictedAccess page={"DNS Settings"}>
|
<RestrictedAccess page={"DNS Settings"}>
|
||||||
{!isLoading && initialDNSGroups !== undefined ? (
|
{!isLoading && (
|
||||||
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
|
<SettingDisabledManagementGroups
|
||||||
) : (
|
initial={settings?.disabled_management_groups}
|
||||||
<div>
|
/>
|
||||||
<Skeleton
|
|
||||||
width={"100%"}
|
|
||||||
className={"mt-8 max-w-xl"}
|
|
||||||
height={240}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,16 +67,16 @@ export default function NameServerSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SettingDisabledManagementGroups = ({
|
const SettingDisabledManagementGroups = ({
|
||||||
initialGroups,
|
initial,
|
||||||
}: {
|
}: {
|
||||||
initialGroups: Group[];
|
initial: string[] | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
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: initialGroups,
|
initial: initial || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
||||||
@@ -121,7 +108,6 @@ 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}
|
||||||
/>
|
/>
|
||||||
@@ -136,7 +122,6 @@ const SettingDisabledManagementGroups = ({
|
|||||||
size={"sm"}
|
size={"sm"}
|
||||||
onClick={saveSettings}
|
onClick={saveSettings}
|
||||||
disabled={!hasChanges}
|
disabled={!hasChanges}
|
||||||
data-cy={"save-changes"}
|
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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";
|
||||||
@@ -14,7 +13,6 @@ import PeersProvider from "@/contexts/PeersProvider";
|
|||||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||||
import { Route } from "@/interfaces/Route";
|
import { Route } from "@/interfaces/Route";
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
|
|
||||||
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
|
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
|
||||||
|
|
||||||
const NetworkRoutesTable = lazy(
|
const NetworkRoutesTable = lazy(
|
||||||
@@ -25,9 +23,6 @@ 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>
|
||||||
@@ -40,8 +35,10 @@ export default function NetworkRoutes() {
|
|||||||
icon={<NetworkRoutesIcon size={13} />}
|
icon={<NetworkRoutesIcon size={13} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>
|
<h1>
|
||||||
Network Routes <NetworkRoutesDeprecationInfo size={18} />
|
{groupedRoutes && groupedRoutes.length > 1
|
||||||
|
? `${groupedRoutes.length} Network Routes`
|
||||||
|
: "Network Routes"}
|
||||||
</h1>
|
</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
|
||||||
@@ -68,7 +65,6 @@ export default function NetworkRoutes() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
groupedRoutes={groupedRoutes}
|
groupedRoutes={groupedRoutes}
|
||||||
routes={routes}
|
routes={routes}
|
||||||
headingTarget={portalTarget}
|
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"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 other resources like LANs and VPCs
|
|
||||||
without installing NetBird on every device.
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ 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 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";
|
||||||
@@ -55,12 +54,15 @@ 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 { AddExitNodeButton } from "@/modules/exit-node/AddExitNodeButton";
|
||||||
|
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
|
||||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||||
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
|
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
|
||||||
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
|
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
||||||
|
|
||||||
export default function PeerPage() {
|
export default function PeerPage() {
|
||||||
const queryParameter = useSearchParams();
|
const queryParameter = useSearchParams();
|
||||||
@@ -70,7 +72,7 @@ export default function PeerPage() {
|
|||||||
useRedirect("/peers", false, !peerId);
|
useRedirect("/peers", false, !peerId);
|
||||||
|
|
||||||
return peer && !isLoading ? (
|
return peer && !isLoading ? (
|
||||||
<PeerProvider peer={peer} key={peerId}>
|
<PeerProvider peer={peer}>
|
||||||
<PeerOverview />
|
<PeerOverview />
|
||||||
</PeerProvider>
|
</PeerProvider>
|
||||||
) : (
|
) : (
|
||||||
@@ -131,6 +133,7 @@ function PeerOverview() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { isUser } = useLoggedInUser();
|
const { isUser } = useLoggedInUser();
|
||||||
|
const hasExitNodes = useHasExitNodes(peer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@@ -297,38 +300,65 @@ function PeerOverview() {
|
|||||||
/>
|
/>
|
||||||
</FullTooltip>
|
</FullTooltip>
|
||||||
|
|
||||||
{!isUser && (
|
<div>
|
||||||
<div>
|
<Label>Assigned Groups</Label>
|
||||||
<Label>Assigned Groups</Label>
|
<HelpText>
|
||||||
<HelpText>
|
Use groups to control what this peer can access.
|
||||||
Use groups to control what this peer can access.
|
</HelpText>
|
||||||
</HelpText>
|
<FullTooltip
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FullTooltip>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLinux && !isUser ? (
|
<Separator />
|
||||||
<>
|
|
||||||
<Separator />
|
|
||||||
<PeerNetworkRoutesSection peer={peer} />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{peer?.id && (
|
{isLinux && !isUser ? (
|
||||||
<>
|
<div className={"px-8 py-6"}>
|
||||||
<Separator />
|
<div className={"max-w-6xl"}>
|
||||||
<AccessiblePeersSection peerID={peer.id} />
|
<div className={"flex justify-between items-center"}>
|
||||||
</>
|
<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 className={"gap-4 flex"}>
|
||||||
|
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
|
||||||
|
<AddRouteDropdownButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PeerRoutesTable peer={peer} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</RoutesProvider>
|
</RoutesProvider>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ 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 { usePortalElement } from "@hooks/usePortalElement";
|
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, useEffect } from "react";
|
||||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
import { useGroups } from "@/contexts/GroupsProvider";
|
||||||
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";
|
||||||
|
|
||||||
@@ -20,22 +21,24 @@ export default function Peers() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
{permission.dashboard_view === "blocked" ? (
|
{permission?.dashboard_view === "blocked" ? (
|
||||||
<PeersBlockedView />
|
<PeersBlockedView />
|
||||||
) : (
|
) : (
|
||||||
<PeersProvider>
|
<PeersView />
|
||||||
<PeersView />
|
|
||||||
</PeersProvider>
|
|
||||||
)}
|
)}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeersView() {
|
function PeersView() {
|
||||||
const { peers, isLoading } = usePeers();
|
const { data: peers, isLoading } = useFetchApi<Peer[]>("/peers");
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const { ref: headingRef, portalTarget } =
|
const { refresh } = useGroups();
|
||||||
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;
|
||||||
@@ -55,7 +58,7 @@ function PeersView() {
|
|||||||
icon={<PeerIcon size={13} />}
|
icon={<PeerIcon size={13} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Peers</h1>
|
<h1>{peers && peers.length > 1 ? `${peers.length} Peers` : "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.
|
||||||
@@ -73,11 +76,7 @@ function PeersView() {
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={<SkeletonTable />}>
|
<Suspense fallback={<SkeletonTable />}>
|
||||||
<PeersTable
|
<PeersTable isLoading={isLoading} peers={peersWithUser} />
|
||||||
isLoading={isLoading}
|
|
||||||
peers={peersWithUser}
|
|
||||||
headingTarget={portalTarget}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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";
|
||||||
@@ -22,9 +21,6 @@ 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>
|
||||||
@@ -42,16 +38,17 @@ export default function PostureChecksPage() {
|
|||||||
icon={<ShieldCheck size={15} />}
|
icon={<ShieldCheck size={15} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Posture Checks</h1>
|
<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
|
<InlineLink href={"https://docs.netbird.io/how-to/manage-posture-checks"} target={"_blank"}>
|
||||||
href={"https://docs.netbird.io/how-to/manage-posture-checks"}
|
|
||||||
target={"_blank"}
|
|
||||||
>
|
|
||||||
Posture Checks
|
Posture Checks
|
||||||
<ExternalLinkIcon size={12} />
|
<ExternalLinkIcon size={12} />
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
@@ -63,7 +60,6 @@ export default function PostureChecksPage() {
|
|||||||
<PoliciesProvider>
|
<PoliciesProvider>
|
||||||
<Suspense fallback={<SkeletonTable />}>
|
<Suspense fallback={<SkeletonTable />}>
|
||||||
<PostureCheckTable
|
<PostureCheckTable
|
||||||
headingTarget={portalTarget}
|
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
postureChecks={postureChecks}
|
postureChecks={postureChecks}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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";
|
||||||
@@ -17,7 +16,6 @@ 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() {
|
||||||
@@ -49,10 +47,6 @@ 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
|
||||||
@@ -63,7 +57,6 @@ 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>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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, useMemo } from "react";
|
import React, { lazy, Suspense, useMemo } from "react";
|
||||||
@@ -39,9 +38,6 @@ export default function SetupKeys() {
|
|||||||
});
|
});
|
||||||
}, [setupKeys, groups]);
|
}, [setupKeys, groups]);
|
||||||
|
|
||||||
const { ref: headingRef, portalTarget } =
|
|
||||||
usePortalElement<HTMLHeadingElement>();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div className={"p-default py-6"}>
|
<div className={"p-default py-6"}>
|
||||||
@@ -52,7 +48,11 @@ export default function SetupKeys() {
|
|||||||
icon={<SetupKeysIcon size={13} />}
|
icon={<SetupKeysIcon size={13} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Setup Keys</h1>
|
<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.
|
||||||
@@ -74,7 +74,6 @@ 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,7 +5,6 @@ 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";
|
||||||
@@ -23,9 +22,6 @@ 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"}>
|
||||||
@@ -42,7 +38,11 @@ export default function ServiceUsers() {
|
|||||||
icon={<IconSettings2 size={17} />}
|
icon={<IconSettings2 size={17} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Service Users</h1>
|
<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,11 +61,7 @@ export default function ServiceUsers() {
|
|||||||
</div>
|
</div>
|
||||||
<RestrictedAccess page={"Service Users"}>
|
<RestrictedAccess page={"Service Users"}>
|
||||||
<Suspense fallback={<SkeletonTable />}>
|
<Suspense fallback={<SkeletonTable />}>
|
||||||
<ServiceUsersTable
|
<ServiceUsersTable users={users} isLoading={isLoading} />
|
||||||
users={users}
|
|
||||||
isLoading={isLoading}
|
|
||||||
headingTarget={portalTarget}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -22,13 +22,11 @@ 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";
|
||||||
@@ -47,10 +45,8 @@ export default function UserPage() {
|
|||||||
|
|
||||||
useRedirect("/team/users", false, !userId);
|
useRedirect("/team/users", false, !userId);
|
||||||
|
|
||||||
const userGroups = useGroupIdsToGroups(user?.auto_groups);
|
return !isLoading && user ? (
|
||||||
|
<UserOverview user={user} />
|
||||||
return !isLoading && user && userGroups !== undefined ? (
|
|
||||||
<UserOverview user={user} initialGroups={userGroups} />
|
|
||||||
) : (
|
) : (
|
||||||
<FullScreenLoading />
|
<FullScreenLoading />
|
||||||
);
|
);
|
||||||
@@ -58,16 +54,16 @@ export default function UserPage() {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User;
|
user: User;
|
||||||
initialGroups: Group[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
function UserOverview({ user }: 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,
|
||||||
@@ -184,7 +180,6 @@ function UserOverview({ user, initialGroups }: Readonly<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>
|
||||||
@@ -192,7 +187,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
<div className={"flex gap-10 w-full mt-8 max-w-6xl"}>
|
||||||
<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 && (
|
||||||
@@ -205,8 +200,6 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
|||||||
disabled={isUser}
|
disabled={isUser}
|
||||||
onChange={setSelectedGroups}
|
onChange={setSelectedGroups}
|
||||||
values={selectedGroups}
|
values={selectedGroups}
|
||||||
hideAllGroup={true}
|
|
||||||
dataCy={"user-group-selector"}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -221,8 +214,6 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
|||||||
<UserRoleSelector
|
<UserRoleSelector
|
||||||
value={role}
|
value={role}
|
||||||
onChange={setRole}
|
onChange={setRole}
|
||||||
hideOwner={user.is_service_user}
|
|
||||||
currentUser={user}
|
|
||||||
disabled={
|
disabled={
|
||||||
isLoggedInUser ||
|
isLoggedInUser ||
|
||||||
!isOwnerOrAdmin ||
|
!isOwnerOrAdmin ||
|
||||||
@@ -250,10 +241,7 @@ function UserOverview({ user, initialGroups }: Readonly<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
|
<Button variant={"primary"}>
|
||||||
variant={"primary"}
|
|
||||||
data-cy={"access-token-open-modal"}
|
|
||||||
>
|
|
||||||
<IconCirclePlus size={16} />
|
<IconCirclePlus size={16} />
|
||||||
Create Access Token
|
Create Access Token
|
||||||
</Button>
|
</Button>
|
||||||
@@ -302,7 +290,6 @@ function UserInformationCard({ user }: { user: User }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Card.ListItem
|
<Card.ListItem
|
||||||
tooltip={false}
|
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<GalleryHorizontalEnd size={16} />
|
<GalleryHorizontalEnd size={16} />
|
||||||
@@ -314,18 +301,15 @@ function UserInformationCard({ user }: { user: User }) {
|
|||||||
|
|
||||||
{!isServiceUser && (
|
{!isServiceUser && (
|
||||||
<>
|
<>
|
||||||
{!user.is_current && user.role != Role.Owner && (
|
<Card.ListItem
|
||||||
<Card.ListItem
|
label={
|
||||||
label={
|
<>
|
||||||
<>
|
<Ban size={16} />
|
||||||
<Ban size={16} />
|
Block User
|
||||||
Block User
|
</>
|
||||||
</>
|
}
|
||||||
}
|
value={<UserBlockCell user={user} isUserPage={true} />}
|
||||||
value={<UserBlockCell user={user} isUserPage={true} />}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card.ListItem
|
<Card.ListItem
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,26 +5,20 @@ 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, User2 } from "lucide-react";
|
import { ExternalLinkIcon, User2 } from "lucide-react";
|
||||||
import React, { lazy, Suspense } from "react";
|
import React, { lazy, Suspense } from "react";
|
||||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||||
import { useGroups } from "@/contexts/GroupsProvider";
|
|
||||||
import { User } from "@/interfaces/User";
|
import { User } from "@/interfaces/User";
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
|
|
||||||
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
|
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
|
||||||
|
|
||||||
export default function TeamUsers() {
|
export default function TeamUsers() {
|
||||||
const { isLoading: isGroupsLoading } = useGroups();
|
|
||||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||||
"/users?service_user=false",
|
"/users?service_user=false",
|
||||||
);
|
);
|
||||||
|
|
||||||
const { ref: headingRef, portalTarget } =
|
|
||||||
usePortalElement<HTMLHeadingElement>();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div className={"p-default py-6"}>
|
<div className={"p-default py-6"}>
|
||||||
@@ -41,7 +35,7 @@ export default function TeamUsers() {
|
|||||||
icon={<User2 size={16} />}
|
icon={<User2 size={16} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Users</h1>
|
<h1>{users && users.length > 1 ? `${users.length} Users` : "Users"}</h1>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
Manage users and their permissions. Same-domain email users are added
|
Manage users and their permissions. Same-domain email users are added
|
||||||
automatically on first sign-in.
|
automatically on first sign-in.
|
||||||
@@ -60,11 +54,7 @@ export default function TeamUsers() {
|
|||||||
</div>
|
</div>
|
||||||
<RestrictedAccess page={"Users"}>
|
<RestrictedAccess page={"Users"}>
|
||||||
<Suspense fallback={<SkeletonTable />}>
|
<Suspense fallback={<SkeletonTable />}>
|
||||||
<UsersTable
|
<UsersTable users={users} isLoading={isLoading} />
|
||||||
users={users}
|
|
||||||
isLoading={isLoading || isGroupsLoading}
|
|
||||||
headingTarget={portalTarget}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
Loaded 20 of 203 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user