mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3e75b640 | ||
|
|
f8281c8057 | ||
|
|
c1fcadaefe | ||
|
|
a0c4520f4b | ||
|
|
76ef50a886 |
No files matched your search
@@ -13,4 +13,4 @@ jobs:
|
||||
with:
|
||||
only_warn: 1
|
||||
skip: package-lock.json,*.svg
|
||||
ignore_words_list: mappin
|
||||
ignore_words_list: mappin, allTime
|
||||
@@ -5,6 +5,7 @@ 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";
|
||||
@@ -20,6 +21,9 @@ const AccessControlTable = lazy(
|
||||
export default function AccessControlPage() {
|
||||
const { data: policies, isLoading } = useFetchApi<Policy[]>("/policies");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<GroupsProvider>
|
||||
@@ -31,12 +35,7 @@ export default function AccessControlPage() {
|
||||
icon={<AccessControlIcon size={14} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
<h1>
|
||||
{policies && policies.length > 1
|
||||
? `${policies.length} Access Control Policies`
|
||||
: "Access Control Policies"}
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Access Control Policies</h1>
|
||||
<Paragraph>
|
||||
Create rules to manage access in your network and define what peers
|
||||
can connect.
|
||||
@@ -57,7 +56,11 @@ export default function AccessControlPage() {
|
||||
<RestrictedAccess page={"Access Control"}>
|
||||
<PoliciesProvider>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<AccessControlTable isLoading={isLoading} policies={policies} />
|
||||
<AccessControlTable
|
||||
isLoading={isLoading}
|
||||
policies={policies}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</PoliciesProvider>
|
||||
</RestrictedAccess>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -15,6 +16,9 @@ import ActivityTable from "@/modules/activity/ActivityTable";
|
||||
export default function Activity() {
|
||||
const { data: events, isLoading } = useFetchApi<ActivityEvent[]>("/events");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
@@ -25,11 +29,7 @@ export default function Activity() {
|
||||
icon={<ActivityIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>
|
||||
{events && events.length > 1
|
||||
? `${events.length} Activity Events`
|
||||
: "Activity Events"}
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Activity Events</h1>
|
||||
<Paragraph>
|
||||
Here you can see all the account and network activity events.
|
||||
</Paragraph>
|
||||
@@ -48,7 +48,11 @@ export default function Activity() {
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess page={"Activity"}>
|
||||
<ActivityTable events={events} isLoading={isLoading} />
|
||||
<ActivityTable
|
||||
events={events}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ 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, ServerIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
@@ -20,6 +21,9 @@ export default function NameServers() {
|
||||
const { data: nameserverGroups, isLoading } =
|
||||
useFetchApi<NameserverGroup[]>("/dns/nameservers");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
@@ -36,11 +40,7 @@ export default function NameServers() {
|
||||
icon={<ServerIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>
|
||||
{nameserverGroups && nameserverGroups.length > 1
|
||||
? `${nameserverGroups.length} Nameservers`
|
||||
: "Nameservers"}
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Nameservers</h1>
|
||||
<Paragraph>
|
||||
Add nameservers for domain name resolution in your NetBird network.
|
||||
</Paragraph>
|
||||
@@ -62,6 +62,7 @@ export default function NameServers() {
|
||||
<NameserverGroupTable
|
||||
nameserverGroups={nameserverGroups}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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";
|
||||
@@ -23,6 +24,9 @@ export default function NetworkRoutes() {
|
||||
const { data: routes, isLoading } = useFetchApi<Route[]>("/routes");
|
||||
const groupedRoutes = useGroupedRoutes({ routes });
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<RoutesProvider>
|
||||
@@ -35,11 +39,7 @@ export default function NetworkRoutes() {
|
||||
icon={<NetworkRoutesIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>
|
||||
{groupedRoutes && groupedRoutes.length > 1
|
||||
? `${groupedRoutes.length} Network Routes`
|
||||
: "Network Routes"}
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Network Routes</h1>
|
||||
<Paragraph>
|
||||
Network routes allow you to access other networks like LANs and
|
||||
VPCs without installing NetBird on every resource.
|
||||
@@ -65,6 +65,7 @@ export default function NetworkRoutes() {
|
||||
isLoading={isLoading}
|
||||
groupedRoutes={groupedRoutes}
|
||||
routes={routes}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
|
||||
@@ -23,6 +23,7 @@ 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";
|
||||
@@ -54,15 +55,12 @@ import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
|
||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { AddExitNodeButton } from "@/modules/exit-node/AddExitNodeButton";
|
||||
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
|
||||
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
||||
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
|
||||
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
|
||||
|
||||
export default function PeerPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
@@ -72,7 +70,7 @@ export default function PeerPage() {
|
||||
useRedirect("/peers", false, !peerId);
|
||||
|
||||
return peer && !isLoading ? (
|
||||
<PeerProvider peer={peer}>
|
||||
<PeerProvider peer={peer} key={peerId}>
|
||||
<PeerOverview />
|
||||
</PeerProvider>
|
||||
) : (
|
||||
@@ -133,7 +131,6 @@ function PeerOverview() {
|
||||
};
|
||||
|
||||
const { isUser } = useLoggedInUser();
|
||||
const hasExitNodes = useHasExitNodes(peer);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -336,30 +333,19 @@ function PeerOverview() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{isLinux && !isUser ? (
|
||||
<div className={"px-8 py-6"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div>
|
||||
<h2>Network Routes</h2>
|
||||
<Paragraph>
|
||||
Access other networks without installing NetBird on every
|
||||
resource.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div className={"gap-4 flex"}>
|
||||
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
|
||||
<AddRouteDropdownButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PeerRoutesTable peer={peer} />
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<Separator />
|
||||
<PeerNetworkRoutesSection peer={peer} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{peer?.id && (
|
||||
<>
|
||||
<Separator />
|
||||
<AccessiblePeersSection peerID={peer.id} />
|
||||
</>
|
||||
)}
|
||||
</RoutesProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ 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, ShieldCheck } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
@@ -21,6 +22,9 @@ export default function PostureChecksPage() {
|
||||
const { data: postureChecks, isLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<GroupsProvider>
|
||||
@@ -38,17 +42,16 @@ export default function PostureChecksPage() {
|
||||
icon={<ShieldCheck size={15} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>
|
||||
{postureChecks && postureChecks.length > 1
|
||||
? `${postureChecks.length} Posture Checks`
|
||||
: "Posture Checks"}
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Posture Checks</h1>
|
||||
<Paragraph>
|
||||
Use posture checks to further restrict access in your network.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
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
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
@@ -60,6 +63,7 @@ export default function PostureChecksPage() {
|
||||
<PoliciesProvider>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PostureCheckTable
|
||||
headingTarget={portalTarget}
|
||||
isLoading={isLoading}
|
||||
postureChecks={postureChecks}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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, useMemo } from "react";
|
||||
@@ -38,6 +39,9 @@ export default function SetupKeys() {
|
||||
});
|
||||
}, [setupKeys, groups]);
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
@@ -48,11 +52,7 @@ export default function SetupKeys() {
|
||||
icon={<SetupKeysIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>
|
||||
{setupKeys && setupKeys.length > 1
|
||||
? `${setupKeys.length} Setup Keys`
|
||||
: "Setup Keys"}
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Setup Keys</h1>
|
||||
<Paragraph>
|
||||
Setup keys are pre-authentication keys that allow to register new
|
||||
machines in your network.
|
||||
@@ -74,6 +74,7 @@ export default function SetupKeys() {
|
||||
<RestrictedAccess page={"Setup Keys"}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<SetupKeysTable
|
||||
headingTarget={portalTarget}
|
||||
setupKeys={setupKeysWithGroups}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 { IconSettings2 } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
@@ -22,6 +23,9 @@ export default function ServiceUsers() {
|
||||
"/users?service_user=true",
|
||||
);
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
@@ -38,11 +42,7 @@ export default function ServiceUsers() {
|
||||
icon={<IconSettings2 size={17} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>
|
||||
{users && users.length > 1
|
||||
? `${users.length} Service Users`
|
||||
: "Service Users"}
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Service Users</h1>
|
||||
<Paragraph>
|
||||
Use service users to create API tokens and avoid losing automated
|
||||
access.
|
||||
@@ -61,7 +61,11 @@ export default function ServiceUsers() {
|
||||
</div>
|
||||
<RestrictedAccess page={"Service Users"}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ServiceUsersTable users={users} isLoading={isLoading} />
|
||||
<ServiceUsersTable
|
||||
users={users}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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, User2 } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
@@ -19,6 +20,9 @@ export default function TeamUsers() {
|
||||
"/users?service_user=false",
|
||||
);
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
@@ -35,7 +39,7 @@ export default function TeamUsers() {
|
||||
icon={<User2 size={16} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>{users && users.length > 1 ? `${users.length} Users` : "Users"}</h1>
|
||||
<h1 ref={headingRef}>Users</h1>
|
||||
<Paragraph>
|
||||
Manage users and their permissions. Same-domain email users are added
|
||||
automatically on first sign-in.
|
||||
@@ -54,7 +58,11 @@ export default function TeamUsers() {
|
||||
</div>
|
||||
<RestrictedAccess page={"Users"}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<UsersTable users={users} isLoading={isLoading} />
|
||||
<UsersTable
|
||||
users={users}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
|
||||
@@ -5,9 +5,17 @@ import NetBirdLogo from "@/assets/netbird.svg";
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
function NetBirdIcon({ size = 16 }: Props) {
|
||||
return <Image src={NetBirdLogo} alt={"Netbird Icon"} width={size} />;
|
||||
function NetBirdIcon({ size = 16, className }: Props) {
|
||||
return (
|
||||
<Image
|
||||
src={NetBirdLogo}
|
||||
alt={"Netbird Icon"}
|
||||
width={size}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NetBirdIcon);
|
||||
@@ -2,7 +2,7 @@ import { cn } from "@utils/helpers";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
type BadgeVariants = VariantProps<typeof variants>;
|
||||
export type BadgeVariants = VariantProps<typeof variants>;
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement>, BadgeVariants {
|
||||
children: React.ReactNode;
|
||||
@@ -22,6 +22,9 @@ const variants = cva("", {
|
||||
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
|
||||
yellow: ["bg-yellow-950 border-yellow-500 border text-yellow-400"],
|
||||
gray: ["bg-nb-gray-930/60 border-nb-gray-800/40 text-nb-gray-300 border"],
|
||||
grayer: [
|
||||
"bg-nb-gray-900/40 border-nb-gray-800/40 text-nb-gray-300 border",
|
||||
],
|
||||
"gray-ghost": [
|
||||
"bg-nb-gray-900 border-nb-gray-800 text-nb-gray-300 border border-nb-gray-800/50",
|
||||
],
|
||||
@@ -37,6 +40,7 @@ const variants = cva("", {
|
||||
"blue-darker": ["hover:bg-sky-800"],
|
||||
red: ["hover:bg-red-950/40"],
|
||||
gray: ["hover:bg-nb-gray-900"],
|
||||
grayer: ["hover:bg-nb-gray-900"],
|
||||
"gray-ghost": ["hover:bg-nb-gray-900"],
|
||||
green: ["hover:bg-green-950/50"],
|
||||
netbird: ["hover:bg-netbird-950/50"],
|
||||
@@ -50,7 +54,7 @@ export default function Badge({
|
||||
variant = "blue",
|
||||
useHover = false,
|
||||
...props
|
||||
}: Props) {
|
||||
}: Readonly<Props>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -49,6 +49,10 @@ export const buttonVariants = cva(
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
"dark:focus:ring-zinc-800/50 dark:bg-white dark:text-gray-800 dark:border-gray-700/40 dark:hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300",
|
||||
],
|
||||
white: [
|
||||
"focus:ring-white/50 bg-white text-gray-800 border-white outline-none hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300",
|
||||
"disabled:dark:bg-nb-gray-900 disabled:dark:text-nb-gray-300 disabled:dark:border-nb-gray-900",
|
||||
],
|
||||
outline: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
"dark:focus:ring-zinc-800/50 dark:bg-transparent dark:text-netbird dark:border-netbird dark:hover:bg-nb-gray-900/30",
|
||||
@@ -69,6 +73,7 @@ export const buttonVariants = cva(
|
||||
},
|
||||
size: {
|
||||
xs: "text-xs py-2 px-4",
|
||||
xs2: "text-[0.78rem] py-2 px-4",
|
||||
sm: "text-sm py-2.5 px-4",
|
||||
md: "text-md py-2.5 px-4",
|
||||
lg: "text-lg py-2.5 px-4",
|
||||
|
||||
@@ -2,19 +2,41 @@
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { Check } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type CheckboxVariants = VariantProps<typeof variants>;
|
||||
|
||||
const variants = cva([], {
|
||||
variants: {
|
||||
variant: {
|
||||
default: [
|
||||
"dark:data-[state=unchecked]:bg-nb-gray-950 dark:border-nb-gray-900 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 ",
|
||||
"dark:data-[state=checked]:bg-netbird dark:data-[state=checked]:text-neutral-50",
|
||||
],
|
||||
tableCell: [
|
||||
"dark:data-[state=unchecked]:bg-nb-gray-920 dark:border-nb-gray-800 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 ",
|
||||
"dark:data-[state=checked]:bg-netbird dark:data-[state=checked]:text-neutral-50",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> &
|
||||
CheckboxVariants
|
||||
>(({ className, variant = "default", ...props }, ref) => (
|
||||
<div className={"h-5 w-5"}>
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"dark:data-[state=unchecked]:bg-nb-gray-950",
|
||||
"peer h-5 w-5 shrink-0 rounded-[4px] border border-neutral-900 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-nb-gray-900 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-netbird dark:data-[state=checked]:text-neutral-50",
|
||||
variants({ variant }),
|
||||
"border-neutral-900",
|
||||
"peer h-5 w-5 shrink-0 rounded-[4px] border",
|
||||
"ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-xs flex justify-between py-2 px-3 cursor-pointer items-center rounded-md",
|
||||
"bg-transparent dark:aria-selected:bg-nb-gray-800/50",
|
||||
"bg-transparent dark:aria-selected:bg-nb-gray-800/20",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Calendar } from "@components/ui/Calendar";
|
||||
import { cn } from "@utils/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import { Calendar as CalendarIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { DateRange } from "react-day-picker";
|
||||
|
||||
interface Props {
|
||||
@@ -15,38 +15,145 @@ interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultRanges = {
|
||||
today: {
|
||||
from: dayjs().startOf("day").toDate(),
|
||||
to: dayjs().endOf("day").toDate(),
|
||||
},
|
||||
yesterday: {
|
||||
from: dayjs().subtract(1, "day").startOf("day").toDate(),
|
||||
to: dayjs().subtract(1, "day").endOf("day").toDate(),
|
||||
},
|
||||
last14Days: {
|
||||
from: dayjs().subtract(14, "day").startOf("day").toDate(),
|
||||
to: dayjs().endOf("day").toDate(),
|
||||
},
|
||||
lastMonth: {
|
||||
from: dayjs().subtract(1, "month").startOf("day").toDate(),
|
||||
to: dayjs().endOf("day").toDate(),
|
||||
},
|
||||
allTime: {
|
||||
from: dayjs("1970-01-01").startOf("day").toDate(),
|
||||
to: dayjs().endOf("day").toDate(),
|
||||
},
|
||||
};
|
||||
|
||||
const isEqualDateRange = (a: DateRange | undefined, b: DateRange) => {
|
||||
if (!a) return false;
|
||||
const aFromDay = dayjs(a.from).format("YYYY-MM-DD");
|
||||
const aToDay = dayjs(a.to).format("YYYY-MM-DD");
|
||||
const bFromDay = dayjs(b.from).format("YYYY-MM-DD");
|
||||
const bToDay = dayjs(b.to).format("YYYY-MM-DD");
|
||||
return aFromDay === bFromDay && aToDay === bToDay;
|
||||
};
|
||||
|
||||
export function DatePickerWithRange({ className, value, onChange }: Props) {
|
||||
const isActive = useMemo(() => {
|
||||
return {
|
||||
today: isEqualDateRange(value, defaultRanges.today),
|
||||
yesterday: isEqualDateRange(value, defaultRanges.yesterday),
|
||||
last14Days: isEqualDateRange(value, defaultRanges.last14Days),
|
||||
lastMonth: isEqualDateRange(value, defaultRanges.lastMonth),
|
||||
allTime: isEqualDateRange(value, defaultRanges.allTime),
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
const displayDateValue = useMemo(() => {
|
||||
if (!value) return "Select date range";
|
||||
|
||||
if (isActive.allTime) return "All Time";
|
||||
if (isActive.lastMonth) return "Last Month";
|
||||
if (isActive.last14Days) return "Last 14 Days";
|
||||
if (isActive.yesterday) return "Yesterday";
|
||||
if (isActive.today) return "Today";
|
||||
|
||||
if (!value.to) return dayjs(value.from).format("MMM DD, YYYY").toString();
|
||||
return `${dayjs(value.from).format("MMM DD, YYYY")} - ${dayjs(
|
||||
value.to,
|
||||
).format("MMM DD, YYYY")}`;
|
||||
}, [value, isActive]);
|
||||
|
||||
const [calendarOpen, setCalendarOpen] = useState(false);
|
||||
|
||||
const updateRangeAndClose = (range: DateRange) => {
|
||||
setCalendarOpen(false);
|
||||
onChange?.(range);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("grid gap-2", className)}>
|
||||
<Popover>
|
||||
<Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date"
|
||||
variant={"secondary"}
|
||||
className={cn("w-[260px] justify-start text-left font-normal")}
|
||||
className={cn("max-w-[260px] justify-start text-left font-normal")}
|
||||
>
|
||||
<CalendarIcon size={16} />
|
||||
{value?.from ? (
|
||||
value.to ? (
|
||||
<>
|
||||
{dayjs(value.from).format("MMM DD, YYYY")} -{" "}
|
||||
{dayjs(value.to).format("MMM DD, YYYY")}
|
||||
</>
|
||||
) : (
|
||||
<>{dayjs(value.from, "LLL dd, y").toString()}</>
|
||||
)
|
||||
) : (
|
||||
<span>Pick your date range</span>
|
||||
)}
|
||||
<CalendarIcon size={16} className={"shrink-0"} />
|
||||
{displayDateValue}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start" sideOffset={10}>
|
||||
<div
|
||||
className={
|
||||
"px-4 py-3 flex flex-wrap gap-2 max-w-[280px] sm:max-w-none border-b border-nb-gray-800 items-center justify-between w-full"
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<CalendarButton
|
||||
label={
|
||||
<>
|
||||
<CalendarIcon size={14} className={"shrink-0"} />
|
||||
All Time
|
||||
</>
|
||||
}
|
||||
active={isActive.allTime}
|
||||
onClick={() => updateRangeAndClose(defaultRanges.allTime)}
|
||||
/>
|
||||
</div>
|
||||
<div className={"flex gap-2 flex-wrap"}>
|
||||
<CalendarButton
|
||||
label={"Last Month"}
|
||||
active={isActive.lastMonth}
|
||||
onClick={() => updateRangeAndClose(defaultRanges.lastMonth)}
|
||||
/>
|
||||
<CalendarButton
|
||||
label={"Last 14 Days"}
|
||||
active={isActive.last14Days}
|
||||
onClick={() => updateRangeAndClose(defaultRanges.last14Days)}
|
||||
/>
|
||||
<CalendarButton
|
||||
label={"Yesterday"}
|
||||
active={isActive.yesterday}
|
||||
onClick={() => updateRangeAndClose(defaultRanges.yesterday)}
|
||||
/>
|
||||
<CalendarButton
|
||||
label={"Today"}
|
||||
active={isActive.today}
|
||||
onClick={() => updateRangeAndClose(defaultRanges.today)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={value?.from}
|
||||
selected={value}
|
||||
onSelect={onChange}
|
||||
onSelect={(range) => {
|
||||
let from =
|
||||
range && range.from
|
||||
? dayjs(range.from).startOf("day").toDate()
|
||||
: undefined;
|
||||
let to =
|
||||
range && range.to
|
||||
? dayjs(range.to).endOf("day").toDate()
|
||||
: undefined;
|
||||
if (!from && !to) {
|
||||
onChange?.(undefined);
|
||||
return;
|
||||
}
|
||||
onChange?.({ from, to });
|
||||
}}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
@@ -54,3 +161,25 @@ export function DatePickerWithRange({ className, value, onChange }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CalendarButtonProps = {
|
||||
label: string | React.ReactNode;
|
||||
onClick: () => void;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
function CalendarButton({ label, onClick, active }: CalendarButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"py-1.5 leading-none px-2.5 rounded-md text-center text-xs transition-all flex gap-2",
|
||||
active
|
||||
? "bg-nb-gray-800 text-white"
|
||||
: "bg-transparent text-nb-gray-300 hover:bg-nb-gray-900 hover:text-nb-gray-100",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const DisableDarkReader = () => {
|
||||
useEffect(() => {
|
||||
try {
|
||||
const lock = document.createElement("meta");
|
||||
lock.name = "darkreader-lock";
|
||||
document.head.appendChild(lock);
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -31,7 +31,7 @@ export default function FancyToggleSwitch({
|
||||
value
|
||||
? "border-nb-gray-800 bg-nb-gray-900/70"
|
||||
: "border-nb-gray-800 bg-nb-gray-900/30 hover:bg-nb-gray-900/40",
|
||||
disabled && "opacity-30 pointer-events-none",
|
||||
disabled && "opacity-50 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div className={"flex justify-between gap-10 "}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@components/Tooltip";
|
||||
import { TooltipProps } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "@utils/helpers";
|
||||
import React, { useState } from "react";
|
||||
|
||||
@@ -19,7 +20,9 @@ type Props = {
|
||||
align?: "end" | "center" | "start";
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
keepOpen?: boolean;
|
||||
};
|
||||
customOpen?: boolean;
|
||||
customOnOpenChange?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
} & TooltipProps;
|
||||
export default function FullTooltip({
|
||||
children,
|
||||
content,
|
||||
@@ -32,6 +35,8 @@ export default function FullTooltip({
|
||||
align = "center",
|
||||
side = "top",
|
||||
keepOpen = false,
|
||||
customOpen,
|
||||
customOnOpenChange,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(!!keepOpen);
|
||||
|
||||
@@ -42,7 +47,11 @@ export default function FullTooltip({
|
||||
|
||||
return !disabled ? (
|
||||
<TooltipProvider disableHoverableContent={!interactive}>
|
||||
<Tooltip delayDuration={1} open={open} onOpenChange={handleOpen}>
|
||||
<Tooltip
|
||||
delayDuration={1}
|
||||
open={customOpen || open}
|
||||
onOpenChange={customOnOpenChange || handleOpen}
|
||||
>
|
||||
{children && (
|
||||
<TooltipTrigger asChild={true}>
|
||||
{hoverButton ? (
|
||||
|
||||
@@ -74,9 +74,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
)}
|
||||
|
||||
<div
|
||||
className={
|
||||
"absolute left-0 top-0 h-full flex items-center text-xs dark:text-nb-gray-300 pl-3 leading-[0]"
|
||||
}
|
||||
className={cn(
|
||||
"absolute left-0 top-0 h-full flex items-center text-xs dark:text-nb-gray-300 pl-3 leading-[0]",
|
||||
props.disabled && "opacity-30",
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
@@ -99,9 +100,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
/>
|
||||
|
||||
<div
|
||||
className={
|
||||
"absolute right-0 top-0 h-full flex items-center text-xs dark:text-nb-gray-300 pr-4 leading-[0]"
|
||||
}
|
||||
className={cn(
|
||||
"absolute right-0 top-0 h-full flex items-center text-xs dark:text-nb-gray-300 pr-4 leading-[0] select-none",
|
||||
props.disabled && "opacity-30",
|
||||
)}
|
||||
>
|
||||
{customSuffix}
|
||||
</div>
|
||||
|
||||
Loaded 20 of 102 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user