Compare commits

..
Author SHA1 Message Date
Eduard Gert e167afb06b Fix missing groups on users and dns settings page 2024-11-13 16:18:54 +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
205 changed files with 8315 additions and 2411 deletions

No files matched your search

+1 -2
View File
@@ -4,7 +4,6 @@ on:
branches: branches:
- "feature/**" - "feature/**"
- main - main
- debug
tags: tags:
- "**" - "**"
pull_request: pull_request:
@@ -56,7 +55,7 @@ jobs:
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
push: true push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64,linux/arm platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+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
+2 -1
View File
@@ -12,5 +12,6 @@
"tokenSource": "$NETBIRD_TOKEN_SOURCE", "tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS", "dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID", "hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID" "googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL"
} }
+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;
+440 -96
View File
File diff suppressed because it is too large. Load diff
+6 -3
View File
@@ -13,7 +13,7 @@
"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",
@@ -22,8 +22,9 @@
"@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 +38,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,7 +55,7 @@
"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.383.0",
"next": "13.5.5", "next": "13.5.5",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"punycode": "^2.3.1", "punycode": "^2.3.1",
@@ -67,6 +69,7 @@
"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",
+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>
+22 -27
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>
) : ( ) : (
@@ -320,6 +324,7 @@ function PeerOverview() {
disabled={isUser} disabled={isUser}
onChange={setSelectedGroups} onChange={setSelectedGroups}
values={selectedGroups} values={selectedGroups}
hideAllGroup={true}
peer={peer} peer={peer}
/> />
</FullTooltip> </FullTooltip>
@@ -328,29 +333,19 @@ function PeerOverview() {
</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}
/> />
+5 -1
View File
@@ -11,6 +11,7 @@ import {
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider"; import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Role } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer"; import PageContainer from "@/layouts/PageContainer";
import { useAccount } from "@/modules/account/useAccount"; import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab"; import AuthenticationTab from "@/modules/settings/AuthenticationTab";
@@ -52,7 +53,10 @@ export default function NetBirdSettings() {
Danger zone Danger zone
</VerticalTabs.Trigger> </VerticalTabs.Trigger>
</VerticalTabs.List> </VerticalTabs.List>
<RestrictedAccess page={"Settings"}> <RestrictedAccess
page={"Settings"}
allow={[Role.Admin, Role.Owner, Role.BillingAdmin]}
>
<div className={"border-l border-nb-gray-930 w-full"}> <div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />} {account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />} {account && <PermissionsTab account={account} />}
+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>
+34 -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,18 @@ function UserInformationCard({ user }: { user: User }) {
{!isServiceUser && ( {!isServiceUser && (
<> <>
<Card.ListItem {!user.is_current && user.role != Role.Owner && (
label={ <Card.ListItem
<> label={
<Ban size={16} /> <>
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={
<> <>
+12 -2
View File
@@ -5,20 +5,26 @@ 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"}>
@@ -35,7 +41,7 @@ export default function TeamUsers() {
icon={<User2 size={16} />} icon={<User2 size={16} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1>{users && users.length > 1 ? `${users.length} Users` : "Users"}</h1> <h1 ref={headingRef}>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.
@@ -54,7 +60,11 @@ export default function TeamUsers() {
</div> </div>
<RestrictedAccess page={"Users"}> <RestrictedAccess page={"Users"}>
<Suspense fallback={<SkeletonTable />}> <Suspense fallback={<SkeletonTable />}>
<UsersTable users={users} isLoading={isLoading} /> <UsersTable
users={users}
isLoading={isLoading || isGroupsLoading}
headingTarget={portalTarget}
/>
</Suspense> </Suspense>
</RestrictedAccess> </RestrictedAccess>
</PageContainer> </PageContainer>
+5
View File
@@ -68,4 +68,9 @@ p {
.stepper-bg-variant .step-circle { .stepper-bg-variant .step-circle {
@apply !border-[#1d2024]; @apply !border-[#1d2024];
}
.webkit-scroll{
-webkit-overflow-scrolling: touch;
-webkit-transform: translate3d(0, 0, 0);
} }
Loaded 20 of 205 files, more files were not shown because too many files have changed in this diff. Show more