Compare commits

..
Author SHA1 Message Date
Eduard Gert 42763b23c0 Update package-lock.json 2025-04-28 12:18:53 +02:00
Eduard Gert a918465c7e Merge remote-tracking branch 'origin/main' into update/nextjs-version 2025-04-28 12:00:34 +02:00
Eduard Gert d7c5f7e183 Hide update available for mobile devices (#106) (#460)
(cherry picked from commit 7f248ae060385acb1245591bd46e2bb6d53ed908)
2025-04-28 11:58:26 +02:00
Eduard Gert ebbe865ce0 Add custom dns domain (#458)
* Update domain validator

* Add custom dns domain
2025-04-28 11:58:16 +02:00
Eduard Gert 6c0ab88488 Update domain validator (#459) 2025-04-28 11:53:44 +02:00
Eduard Gert a50576d851 Fix nameserver port input for Safari (#456) 2025-04-28 11:21:25 +02:00
Eduard Gert 150a9cecc3 Update Next.js version 2025-04-17 16:24:13 +02:00
Eduard Gert 676250266c Fix browse posture checks table filters (#448) 2025-04-07 10:23:07 +02:00
Vladislav Tropnikov 042c65a652 Add display of ID if user does not have email (#450)
* Add display of ID if user does not have email

* Update PeerNameCell.tsx

* Add more possible id parameters

* Hide user if there is nothing

* change id order

* Keep default behavior
2025-03-27 17:30:26 +01:00
Misha Bragin 96f2d39e54 Add CLA 2025-03-18 15:58:05 +01:00
Eduard Gert 61e11d3740 Apply recent cloud changes (#447)
* Add resource description, add single resource for acl, add icons for group badges, add inactivity expiration

* Add extra dns labels, remove routing restriction
2025-02-21 15:53:40 +01:00
Edouard VanbelleandEduard Gert c8e3b50f1b Display serial number on peer information and on peers table (#444)
* display serial number on peer information and on peers table

  * add serial on peers list (included in OS information to minimize informations)
  * permit a lookup via serial number
  * add serial on peer information

* Update os icon to match existing one and hide serial if it does not exist

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2025-01-30 13:14:38 +01:00
Eduard GertandMisha Bragin 25be69e7bb Add improvements to new networks features (#439)
* Fix wrong ui state for routing peer modal in networks

* Add confirmation dialog when blocking users

* Keep peer sort order when switching pages

* Update sidebar navigation order and remove deprecation notice

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

* Update group text in network resource modal

* Update networks page text

* Fix line height

* Add search to resource table

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

* Add enabled toggle to routing peers

* Add enabled toggle to network resources

* Add resource group modal and adjust tables

* Clarify networks

* Fix not properly aligned horizontal scroll bar

* Add option to install netbird after creating a setup key

* Fix text for install netbird modal

* Show resources count in group settings

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

* Fix wording

* Fix resource policy count

* Hide resource count when selection source groups

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

* Add option for horizontal stepper

* Generate setup key when installing netbird from routing peer modal

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

---------

Co-authored-by: Misha Bragin <bangvalo@gmail.com>
2025-01-20 16:18:21 +01:00
Eduard Gert 43e5d5cf53 Fix activity search and allow searching for meta fields (#440) 2025-01-15 16:41:55 +01:00
Eduard Gert 18819d6fdf Add confirmation dialog when blocking users (#437) 2025-01-15 16:29:05 +01:00
Eduard Gert 158804c1ac Fix wrong ui state for routing peer modal in networks (#436) 2025-01-15 16:28:50 +01:00
Misha Bragin 14d2d68819 Update links to networks doc (#435) 2024-12-27 21:48:36 +01:00
Pascal Fischer 40902b3629 add resources to groups update operation (#434) 2024-12-27 14:20:11 +01:00
Pascal Fischer fa9bcea4ab Update links for networks concept (#433) 2024-12-23 18:08:06 +01:00
Eduard Gert 3ba7acdecf Add new networks feature (#427) 2024-12-23 13:20:01 +03:00
Eduard Gert c7775ade8c Hide groups for regular users (#423) 2024-11-20 17:50:28 +01:00
Maycon Santos cd3e75b640 Add setup-key improvements (#420)
- Add support to key deletion
- Add custom and unlimited expiration
2024-11-01 16:04:43 +01:00
171 changed files with 11043 additions and 3530 deletions

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
## Contributor License Agreement
This Contributor License Agreement (referred to as the "Agreement") is entered into by the individual
submitting this Agreement and NetBird GmbH, c/o Max-Beer-Straße 2-4 Münzstraße 12 10178 Berlin, Germany,
referred to as "NetBird" (collectively, the "Parties"). The Agreement outlines the terms and conditions
under which NetBird may utilize software contributions provided by the Contributor for inclusion in
its software development projects. By submitting this Agreement, the Contributor confirms their acceptance
of the terms and conditions outlined below. The Contributor further represents that they are authorized to
complete this process as described herein.
## 1 Preamble
In order to clarify the IP Rights situation with regard to Contributions from any person or entity, NetBird
must have a contributor license agreement on file to be signed by each Contributor, containing the license
terms below. This license serves as protection for both the Contributor as well as NetBird and its software users;
it does not change Contributor’s rights to use his/her own Contributions for any other purpose.
## 2 Definitions
2.1 “IP Rights” shall mean all industrial and intellectual property rights, whether registered or not registered, whether created by Contributor or acquired by Contributor from third parties, and similar rights, including (but not limited to) semiconductor property rights, design rights, copyrights (including in the form of database rights and rights to software), all neighbouring rights (Leistungsschutzrechte), trademarks, service marks, titles, internet domain names, trade names and other labelling rights, rights deriving from corresponding applications and registrations of such rights as well as any licenses (Nutzungsrechte) under and entitlements to any such intellectual and industrial property rights.
2.2 "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is or previously has been intentionally Submitted by Contributor to NetBird for inclusion in, or documentation of any Work.
2.3 "Contributor" shall mean the copyright owner or legal entity authorized by the copyright owner that is concluding this Agreement with NetBird. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
2.4 "Submitted" shall mean any form of electronic, verbal, or written communication sent to NetBird or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, NetBird for the purpose of discussing and improving the Work, but excluding communication that is marked or otherwise designated in writing by Contributor as "Not a Contribution".
2.5 "Work" means any of the products owned or managed by NetBird, in particular, but not exclusively, software.
## 3 Licenses
3.1 Subject to the terms and conditions of this agreement, Contributor hereby grants to NetBird and to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license to reproduce by any means and in any form, in whole or in part, permanently or temporarily, the Contributions (including loading, displaying, executing, transmitting or storing works for the purpose of executing and processing data or transferring them to video, audio and other data carriers), including the right to distribute, display and present such Contributions and make them available to the public (e.g. via the internet) and to transmit and display such Contributions by any means. The license also includes the right to modify, translate, adapt, edit and otherwise alter the Contributions and to use these results in the same manner as the original Contributions and derivative works. Except for licenses in patents acc. to Sec. 3, such license refers to any IP Rights in the Contributions and derivative works. The Contributor acknowledges that NetBird is not required to credit them by name for their Contribution and agrees to waive any moral rights associated with their Contribution in relation to NetBird or its sublicensees.
3.2 Subject to the terms and conditions of this agreement, Contributor hereby grants to NetBird and to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license in the Contributions to make, have made, use, sell, offer to sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by the Contributor which are necessarily infringed by Contributor‘s Contribution(s) alone or by combination of Contributor’s Contribution(s) with the Work to which such Contribution(s) was Submitted.
3.3 NetBird hereby accepts such licenses.
## 4 Contributor’s Representations
4.1 Contributor represents that Contributor is legally entitled to grant the above license. If Contributor’s employer has IP Rights to Contributor’s Contributions, Contributor represent that he/she has received permission to make Contributions on behalf of such employer, that such employer has waived such IP Rights to the Contributions of Contributor to NetBird, or that such employer has executed a separate contributor license agreement with NetBird.
4.2 Contributor represents that any Contribution is his/her original creation.
4.3 Contributor represents to his/her best knowledge that any Contribution does not violate any third party IP Rights.
4.4 Contributor represents that any Contribution submission includes complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which Contributor is personally aware and which are associated with any part of the Contribution.
4.5 The Contributor represents that their Contribution does not include any work distributed under a copyleft license.
## 5 Information obligation
Contributor agrees to notify NetBird of any facts or circumstances of which Contributor become aware that would make these representations inaccurate in any respect.
## 6 Submission of Third-Party works
Should Contributor wish to submit work that is not Contributor’s original creation, Contributor may submit it to NetBird separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which Contributor are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
## 7 No Consideration
Unless compensation is mandatory under statutory law, no compensation for any license under this agreement shall be payable.
## 8 Final Provisions
8.1 Laws. This Agreement is governed by the laws of the Federal Republic of Germany.
8.2 Venue. Place of jurisdiction shall, to the extent legally permissible, be Berlin, Germany.
8.3 Severability. If any provision in this agreement is unlawful, invalid or ineffective, it shall not affect the enforceability or effectiveness of the remainder of this agreement. The parties agree to replace any unlawful, invalid or ineffective provision with a provision that comes as close as possible to the commercial intent and purpose of the original provision. This section also applies accordingly to any gaps in the contract.
8.4 Variations. Any variations, amendments or supplements to this Agreement must be in writing. This also applies to any variation of this Section 8.4.
+4139 -2547
View File
File diff suppressed because it is too large. Load diff
+7 -6
View File
@@ -19,6 +19,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -47,7 +48,6 @@
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"eslint": "^8",
"eslint-config-next": "13.5.5",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"flowbite": "^1.8.1",
@@ -55,13 +55,13 @@
"framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.383.0",
"next": "13.5.5",
"lucide-react": "^0.460.0",
"next": "^14.2.28",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18",
"react": "^18.3.1",
"react-day-picker": "^8.9.1",
"react-dom": "^18",
"react-dom": "^18.3.1",
"react-ga4": "^2.1.0",
"react-hot-toast": "^2.4.1",
"react-hotjar": "^6.2.0",
@@ -76,7 +76,8 @@
"typescript": "^5"
},
"devDependencies": {
"cypress": "^13.3.3",
"cypress": "^13.13.0",
"eslint-config-next": "^14.2.28",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
+22 -7
View File
@@ -14,17 +14,24 @@ import { IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React from "react";
import Skeleton from "react-loading-skeleton";
import { useSWRConfig } from "swr";
import DNSIcon from "@/assets/icons/DNSIcon";
import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { NameserverSettings } from "@/interfaces/NameserverSettings";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
export default function NameServerSettings() {
const { data: settings, isLoading } =
useFetchApi<NameserverSettings>("/dns/settings");
const initialDNSGroups = useGroupIdsToGroups(
settings?.disabled_management_groups,
);
return (
<PageContainer>
<div className={"p-default py-6"}>
@@ -55,10 +62,16 @@ export default function NameServerSettings() {
in our documentation.
</Paragraph>
<RestrictedAccess page={"DNS Settings"}>
{!isLoading && (
<SettingDisabledManagementGroups
initial={settings?.disabled_management_groups}
/>
{!isLoading && initialDNSGroups !== undefined ? (
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
) : (
<div>
<Skeleton
width={"100%"}
className={"mt-8 max-w-xl"}
height={240}
/>
</div>
)}
</RestrictedAccess>
</div>
@@ -67,16 +80,16 @@ export default function NameServerSettings() {
}
const SettingDisabledManagementGroups = ({
initial,
initialGroups,
}: {
initial: string[] | undefined;
initialGroups: Group[];
}) => {
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
const { mutate } = useSWRConfig();
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initial || [],
initial: initialGroups,
});
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
@@ -108,6 +121,7 @@ const SettingDisabledManagementGroups = ({
Peers in these groups will require manual domain name resolution
</HelpText>
<PeerGroupSelector
dataCy={"dns-groups-selector"}
onChange={setSelectedGroups}
values={selectedGroups}
/>
@@ -122,6 +136,7 @@ const SettingDisabledManagementGroups = ({
size={"sm"}
onClick={saveSettings}
disabled={!hasChanges}
data-cy={"save-changes"}
>
Save Changes
</Button>
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Network - Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+229
View File
@@ -0,0 +1,229 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Card from "@components/Card";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import {
ArrowUpRightIcon,
HelpCircle,
PencilLineIcon,
ServerIcon,
ShieldCheckIcon,
ShieldXIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
import NetworkModal from "@/modules/networks/NetworkModal";
import { NetworkProvider } from "@/modules/networks/NetworkProvider";
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
export default function NetworkDetailPage() {
const queryParameter = useSearchParams();
const networkId = queryParameter.get("id");
const { data: network, isLoading } = useFetchApi<Network>(
`/networks/${networkId}`,
true,
);
useRedirect("/networks", false, !networkId);
return network && !isLoading ? (
<NetworkOverview network={network} />
) : (
<FullScreenLoading />
);
}
function NetworkOverview({ network }: Readonly<{ network: Network }>) {
const { isUser } = useLoggedInUser();
const [networkModal, setNetworkModal] = useState(false);
const { mutate } = useSWRConfig();
const isActive = !!(
network?.routing_peers_count && network.routing_peers_count > 0
);
return (
<PageContainer>
<NetworkProvider network={network}>
<div className={"p-default py-6 mb-4"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={isUser}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center",
!network.description && "gap-2",
)}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
<button
className={
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
}
onClick={() => setNetworkModal(true)}
>
<PencilLineIcon size={18} />
</button>
<NetworkModal
open={networkModal}
setOpen={setNetworkModal}
onUpdated={() => {
mutate(`/networks/${network.id}`);
}}
network={network}
/>
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<Separator />
<ResourcesSection network={network} />
<div className={"h-3"} />
<Separator />
<NetworkRoutingPeersSection network={network} />
</NetworkProvider>
</PageContainer>
);
}
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const isHighlyAvailable = !!(
network?.routing_peers_count && network?.routing_peers_count >= 2
);
const disabledText = useMemo(
() => (
<>
High availability is currently{" "}
<span className={"text-yellow-400 font-medium"}>inactive</span> for this
network.
</>
),
[],
);
const enabledText = useMemo(
() => (
<>
High availability is{" "}
<span className={"text-green-500 font-medium"}>active</span> for this
network.
</>
),
[],
);
const policyCount = network.policies?.length ?? 0;
return (
<Card>
<Card.List>
<Card.ListItem
tooltip={false}
label={
<>
<ServerIcon size={16} />
High Availability
</>
}
value={
<FullTooltip
interactive={false}
content={
<div className={"max-w-xs text-xs"}>
{isHighlyAvailable ? enabledText : disabledText}
{isHighlyAvailable ? (
<div className={"inline-flex mt-2"}>
You can add more routing peers to increase the
availability of this network.
</div>
) : (
<div className={"inline-flex mt-2"}>
Go ahead and add more routing peers or groups with routing
peers to enable high availability for this network.
</div>
)}
</div>
}
>
<div
className={cn(
"flex gap-2.5 items-center text-nb-gray-300 text-sm cursor-help",
)}
>
<span
className={cn(
"h-2 w-2 rounded-full",
!isHighlyAvailable ? "bg-yellow-400" : "bg-green-500",
)}
></span>
{isHighlyAvailable ? "Active" : "Inactive"}
<HelpCircle size={12} />
</div>
</FullTooltip>
}
/>
<Card.ListItem
tooltip={false}
label={
policyCount > 0 ? (
<>
<ShieldCheckIcon size={16} className={"text-green-500"} />
{policyCount}{" "}
{policyCount === 1 ? "Active Policy" : "Active Policies"}
</>
) : (
<>
<ShieldXIcon size={16} className={"text-red-500"} />
No Active Policies
</>
)
}
value={
policyCount > 0 ? (
<InlineLink href={"/access-control"}>
Go to Policies
<ArrowUpRightIcon size={14} />
</InlineLink>
) : null
}
/>
</Card.List>
</Card>
);
}
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+61
View File
@@ -0,0 +1,61 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import NetworksTable from "@/modules/networks/table/NetworksTable";
export default function Networks() {
const { data: networks, isLoading } = useFetchApi<Network[]>("/networks");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Networks</h1>
<Paragraph>
Networks allow you to access internal resources in LANs and VPCs without
installing NetBird on every machine.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/networks"}
target={"_blank"}
>
Networks
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess>
<Suspense fallback={<SkeletonTable />}>
<NetworksTable
data={networks}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+96 -89
View File
@@ -23,13 +23,13 @@ import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import useRedirect from "@hooks/useRedirect";
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import dayjs from "dayjs";
import { isEmpty, trim } from "lodash";
import {
Barcode,
Cpu,
FlagIcon,
Globe,
@@ -40,6 +40,7 @@ import {
NetworkIcon,
PencilIcon,
TerminalSquare,
TimerResetIcon,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { toASCII } from "punycode";
@@ -55,11 +56,11 @@ import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import type { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
export default function PeerPage() {
@@ -69,9 +70,16 @@ export default function PeerPage() {
useRedirect("/peers", false, !peerId);
const peerKey = useMemo(() => {
let id = peer?.id ?? "";
let ssh = peer?.ssh_enabled ? "1" : "0";
let expiration = peer?.login_expiration_enabled ? "1" : "0";
return `${id}-${ssh}-${expiration}`;
}, [peer]);
return peer && !isLoading ? (
<PeerProvider peer={peer} key={peerId}>
<PeerOverview />
<PeerOverview key={peerKey} />
</PeerProvider>
) : (
<FullScreenLoading />
@@ -88,20 +96,15 @@ function PeerOverview() {
const [loginExpiration, setLoginExpiration] = useState(
peer.login_expiration_enabled,
);
const [inactivityExpiration, setInactivityExpiration] = useState(
peer.inactivity_expiration_enabled,
);
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
useGroupHelper({
initial: peerGroups,
peer,
});
/**
* Check the operating system of the peer, if it is linux, then show the routes table, otherwise hide it.
*/
const isLinux = useMemo(() => {
const operatingSystem = getOperatingSystem(peer.os);
return operatingSystem == OperatingSystem.LINUX;
}, [peer.os]);
/**
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
*/
@@ -110,10 +113,16 @@ function PeerOverview() {
ssh,
selectedGroups,
loginExpiration,
inactivityExpiration,
]);
const updatePeer = async () => {
const updateRequest = update(name, ssh, loginExpiration);
const updateRequest = update({
name,
ssh,
loginExpiration,
inactivityExpiration,
});
const groupCalls = getAllGroupCalls();
const batchCall = groupCalls
? [...groupCalls, updateRequest]
@@ -124,13 +133,19 @@ function PeerOverview() {
promise: Promise.all(batchCall).then(() => {
mutate("/peers/" + peer.id);
mutate("/groups");
updateHasChangedRef([name, ssh, selectedGroups, loginExpiration]);
updateHasChangedRef([
name,
ssh,
selectedGroups,
loginExpiration,
inactivityExpiration,
]);
}),
loadingMessage: "Saving the peer...",
});
};
const { isUser } = useLoggedInUser();
const { isUser, isOwnerOrAdmin } = useLoggedInUser();
return (
<PageContainer>
@@ -212,53 +227,43 @@ function PeerOverview() {
<div className={"flex gap-10 w-full mt-5 max-w-6xl"}>
<PeerInformationCard peer={peer} />
<div className={"flex flex-col gap-6 w-1/2"}>
<FullTooltip
content={
<div
className={
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
{!peer.user_id ? (
<>
<>
<IconInfoCircle size={14} />
<span>
Login expiration is disabled for all peers added
with an setup-key.
</span>
</>
</>
) : (
<>
<LockIcon size={14} />
<span>
{`You don't have the required permissions to update this
setting.`}
</span>
</>
)}
</div>
}
className={"w-full block"}
disabled={!!peer.user_id && !isUser}
>
<FancyToggleSwitch
disabled={!peer.user_id || isUser}
<div className={"flex flex-col gap-6 w-1/2 transition-all"}>
<div>
<PeerExpirationToggle
peer={peer}
value={loginExpiration}
onChange={setLoginExpiration}
label={
<>
<IconCloudLock size={16} />
Login Expiration
</>
}
helpText={
"Enable to require SSO login peers to re-authenticate when their login expires."
}
icon={<TimerResetIcon size={16} />}
onChange={(state) => {
setLoginExpiration(state);
!state && setInactivityExpiration(false);
}}
/>
</FullTooltip>
{isOwnerOrAdmin && !!peer?.user_id && (
<div
className={cn(
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
!loginExpiration
? "opacity-50 pointer-events-none"
: "bg-nb-gray-930/80",
)}
>
<PeerExpirationToggle
peer={peer}
variant={"blank"}
value={inactivityExpiration}
onChange={setInactivityExpiration}
title={"Require login after disconnect"}
description={
"Enable to require authentication after users disconnect from management for 10 minutes."
}
className={
!loginExpiration ? "opacity-40 pointer-events-none" : ""
}
/>
</div>
)}
</div>
<FullTooltip
content={
<div
@@ -297,29 +302,12 @@ function PeerOverview() {
/>
</FullTooltip>
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</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}
>
{!isUser && (
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<PeerGroupSelector
disabled={isUser}
onChange={setSelectedGroups}
@@ -327,13 +315,13 @@ function PeerOverview() {
hideAllGroup={true}
peer={peer}
/>
</FullTooltip>
</div>
</div>
)}
</div>
</div>
</div>
{isLinux && !isUser ? (
{!isUser ? (
<>
<Separator />
<PeerNetworkRoutesSection peer={peer} />
@@ -351,7 +339,7 @@ function PeerOverview() {
);
}
function PeerInformationCard({ peer }: { peer: Peer }) {
function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
const { isLoading, getRegionByPeer } = useCountries();
const countryText = useMemo(() => {
@@ -387,14 +375,20 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
<Card.ListItem
copy
copyText={"Domain name"}
copyText={"DNS label"}
label={
<>
<Globe size={16} />
Domain Name
</>
}
className={
peer?.extra_dns_labels && peer.extra_dns_labels.length > 0
? "items-start"
: ""
}
value={peer.dns_label}
extraText={peer?.extra_dns_labels}
/>
<Card.ListItem
@@ -446,6 +440,19 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
}
value={peer.os}
/>
{peer.serial_number && peer.serial_number !== "" && (
<Card.ListItem
label={
<>
<Barcode size={16} />
Serial Number
</>
}
value={peer.serial_number}
/>
)}
<Card.ListItem
label={
<>
@@ -492,7 +499,7 @@ interface ModalProps {
peer: Peer;
initialName: string;
}
function EditNameModal({ onSuccess, peer, initialName }: ModalProps) {
function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
const [name, setName] = useState(initialName);
const isDisabled = useMemo(() => {
+7
View File
@@ -6,6 +6,7 @@ import {
AlertOctagonIcon,
FolderGit2Icon,
LockIcon,
NetworkIcon,
ShieldIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
@@ -16,6 +17,7 @@ import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
import GroupsTab from "@/modules/settings/GroupsTab";
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab";
export default function NetBirdSettings() {
@@ -47,6 +49,10 @@ export default function NetBirdSettings() {
<LockIcon size={14} />
Permissions
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="networks">
<NetworkIcon size={14} />
Networks
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
<AlertOctagonIcon size={14} />
Danger zone
@@ -57,6 +63,7 @@ export default function NetBirdSettings() {
{account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />}
{account && <GroupsTab account={account} />}
{account && <NetworkSettingsTab account={account} />}
{account && <DangerZoneTab account={account} />}
</div>
</RestrictedAccess>
+25 -9
View File
@@ -22,11 +22,13 @@ import { useSWRConfig } from "swr";
import TeamIcon from "@/assets/icons/TeamIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { Role, User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable";
import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
@@ -38,6 +40,7 @@ export default function UserPage() {
const { data: users, isLoading } = useFetchApi<User[]>(
`/users?service_user=${isServiceUser}`,
);
const { isOwnerOrAdmin } = useLoggedInUser();
const user = useMemo(() => {
return users?.find((u) => u.id === userId);
@@ -45,25 +48,31 @@ export default function UserPage() {
useRedirect("/team/users", false, !userId);
return !isLoading && user ? (
<UserOverview user={user} />
) : (
<FullScreenLoading />
);
const userGroups = useGroupIdsToGroups(user?.auto_groups);
if (!isOwnerOrAdmin && user && !isLoading) {
return <UserOverview user={user} initialGroups={[]} />;
}
if (isOwnerOrAdmin && user && !isLoading && userGroups) {
return <UserOverview user={user} initialGroups={userGroups} />;
}
return <FullScreenLoading />;
}
type Props = {
user: User;
initialGroups: Group[];
};
function UserOverview({ user }: Props) {
function UserOverview({ user, initialGroups }: Readonly<Props>) {
const router = useRouter();
const userRequest = useApiCall<User>("/users");
const { mutate } = useSWRConfig();
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
const initialGroups = user.auto_groups;
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initialGroups,
@@ -180,6 +189,7 @@ function UserOverview({ user }: Props) {
className={"w-full"}
disabled={!hasChanges}
onClick={save}
data-cy={"save-changes"}
>
Save Changes
</Button>
@@ -190,7 +200,7 @@ function UserOverview({ user }: Props) {
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<UserInformationCard user={user} />
<div className={"flex flex-col gap-8 w-1/2 "}>
{!user.is_service_user && (
{!user.is_service_user && isOwnerOrAdmin && (
<div>
<Label>Auto-assigned groups</Label>
<HelpText>
@@ -201,6 +211,7 @@ function UserOverview({ user }: Props) {
onChange={setSelectedGroups}
values={selectedGroups}
hideAllGroup={true}
dataCy={"user-group-selector"}
/>
</div>
)}
@@ -244,7 +255,10 @@ function UserOverview({ user }: Props) {
<div className={"inline-flex gap-4 justify-end"}>
<div>
<CreateAccessTokenModal user={user}>
<Button variant={"primary"}>
<Button
variant={"primary"}
data-cy={"access-token-open-modal"}
>
<IconCirclePlus size={16} />
Create Access Token
</Button>
@@ -293,6 +307,7 @@ function UserInformationCard({ user }: { user: User }) {
)}
<Card.ListItem
tooltip={false}
label={
<>
<GalleryHorizontalEnd size={16} />
@@ -306,6 +321,7 @@ function UserInformationCard({ user }: { user: User }) {
<>
{!user.is_current && user.role != Role.Owner && (
<Card.ListItem
tooltip={false}
label={
<>
<Ban size={16} />
+3 -1
View File
@@ -10,12 +10,14 @@ import useFetchApi from "@utils/api";
import { ExternalLinkIcon, User2 } from "lucide-react";
import React, { lazy, Suspense } from "react";
import TeamIcon from "@/assets/icons/TeamIcon";
import { useGroups } from "@/contexts/GroupsProvider";
import { User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
export default function TeamUsers() {
const { isLoading: isGroupsLoading } = useGroups();
const { data: users, isLoading } = useFetchApi<User[]>(
"/users?service_user=false",
);
@@ -60,7 +62,7 @@ export default function TeamUsers() {
<Suspense fallback={<SkeletonTable />}>
<UsersTable
users={users}
isLoading={isLoading}
isLoading={isLoading || isGroupsLoading}
headingTarget={portalTarget}
/>
</Suspense>
+1 -1
View File
@@ -12,7 +12,7 @@ export default function CircleIcon({
size = 11,
inactiveDot = "gray",
className,
}: Props) {
}: Readonly<Props>) {
return (
<span
style={{ width: size + "px", height: size + "px" }}
+39
View File
@@ -0,0 +1,39 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function EntraIcon(props: Readonly<IconProps>) {
return (
<svg
width="231"
height="231"
viewBox="0 0 231 231"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
>
<path
d="M48.7923 180.077C53.7717 183.183 62.0492 186.635 70.8015 186.635C78.771 186.635 86.1758 184.325 92.3102 180.385C92.3102 180.385 92.323 180.385 92.3358 180.373L115.5 165.896V218.167C111.83 218.167 108.134 217.166 104.925 215.164L48.7923 180.077Z"
fill="#225086"
/>
<path
d="M100.78 19.3398L4.53017 127.91C-2.90033 136.303 -0.962501 148.982 8.67533 155.001C8.67533 155.001 44.3007 177.267 48.7923 180.077C53.7717 183.183 62.0492 186.635 70.8015 186.635C78.771 186.635 86.1758 184.325 92.3102 180.385C92.3102 180.385 92.323 180.385 92.3358 180.373L115.5 165.896L59.4953 130.887L115.513 67.6958V12.8333C110.072 12.8333 104.63 15.0022 100.78 19.3398Z"
fill="#66DDFF"
/>
<path
d="M59.4953 130.887L60.1627 131.298L115.5 165.896H115.513V67.7087L115.5 67.6958L59.4953 130.887Z"
fill="#CBF8FF"
/>
<path
d="M222.325 155.001C231.963 148.982 233.9 136.303 226.47 127.91L163.317 56.672C158.222 54.2978 152.511 52.9375 146.467 52.9375C134.596 52.9375 123.983 58.058 116.925 66.1045L115.526 67.683L171.53 130.874L115.513 165.884V218.154C119.196 218.154 122.866 217.153 126.075 215.151L222.325 154.988V155.001Z"
fill="#074793"
/>
<path
d="M115.513 12.8333V67.6958L116.912 66.1173C123.97 58.0708 134.583 52.9503 146.454 52.9503C152.511 52.9503 158.209 54.3235 163.304 56.6848L130.207 19.3527C126.37 15.015 120.929 12.8462 115.5 12.8462L115.513 12.8333Z"
fill="#0294E4"
/>
<path
d="M171.518 130.887L115.513 67.7087V165.884L171.518 130.887Z"
fill="#96BCC2"
/>
</svg>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function GoogleIcon(props: Readonly<IconProps>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
{...iconProperties(props)}
>
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
<path d="M1 1h22v22H1z" fill="none" />
</svg>
);
}
+36
View File
@@ -0,0 +1,36 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function JWTIcon(props: Readonly<IconProps>) {
return (
<svg
height="2500"
viewBox=".4 .3 99.7 100"
width="2500"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
>
<g fill="none">
<path
d="m57.8 27.2-.1-26.9h-15l.1 26.9 7.5 10.3zm-15 46.1v27h15v-27l-7.5-10.3z"
fill="#fff"
/>
<path
d="m57.8 73.3 15.8 21.8 12.1-8.8-15.8-21.8-12.1-3.9zm-15-46.1-15.9-21.8-12.1 8.8 15.8 21.8 12.2 3.9z"
fill="#00f2e6"
/>
<path
d="m30.6 36-25.6-8.3-4.6 14.2 25.6 8.4 12.1-4zm31.8 18.2 7.5 10.3 25.6 8.3 4.6-14.2-25.6-8.3z"
fill="#00b9f1"
/>
<path
d="m74.5 50.3 25.6-8.4-4.6-14.2-25.6 8.3-7.5 10.3zm-48.5 0-25.6 8.3 4.6 14.2 25.6-8.3 7.5-10.3z"
fill="#d63aff"
/>
<path
d="m30.6 64.5-15.8 21.8 12.1 8.8 15.9-21.8v-12.7zm39.3-28.5 15.8-21.8-12.1-8.8-15.8 21.8v12.7z"
fill="#fb015b"
/>
</g>
</svg>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function OktaIcon(props: Readonly<IconProps>) {
return (
<svg
width="215"
height="215"
viewBox="0 0 215 215"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
>
<path
d="M118.287 1.49306L113.865 55.9597C111.773 55.7208 109.681 55.6014 107.53 55.6014C104.841 55.6014 102.211 55.7806 99.6412 56.1986L97.1312 29.8014C97.0714 28.9653 97.7288 28.2486 98.5655 28.2486H103.048L100.896 1.55278C100.836 0.716667 101.494 0 102.271 0H116.913C117.749 0 118.407 0.716667 118.287 1.55278V1.49306ZM81.3538 4.18056C81.1147 3.40417 80.278 2.92639 79.5011 3.225L65.7557 8.24167C64.9788 8.54028 64.6202 9.43611 64.9788 10.1528L76.1544 34.5194L71.9112 36.0722C71.1343 36.3708 70.7758 37.2667 71.1343 37.9833L82.549 61.9319C86.6727 59.6625 91.1549 57.9306 95.8761 56.8556L81.4135 4.18056H81.3538ZM47.6475 19.35L79.2621 63.9028C75.258 66.5306 71.6124 69.6361 68.5048 73.2194L49.56 54.5861C48.9623 53.9889 49.0221 53.0333 49.6197 52.4958L53.086 49.6292L34.2607 30.5778C33.663 29.9806 33.7228 29.025 34.3802 28.4875L45.5558 19.1111C46.2132 18.5736 47.1097 18.6931 47.5878 19.35H47.6475ZM21.1129 45.15C20.4555 44.6722 19.4993 44.9111 19.0809 45.6278L11.7899 58.2889C11.3715 59.0056 11.6703 59.9014 12.3875 60.2597L36.6512 71.7264L34.3802 75.6083C33.9619 76.325 34.2607 77.2806 35.0376 77.5792L59.1818 88.6278C60.9149 84.1486 63.2456 79.9681 66.1143 76.2056L21.1129 45.15ZM3.24377 79.55C3.3633 78.7139 4.19998 78.2361 4.97689 78.4153L57.867 92.2111C56.4924 96.6903 55.7155 101.408 55.596 106.306L29.1211 104.156C28.2844 104.096 27.6868 103.319 27.8661 102.483L28.643 98.0639L1.92899 95.5556C1.09231 95.4958 0.554444 94.7194 0.67397 93.8833L3.18401 79.4903L3.24377 79.55ZM1.2716 115.264C0.434919 115.324 -0.102947 116.1 0.0165789 116.936L2.58638 131.329C2.70591 132.165 3.54259 132.643 4.3195 132.464L30.2566 125.715L31.0335 130.135C31.153 130.971 31.9897 131.449 32.7666 131.269L58.3451 124.222C56.851 119.803 55.8948 115.085 55.6558 110.247L1.21184 115.264H1.2716ZM9.75792 153.128C9.33958 152.411 9.63839 151.515 10.3555 151.157L59.7196 127.746C61.5723 132.165 64.0823 136.286 67.0705 139.989L45.4363 155.397C44.7789 155.875 43.8227 155.696 43.4044 154.979L41.1334 151.037L19.0809 16Line truncated
fill="white"
/>
</svg>
);
}
+9 -2
View File
@@ -15,7 +15,9 @@ export const OIDCError = () => {
const params = useSearchParams();
const errorParam = params.get("error");
const accessDenied = errorParam === "access_denied";
const invalidRequest = errorParam === "invalid_request";
const [title, setTitle] = useState(params.get("error_description"));
const errorDescription = params.get("error_description");
const { logout, login } = useOidc();
useEffect(() => {
@@ -72,9 +74,14 @@ export const OIDCError = () => {
</>
) : (
<>
<Paragraph className={"text-center mt-2"}>
<Paragraph className={"text-center mt-2 block"}>
There was an error logging you in. <br />
Error: {oidcUserLoadingState}
Error:{" "}
<span className={"inline capitalize"}>
{invalidRequest && errorDescription
? errorDescription
: oidcUserLoadingState}
</span>
</Paragraph>
<Button
variant={"primary"}
+5
View File
@@ -35,6 +35,11 @@ export const buttonVariants = cva(
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
secondaryLighter: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
"dark:bg-nb-gray-900/70 dark:text-gray-400 dark:border-gray-700/70 dark:hover:text-white dark:hover:bg-nb-gray-800/60",
],
input: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
+60 -20
View File
@@ -7,6 +7,7 @@ import React from "react";
interface Props extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
function Card({ children, className, ...props }: Props) {
return (
<div
@@ -32,6 +33,7 @@ type CardListItemProps = {
copy?: boolean;
copyText?: string;
tooltip?: boolean;
extraText?: string[];
};
function CardListItem({
@@ -41,9 +43,8 @@ function CardListItem({
copy = false,
copyText,
tooltip = true,
extraText = [],
}: CardListItemProps) {
const [, copyToClipBoard] = useCopyToClipboard(value as string);
return (
<li
className={cn(
@@ -52,29 +53,68 @@ function CardListItem({
)}
>
<div className={"flex gap-2.5 items-center text-sm"}>{label}</div>
<div
className={cn(
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
)}
onClick={() =>
copy &&
copyToClipBoard(
`${copyText ? copyText : label} has been copied to clipboard.`,
)
}
>
{tooltip ? (
<TextWithTooltip text={value as string} maxChars={40} />
) : (
value
)}
{copy && <Copy size={13} className={"shrink-0"} />}
<div className={"flex flex-col gap-2"}>
<CardTextItem
label={label}
value={value}
copy={copy}
copyText={copyText}
tooltip={tooltip}
/>
{extraText?.map((extraLabel, index) => (
<CardTextItem
key={index}
label={label}
value={extraLabel}
copy={copy}
copyText={copyText}
tooltip={tooltip}
/>
))}
</div>
</li>
);
}
type CardTextItemProps = {
label: React.ReactNode;
value: React.ReactNode;
copy?: boolean;
copyText?: string;
tooltip?: boolean;
};
const CardTextItem = ({
label,
value,
copy = false,
copyText,
tooltip = true,
}: CardTextItemProps) => {
const [, copyToClipBoard] = useCopyToClipboard(value as string);
return (
<div
className={cn(
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
)}
onClick={() =>
copy &&
copyToClipBoard(
`${copyText ? copyText : label} has been copied to clipboard.`,
)
}
>
{tooltip ? (
<TextWithTooltip text={value as string} maxChars={40} />
) : (
value
)}
{copy && <Copy size={13} className={"shrink-0"} />}
</div>
);
};
Card.List = CardList;
Card.ListItem = CardListItem;
Loaded 20 of 171 files, more files were not shown because too many files have changed in this diff. Show more