Compare commits

...
10 Commits
Author SHA1 Message Date
Eduard Gert 818ba5daa4 Allow wildcard dns zone records (#536) 2026-01-20 17:32:14 +01:00
Ali AmerandEduard Gert 3a30f76629 Add Frontend Support for Peer Debug Bundle Trigger and History (#485)
* implement debug ui

* update job ui

* Add type cell, show tooltip if peer is offline, add copy to clipboard for upload key, show error reason in tooltip

* update job event description

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-01-20 17:12:33 +01:00
Misha Bragin 34dc21c89d Add password change (embedded Idp) (#535) 2026-01-20 15:00:14 +01:00
Eduard Gert 2e37703622 Update CONTRIBUTOR_LICENSE_AGREEMENT.md (#534) 2026-01-19 14:55:04 +01:00
Eduard Gert 8aec338c43 Fix dns doc link (#533) 2026-01-19 10:01:55 +01:00
Viktor Liu f4f0c240fd Bump wasm to v0.63.0 (#531) 2026-01-19 09:49:26 +01:00
Viktor Liu 04e22a3c7e Enable SSH for Windows and Android peers (#532)
* Enable SSH for Windows and Android peers, hide update badge for temporary peers

* Fix RDP to use tcp protocol instead of netbird-ssh
2026-01-19 09:49:08 +01:00
Eduard Gert 54ef076303 Fix config vars (#529) 2026-01-16 19:59:42 +01:00
Eduard Gert 92676b6c38 Add DNS zones (#528) 2026-01-16 17:33:16 +01:00
Eduard Gert 3affa8908f Redirect /setup to /peers if no setup is required (#526)
* Redirect /setup to /peers if not setup is required

* Fix bad state while redirect

* Prevent redirect to /setup if already on /setup

* Fix loading state
2026-01-08 15:01:45 +01:00
82 changed files with 3357 additions and 192 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
## 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,
submitting this Agreement and NetBird GmbH, BrunnenstraĂźe 196, 10119 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
+1 -1
View File
@@ -15,4 +15,4 @@
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
"wasmPath": "$NETBIRD_WASM_PATH"
}
}
+21 -9
View File
@@ -56,6 +56,7 @@
"flowbite": "^1.8.1",
"flowbite-react": "^0.6.4",
"framer-motion": "^10.16.4",
"ip-address": "^10.1.0",
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
@@ -6598,15 +6599,12 @@
}
},
"node_modules/ip-address": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-7.1.0.tgz",
"integrity": "sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "1.1.2"
},
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 10"
"node": ">= 12"
}
},
"node_modules/ip-cidr": {
@@ -6621,6 +6619,19 @@
"node": ">=10.0.0"
}
},
"node_modules/ip-cidr/node_modules/ip-address": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-7.1.0.tgz",
"integrity": "sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==",
"license": "MIT",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "1.1.2"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -8561,7 +8572,8 @@
"node_modules/sprintf-js": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
"license": "BSD-3-Clause"
},
"node_modules/stable-hash": {
"version": "0.0.5",
+1
View File
@@ -61,6 +61,7 @@
"flowbite": "^1.8.1",
"flowbite-react": "^0.6.4",
"framer-motion": "^10.16.4",
"ip-address": "^10.1.0",
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
+3 -3
View File
@@ -7,7 +7,7 @@ import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon, ServerIcon } from "lucide-react";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import DNSIcon from "@/assets/icons/DNSIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
@@ -15,7 +15,7 @@ import { NameserverGroup } from "@/interfaces/Nameserver";
import PageContainer from "@/layouts/PageContainer";
const NameserverGroupTable = lazy(
() => import("@/modules/dns-nameservers/table/NameserverGroupTable"),
() => import("@/modules/dns/nameservers/table/NameserverGroupTable"),
);
export default function NameServers() {
@@ -40,7 +40,7 @@ export default function NameServers() {
href={"/dns/nameservers"}
label={"Nameservers"}
active
icon={<ServerIcon size={13} />}
icon={<DNSIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Nameservers</h1>
+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: `Zones - DNS - ${globalMetaTitle}`,
};
export default BlankLayout;
+70
View File
@@ -0,0 +1,70 @@
"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, { lazy, Suspense } from "react";
import DNSIcon from "@/assets/icons/DNSIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { DNS_ZONE_DOCS_LINK, DNSZone } from "@/interfaces/DNS";
import PageContainer from "@/layouts/PageContainer";
import { DNSZonesProvider } from "@/modules/dns/zones/DNSZonesProvider";
import DNSZoneIcon from "@/assets/icons/DNSZoneIcon";
const DNSZonesTable = lazy(
() => import("@/modules/dns/zones/table/DNSZonesTable"),
);
export default function DNSZonePage() {
const { permission } = usePermissions();
const { data: zones, isLoading } = useFetchApi<DNSZone[]>("/dns/zones");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item label={"DNS"} icon={<DNSIcon size={13} />} />
<Breadcrumbs.Item
href={"/dns/zones"}
label={"Zones"}
active
icon={<DNSZoneIcon size={16} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Zones</h1>
<Paragraph>
Manage DNS zones to control domain name resolution for your network.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink href={DNS_ZONE_DOCS_LINK} target={"_blank"}>
DNS Zones
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"DNS Zones"} hasAccess={permission?.dns?.read}>
<Suspense fallback={<SkeletonTable />}>
<DNSZonesProvider>
<DNSZonesTable
isLoading={isLoading}
headingTarget={portalTarget}
data={zones}
/>
</DNSZonesProvider>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+25
View File
@@ -15,6 +15,7 @@ import { useSearchParams } from "next/navigation";
import React, { useState } from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import DNSIcon from "@/assets/icons/DNSIcon";
import DNSZoneIcon from "@/assets/icons/DNSZoneIcon";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
@@ -24,6 +25,7 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { Group, GROUP_TOOLTIP_TEXT } from "@/interfaces/Group";
import PageContainer from "@/layouts/PageContainer";
import { GroupDNSZonesSection } from "@/modules/groups/details/GroupDNSZonesSection";
import { GroupNameserversSection } from "@/modules/groups/details/GroupNameserversSection";
import { GroupNetworkRoutesSection } from "@/modules/groups/details/GroupNetworkRoutesSection";
import { GroupPeersSection } from "@/modules/groups/details/GroupPeersSection";
@@ -134,7 +136,9 @@ const validAllGroupTabs = [
"resources",
"network-routes",
"nameservers",
"zones",
];
const validOtherGroupTabs = ["users", "peers", "setup-keys"];
const GroupOverviewTabs = ({ group }: { group: Group }) => {
@@ -162,6 +166,7 @@ const GroupOverviewTabs = ({ group }: { group: Group }) => {
const resourcesCount = groupDetails?.resources_count || 0;
const routesCount = groupDetails?.routes?.length || 0;
const nameserversCount = groupDetails?.nameservers?.length || 0;
const zonesCount = groupDetails?.zones?.length || 0;
const setupKeysCount = groupDetails?.setupKeys?.length || 0;
return (
@@ -249,6 +254,19 @@ const GroupOverviewTabs = ({ group }: { group: Group }) => {
{singularize("Nameservers", nameserversCount)}
</TabsTrigger>
<TabsTrigger
value={"zones"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<DNSZoneIcon
size={16}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Zones", zonesCount)}
</TabsTrigger>
{group.name !== "All" && (
<TabsTrigger
value={"setup-keys"}
@@ -304,6 +322,13 @@ const GroupOverviewTabs = ({ group }: { group: Group }) => {
/>
</TabsContent>
<TabsContent value={"zones"} className={"pb-8"}>
<GroupDNSZonesSection
zones={groupDetails?.zones}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"setup-keys"} className={"pb-8"}>
<GroupSetupKeysSection
setupKeys={groupDetails?.setupKeys}
+21 -62
View File
@@ -26,7 +26,6 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import dayjs from "dayjs";
import { isEmpty, trim } from "lodash";
import {
@@ -41,6 +40,7 @@ import {
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
RadioTowerIcon,
TimerResetIcon,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
@@ -61,12 +61,13 @@ 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";
import { PeerRemoteJobsSection } from "@/modules/peer/PeerRemoteJobsSection";
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
import Link from "next/link";
import { PeerExpirationSettings } from "@/modules/peer/PeerExpirationSettings";
export default function PeerPage() {
const queryParameter = useSearchParams();
@@ -80,12 +81,6 @@ export default function PeerPage() {
useRedirect("/peers", false, !peerId || isRestricted);
const peerKey = useMemo(() => {
let id = peer?.id ?? "";
let expiration = peer?.login_expiration_enabled ? "1" : "0";
return `${id}-${expiration}`;
}, [peer]);
if (isRestricted) {
return (
<PageContainer>
@@ -106,7 +101,7 @@ export default function PeerPage() {
return peer && !isLoading ? (
<PeerProvider peer={peer} key={peerId} isPeerDetailPage={true}>
<PeerOverview key={peerKey} />
<PeerOverview key={peer?.id} />
</PeerProvider>
) : (
<FullScreenLoading />
@@ -142,12 +137,6 @@ const PeerGeneralInformation = () => {
const { peer, user, peerGroups, update } = usePeer();
const [name, setName] = useState(peer.name);
const [showEditNameModal, setShowEditNameModal] = useState(false);
const [loginExpiration, setLoginExpiration] = useState(
peer.login_expiration_enabled,
);
const [inactivityExpiration, setInactivityExpiration] = useState(
peer.inactivity_expiration_enabled,
);
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
useGroupHelper({
initial: peerGroups?.filter((g) => g?.name !== "All"),
@@ -159,8 +148,6 @@ const PeerGeneralInformation = () => {
*/
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
selectedGroups,
loginExpiration,
inactivityExpiration,
]);
const updatePeer = async (newName?: string) => {
@@ -170,8 +157,6 @@ const PeerGeneralInformation = () => {
if (permission.peers.update) {
const updateRequest = update({
name: newName ?? name,
loginExpiration,
inactivityExpiration,
});
batchCall = groupCalls ? [...groupCalls, updateRequest] : [updateRequest];
} else {
@@ -184,11 +169,7 @@ const PeerGeneralInformation = () => {
promise: Promise.all(batchCall).then(() => {
mutate("/peers/" + peer.id);
mutate("/groups");
updateHasChangedRef([
selectedGroups,
loginExpiration,
inactivityExpiration,
]);
updateHasChangedRef([selectedGroups]);
}),
loadingMessage: "Saving the peer...",
});
@@ -284,41 +265,7 @@ const PeerGeneralInformation = () => {
<PeerInformationCard peer={peer} />
<div className={"flex flex-col gap-6 lg:w-1/2 transition-all"}>
<div>
<PeerExpirationToggle
peer={peer}
value={loginExpiration}
icon={<TimerResetIcon size={16} />}
onChange={(state) => {
setLoginExpiration(state);
!state && setInactivityExpiration(false);
}}
/>
{permission.peers.update && !!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>
<PeerExpirationSettings />
<PeerSSHToggle />
@@ -382,6 +329,13 @@ const PeerOverviewTabs = () => {
Accessible Peers
</TabsTrigger>
)}
{peer?.id && permission.peers.delete && (
<TabsTrigger value={"peer-job"}>
<RadioTowerIcon size={16} />
Remote Jobs
</TabsTrigger>
)}
</TabsList>
{permission.routes.read && (
@@ -395,6 +349,11 @@ const PeerOverviewTabs = () => {
<AccessiblePeersSection peerID={peer.id} />
</TabsContent>
)}
{peer.id && permission.peers.delete && (
<TabsContent value={"peer-job"} className={"pb-8"}>
<PeerRemoteJobsSection peerID={peer.id} />
</TabsContent>
)}
</Tabs>
);
};
@@ -582,9 +541,9 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
peer.connected
? "just now"
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(peer.last_seen) +
")"
" (" +
dayjs().to(peer.last_seen) +
")"
}
/>
+1 -5
View File
@@ -20,7 +20,6 @@ import {
useNetBirdClient,
} from "@/modules/remote-access/useNetBirdClient";
import { cn } from "@utils/helpers";
import { isNetbirdSSHProtocolSupported } from "@utils/version";
export default function RDPPage() {
const { peerId } = useRDPQueryParams();
@@ -85,11 +84,8 @@ function RDPSession({ peer }: Props) {
try {
setCredentials(rdpCredentials);
setIsNetBirdConnecting(true);
const protocol = isNetbirdSSHProtocolSupported(peer.version)
? "netbird-ssh"
: "tcp";
await client.connectTemporary(peer.id, [
`${protocol}/${rdpCredentials.port}`,
`tcp/${rdpCredentials.port}`,
]);
setIsNetBirdConnecting(false);
} catch (error) {
+16 -1
View File
@@ -1,7 +1,22 @@
"use client";
import InstanceSetupWizard from "@/modules/instance-setup/InstanceSetupWizard";
import { useInstanceSetup } from "@/contexts/InstanceSetupProvider";
import { useRouter } from "next/navigation";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useEffect } from "react";
export default function SetupPage() {
return <InstanceSetupWizard />;
const { setupRequired, loading } = useInstanceSetup();
const router = useRouter();
useEffect(() => {
if (!loading && !setupRequired) router.replace("/peers");
}, [loading, setupRequired]);
return loading || !setupRequired ? (
<FullScreenLoading />
) : (
<InstanceSetupWizard />
);
}
+19
View File
@@ -0,0 +1,19 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function DNSZoneIcon(props: IconProps) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
>
<path
fillRule="evenodd"
d="M5 5a2 2 0 0 0-2 2v3a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V7a2 2 0 0 0-2-2H5Zm9 2a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H14Zm3 0a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17ZM3 17v-3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Zm11-2a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H14Zm3 0a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17Z"
clipRule="evenodd"
/>
</svg>
);
}
+30
View File
@@ -0,0 +1,30 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function SlackIcon(props: Readonly<IconProps>) {
return (
<svg
width="127"
height="127"
viewBox="0 0 127 127"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
>
<path
d="M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z"
fill="#E01E5A"
/>
<path
d="M47 27c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.5 39.7.6 47 .6c7.3 0 13.2 5.9 13.2 13.2V27H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.1.7 54.2.7 46.9c0-7.3 5.9-13.2 13.2-13.2H47z"
fill="#36C5F0"
/>
<path
d="M99.9 46.9c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V46.9zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.8C66.9 6.5 72.8.6 80.1.6c7.3 0 13.2 5.9 13.2 13.2v33.1z"
fill="#2EB67D"
/>
<path
d="M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z"
fill="#ECB22E"
/>
</svg>
);
}
+1
View File
@@ -76,6 +76,7 @@ export const buttonVariants = cva(
"default-outline": [
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
"dark:bg-transparent dark:text-nb-gray-400 dark:border-transparent dark:hover:text-white dark:hover:bg-nb-gray-900/30 dark:hover:border-nb-gray-800/50",
"data-[state=open]:dark:text-white data-[state=open]:dark:bg-nb-gray-900/30 data-[state=open]:dark:border-nb-gray-800/50",
],
danger: [
"", // TODO - add danger button styles for light mode
+45 -17
View File
@@ -93,25 +93,53 @@ const DropdownMenuItem = React.forwardRef<
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "danger";
href?: string;
target?: string;
rel?: string;
}
>(({ className, inset, variant = "default", onClick, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex select-none items-center rounded-md pr-2 pl-3 py-1.5 text-sm outline-none",
"transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer ",
inset && "pl-8",
menuItemVariants({ variant }),
>(
(
{
className,
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClick && onClick(e);
}}
{...props}
/>
));
inset,
variant = "default",
onClick,
href,
target,
rel,
...props
},
ref,
) => {
return (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex select-none items-center rounded-md pr-2 pl-3 py-1.5 text-sm outline-none",
"transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer ",
inset && "pl-8",
menuItemVariants({ variant }),
className,
)}
onClick={(e) => {
if (href) return;
e.preventDefault();
e.stopPropagation();
onClick && onClick(e);
}}
{...props}
>
{href ? (
<a href={href} target={target} rel={rel}>
{props.children}
</a>
) : (
props.children
)}
</DropdownMenuPrimitive.Item>
);
},
);
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
+1 -1
View File
@@ -17,7 +17,7 @@ export interface InputProps
icon?: React.ReactNode;
error?: string;
errorTooltip?: boolean;
errorTooltipPosition?: "top" | "top-right";
errorTooltipPosition?: "top" | "top-right" | "bottom";
prefixClassName?: string;
showPasswordToggle?: boolean;
}
+36
View File
@@ -0,0 +1,36 @@
import { cn } from "@utils/helpers";
import * as React from "react";
export const TooltipListItem = ({
icon,
label,
value,
className,
labelClassName,
}: {
icon?: React.ReactNode;
label: string;
value: string | React.ReactNode;
className?: string;
labelClassName?: string;
}) => {
return (
<div
className={cn(
"flex justify-between gap-12 border-b border-nb-gray-920 py-2 px-4 last:border-b-0",
className,
)}
>
<div
className={cn(
"flex items-center gap-2 text-nb-gray-100 font-medium",
labelClassName,
)}
>
{icon}
{label}
</div>
<div className={"text-nb-gray-300"}>{value}</div>
</div>
);
};
+1 -1
View File
@@ -104,7 +104,7 @@ const TableRow = React.forwardRef<
" transition-colors group/table-row data-[state=selected]:bg-neutral-100 dark:data-[state=selected]:bg-nb-gray-930/70",
"dark:data-[state=selected]:border-nb-gray-900",
minimal
? "dark:hover:bg-nb-gray-900/10"
? "dark:hover:bg-nb-gray-910/[15%]"
: "border-b dark:border-zinc-700/40 dark:hover:bg-nb-gray-940 hover:bg-neutral-100/50",
className,
)}
+145
View File
@@ -0,0 +1,145 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@components/DropdownMenu";
import {
ArrowUpRightIcon,
BookText,
CircleQuestionMark,
MailIcon,
MessageSquareShare,
MessagesSquareIcon,
TriangleAlert,
} from "lucide-react";
import { useState } from "react";
import Button from "@components/Button";
import { cn } from "@utils/helpers";
import SlackIcon from "@/assets/icons/SlackIcon";
import { isNetBirdHosted } from "@utils/netbird";
export default function HelpAndSupportButton() {
const [dropdownOpen, setDropdownOpen] = useState(false);
return (
<DropdownMenu
modal={false}
open={dropdownOpen}
onOpenChange={setDropdownOpen}
>
<DropdownMenuTrigger asChild={true}>
<Button
size={"xs"}
variant={"default-outline"}
className={cn(
"!rounded-full h-[38px] w-[38px] !p-0",
dropdownOpen && "text-white",
)}
>
<CircleQuestionMark size={18} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1 px-1">
<div className="text-sm font-normal leading-none text-nb-gray-200 py-1">
Help and Support
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
href="https://docs.netbird.io/"
target="_blank"
rel="noopener noreferrer"
asChild
>
<div className={"flex gap-3 items-center"}>
<BookText size={14} />
Documentation
</div>
<DropdownMenuShortcut>
<ArrowUpRightIcon size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem
href="https://docs.netbird.io/help/troubleshooting-client"
target="_blank"
rel="noopener noreferrer"
asChild
>
<div className={"flex gap-3 items-center"}>
<TriangleAlert size={14} />
Troubleshooting
</div>
<DropdownMenuShortcut>
<ArrowUpRightIcon size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
{isNetBirdHosted() && (
<DropdownMenuItem href="mailto:support@netbird.io?subject=Support Request">
<div className={"flex gap-3 items-center"}>
<MailIcon size={14} />
support@netbird.io
</div>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
href="https://forum.netbird.io/"
target="_blank"
rel="noopener noreferrer"
asChild
>
<div className={"flex gap-3 items-center"}>
<MessagesSquareIcon size={14} />
NetBird Forum
</div>
<DropdownMenuShortcut>
<ArrowUpRightIcon size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem
href="https://docs.netbird.io/slack-url"
target="_blank"
rel="noopener noreferrer"
asChild
>
<div className={"flex gap-3 items-center"}>
<SlackIcon size={14} />
NetBird Slack
</div>
<DropdownMenuShortcut>
<ArrowUpRightIcon size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
href={"https://forms.gle/TeLw2zrXEdw6RcQ36"}
target={"_blank"}
rel="noopener noreferrer"
asChild
>
<div className={"flex gap-3 items-center"}>
<MessageSquareShare size={14} />
Feedback
</div>
<DropdownMenuShortcut>
<ArrowUpRightIcon size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+6 -1
View File
@@ -14,7 +14,9 @@ type Props = {
className?: string;
hasFiltersApplied?: boolean;
onResetFilters?: () => void;
contentClassName?: string;
};
export default function NoResults({
icon,
title = "Could not find any results",
@@ -23,6 +25,7 @@ export default function NoResults({
className,
hasFiltersApplied = false,
onResetFilters,
contentClassName,
}: Props) {
const router = useRouter();
const pathname = usePathname();
@@ -65,7 +68,9 @@ export default function NoResults({
<Skeleton className={"w-full"} height={70} duration={4} />
</div>
</div>
<div className={cn("max-w-md mx-auto relative z-20 py-6")}>
<div
className={cn("max-w-md mx-auto relative z-20 py-6", contentClassName)}
>
<div
className={
"mx-auto w-14 h-14 bg-nb-gray-930 flex items-center justify-center mb-3 rounded-md"
Loaded 20 of 82 files, more files were not shown because too many files have changed in this diff. Show more