Compare commits

..
72 changed files with 3755 additions and 364 deletions

No files matched your search

+1 -2
View File
@@ -12,6 +12,5 @@
"tokenSource": "$NETBIRD_TOKEN_SOURCE", "tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS", "dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID", "hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID", "googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID"
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL"
} }
+372 -195
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -55,8 +55,8 @@
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0", "ip-cidr": "^3.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.383.0", "lucide-react": "^0.460.0",
"next": "13.5.5", "next": "13.5.7",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"punycode": "^2.3.1", "punycode": "^2.3.1",
"react": "^18", "react": "^18",
@@ -76,7 +76,7 @@
"typescript": "^5" "typescript": "^5"
}, },
"devDependencies": { "devDependencies": {
"cypress": "^13.3.3", "cypress": "^13.13.0",
"postcss": "^8", "postcss": "^8",
"prettier": "3.0.3", "prettier": "3.0.3",
"tailwindcss": "^3" "tailwindcss": "^3"
+4 -1
View File
@@ -14,6 +14,7 @@ import PeersProvider from "@/contexts/PeersProvider";
import RoutesProvider from "@/contexts/RoutesProvider"; import RoutesProvider from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route"; import { Route } from "@/interfaces/Route";
import PageContainer from "@/layouts/PageContainer"; import PageContainer from "@/layouts/PageContainer";
import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes"; import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
const NetworkRoutesTable = lazy( const NetworkRoutesTable = lazy(
@@ -39,7 +40,9 @@ export default function NetworkRoutes() {
icon={<NetworkRoutesIcon size={13} />} icon={<NetworkRoutesIcon size={13} />}
/> />
</Breadcrumbs> </Breadcrumbs>
<h1 ref={headingRef}>Network Routes</h1> <h1 ref={headingRef}>
Network Routes <NetworkRoutesDeprecationInfo size={18} />
</h1>
<Paragraph> <Paragraph>
Network routes allow you to access other networks like LANs and Network routes allow you to access other networks like LANs and
VPCs without installing NetBird on every resource. VPCs without installing NetBird on every resource.
+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;
+58
View File
@@ -0,0 +1,58 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import NetworksTable from "@/modules/networks/table/NetworksTable";
export default function Networks() {
const { data: networks, isLoading } = useFetchApi<Network[]>("/networks");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Networks</h1>
<Paragraph>
Networks allow you to access other resources like LANs and VPCs
without installing NetBird on every device.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink href={"https://docs.netbird.io/how-to/networks-concept"} 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>
);
}
+8 -25
View File
@@ -297,29 +297,12 @@ function PeerOverview() {
/> />
</FullTooltip> </FullTooltip>
<div> {!isUser && (
<Label>Assigned Groups</Label> <div>
<HelpText> <Label>Assigned Groups</Label>
Use groups to control what this peer can access. <HelpText>
</HelpText> Use groups to control what this peer can access.
<FullTooltip </HelpText>
content={
<div
className={
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
<LockIcon size={14} />
<span>
{`You don't have the required permissions to update this
setting.`}
</span>
</div>
}
interactive={false}
className={"w-full block"}
disabled={!isUser}
>
<PeerGroupSelector <PeerGroupSelector
disabled={isUser} disabled={isUser}
onChange={setSelectedGroups} onChange={setSelectedGroups}
@@ -327,8 +310,8 @@ function PeerOverview() {
hideAllGroup={true} hideAllGroup={true}
peer={peer} peer={peer}
/> />
</FullTooltip> </div>
</div> )}
</div> </div>
</div> </div>
</div> </div>
+8 -5
View File
@@ -6,17 +6,18 @@ import {
AlertOctagonIcon, AlertOctagonIcon,
FolderGit2Icon, FolderGit2Icon,
LockIcon, LockIcon,
NetworkIcon,
ShieldIcon, ShieldIcon,
} from "lucide-react"; } from "lucide-react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider"; import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Role } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer"; import PageContainer from "@/layouts/PageContainer";
import { useAccount } from "@/modules/account/useAccount"; import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab"; import AuthenticationTab from "@/modules/settings/AuthenticationTab";
import DangerZoneTab from "@/modules/settings/DangerZoneTab"; import DangerZoneTab from "@/modules/settings/DangerZoneTab";
import GroupsTab from "@/modules/settings/GroupsTab"; import GroupsTab from "@/modules/settings/GroupsTab";
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab"; import PermissionsTab from "@/modules/settings/PermissionsTab";
export default function NetBirdSettings() { export default function NetBirdSettings() {
@@ -48,19 +49,21 @@ export default function NetBirdSettings() {
<LockIcon size={14} /> <LockIcon size={14} />
Permissions Permissions
</VerticalTabs.Trigger> </VerticalTabs.Trigger>
<VerticalTabs.Trigger value="networks">
<NetworkIcon size={14} />
Networks
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}> <VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
<AlertOctagonIcon size={14} /> <AlertOctagonIcon size={14} />
Danger zone Danger zone
</VerticalTabs.Trigger> </VerticalTabs.Trigger>
</VerticalTabs.List> </VerticalTabs.List>
<RestrictedAccess <RestrictedAccess page={"Settings"}>
page={"Settings"}
allow={[Role.Admin, Role.Owner, Role.BillingAdmin]}
>
<div className={"border-l border-nb-gray-930 w-full"}> <div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />} {account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />} {account && <PermissionsTab account={account} />}
{account && <GroupsTab account={account} />} {account && <GroupsTab account={account} />}
{account && <NetworkSettingsTab account={account} />}
{account && <DangerZoneTab account={account} />} {account && <DangerZoneTab account={account} />}
</div> </div>
</RestrictedAccess> </RestrictedAccess>
+9 -2
View File
@@ -15,7 +15,9 @@ export const OIDCError = () => {
const params = useSearchParams(); const params = useSearchParams();
const errorParam = params.get("error"); const errorParam = params.get("error");
const accessDenied = errorParam === "access_denied"; const accessDenied = errorParam === "access_denied";
const invalidRequest = errorParam === "invalid_request";
const [title, setTitle] = useState(params.get("error_description")); const [title, setTitle] = useState(params.get("error_description"));
const errorDescription = params.get("error_description");
const { logout, login } = useOidc(); const { logout, login } = useOidc();
useEffect(() => { 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 /> There was an error logging you in. <br />
Error: {oidcUserLoadingState} Error:{" "}
<span className={"inline capitalize"}>
{invalidRequest && errorDescription
? errorDescription
: oidcUserLoadingState}
</span>
</Paragraph> </Paragraph>
<Button <Button
variant={"primary"} 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: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", "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: [ input: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900", "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 ", "dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
+14 -3
View File
@@ -1,12 +1,15 @@
import FullTooltip from "@components/FullTooltip"; import FullTooltip from "@components/FullTooltip";
import Paragraph from "@components/Paragraph"; import Paragraph from "@components/Paragraph";
import { cn } from "@utils/helpers"; import { cn } from "@utils/helpers";
import { cva } from "class-variance-authority"; import { cva, VariantProps } from "class-variance-authority";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import * as React from "react"; import * as React from "react";
type InputVariants = VariantProps<typeof inputVariants>;
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { extends React.InputHTMLAttributes<HTMLInputElement>,
InputVariants {
customPrefix?: React.ReactNode; customPrefix?: React.ReactNode;
customSuffix?: React.ReactNode; customSuffix?: React.ReactNode;
maxWidthClass?: string; maxWidthClass?: string;
@@ -14,6 +17,7 @@ export interface InputProps
error?: string; error?: string;
errorTooltip?: boolean; errorTooltip?: boolean;
errorTooltipPosition?: "top" | "top-right"; errorTooltipPosition?: "top" | "top-right";
prefixClassName?: string;
} }
const inputVariants = cva("", { const inputVariants = cva("", {
@@ -23,6 +27,10 @@ const inputVariants = cva("", {
"dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-nb-gray-700", "dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-nb-gray-700",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10", "ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10",
], ],
darker: [
"dark:bg-nb-gray-920 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-300 dark:border-nb-gray-800",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10",
],
error: [ error: [
"dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-red-500 text-red-500", "dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-red-500 text-red-500",
"ring-offset-red-500/10 dark:ring-offset-red-500/10 dark:focus-visible:ring-red-500/10 focus-visible:ring-red-500/10", "ring-offset-red-500/10 dark:ring-offset-red-500/10 dark:focus-visible:ring-red-500/10 focus-visible:ring-red-500/10",
@@ -51,6 +59,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
error, error,
errorTooltip = false, errorTooltip = false,
errorTooltipPosition = "top", errorTooltipPosition = "top",
variant = "default",
prefixClassName,
...props ...props
}, },
ref, ref,
@@ -67,6 +77,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
"flex h-[42px] w-auto rounded-l-md bg-white px-3 py-2 text-sm ", "flex h-[42px] w-auto rounded-l-md bg-white px-3 py-2 text-sm ",
"border items-center whitespace-nowrap", "border items-center whitespace-nowrap",
props.disabled && "opacity-20", props.disabled && "opacity-20",
prefixClassName,
)} )}
> >
{customPrefix} {customPrefix}
@@ -87,7 +98,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref} ref={ref}
{...props} {...props}
className={cn( className={cn(
inputVariants({ variant: error ? "error" : "default" }), inputVariants({ variant: error ? "error" : variant }),
"flex h-[42px] w-full rounded-md bg-white px-3 py-2 text-sm file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-20 ", "flex h-[42px] w-full rounded-md bg-white px-3 py-2 text-sm file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-20 ",
"file:border-0", "file:border-0",
"focus-visible:ring-2 focus-visible:ring-offset-2", "focus-visible:ring-2 focus-visible:ring-offset-2",
+114 -5
View File
@@ -7,22 +7,31 @@ import { ScrollArea } from "@components/ScrollArea";
import { AccessControlGroupCount } from "@components/ui/AccessControlGroupCount"; import { AccessControlGroupCount } from "@components/ui/AccessControlGroupCount";
import GroupBadge from "@components/ui/GroupBadge"; import GroupBadge from "@components/ui/GroupBadge";
import GroupBadgeWithEditPeers from "@components/ui/GroupBadgeWithEditPeers"; import GroupBadgeWithEditPeers from "@components/ui/GroupBadgeWithEditPeers";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { VirtualScrollAreaList } from "@components/VirtualScrollAreaList";
import { useSearch } from "@hooks/useSearch";
import useSortedDropdownOptions from "@hooks/useSortedDropdownOptions"; import useSortedDropdownOptions from "@hooks/useSortedDropdownOptions";
import { IconArrowBack } from "@tabler/icons-react"; import { IconArrowBack } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers"; import { cn } from "@utils/helpers";
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk"; import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
import { sortBy, trim, unionBy } from "lodash"; import { sortBy, trim, unionBy } from "lodash";
import { import {
ChevronsUpDown, ChevronsUpDown,
FolderGit2, FolderGit2,
GlobeIcon,
Layers3,
MonitorSmartphoneIcon, MonitorSmartphoneIcon,
NetworkIcon,
SearchIcon, SearchIcon,
WorkflowIcon,
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useEffect, useMemo, useState } from "react"; import { Fragment, useEffect, useMemo, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider"; import { useGroups } from "@/contexts/GroupsProvider";
import { useElementSize } from "@/hooks/useElementSize"; import { useElementSize } from "@/hooks/useElementSize";
import type { Group, GroupPeer } from "@/interfaces/Group"; import type {Group, GroupPeer, GroupResource} from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer"; import type { Peer } from "@/interfaces/Peer";
interface MultiSelectProps { interface MultiSelectProps {
@@ -95,19 +104,21 @@ export function PeerGroupSelector({
const option = dropdownOptions.find((option) => option.name == name); const option = dropdownOptions.find((option) => option.name == name);
const groupPeers: GroupPeer[] | undefined = const groupPeers: GroupPeer[] | undefined =
(group?.peers as GroupPeer[]) || []; (group?.peers as GroupPeer[]) || [];
const groupResources: GroupResource[] | undefined =
(group?.resources as GroupResource[]) || [];
if (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name }); if (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
if (!group && !option) { if (!group && !option) {
addDropdownOptions([{ name: name, peers: groupPeers }]); addDropdownOptions([{ name: name, peers: groupPeers, resources: groupResources }]);
} }
if (max == 1 && values.length == 1) { if (max == 1 && values.length == 1) {
onChange([{ name: name, id: group?.id, peers: groupPeers }]); onChange([{ name: name, id: group?.id, peers: groupPeers, resources: groupResources }]);
} else { } else {
onChange((previous) => [ onChange((previous) => [
...previous, ...previous,
{ name: name, id: group?.id, peers: groupPeers }, { name: name, id: group?.id, peers: groupPeers, resources: groupResources },
]); ]);
} }
@@ -385,6 +396,8 @@ export function PeerGroupSelector({
<AccessControlGroupCount group_id={option.id} /> <AccessControlGroupCount group_id={option.id} />
)} )}
<ResourcesCounter group={option} />
<div <div
className={ className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2" "text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
@@ -407,3 +420,99 @@ export function PeerGroupSelector({
</Popover> </Popover>
); );
} }
const ResourcesCounter = ({ group }: { group: Group }) => {
return group?.resources_count && group.resources_count > 0 ? (
<div
className={
"text-nb-gray-300 font-medium flex items-center gap-2 transition-all"
}
>
<Layers3 size={14} className={"shrink-0"} />
{group.resources_count} Resource(s)
</div>
) : null;
};
const resourcesSearchPredicate = (item: NetworkResource, query: string) => {
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
return item.address.toLowerCase().includes(lowerCaseQuery);
};
const ResourcesList = ({ search }: { search: string }) => {
const { data: resources, isLoading } = useFetchApi<NetworkResource[]>(
"/networks/resources",
);
const [filteredItems, _, setSearch] = useSearch(
resources || [],
resourcesSearchPredicate,
{ filter: true, debounce: 150 },
);
useEffect(() => {
setSearch(search);
}, [search, setSearch]);
return isLoading ? (
<>Loading...</>
) : (
filteredItems.length > 0 && (
<VirtualScrollAreaList
items={filteredItems}
onSelect={(option) => null}
renderItem={(res) => {
const isSelected = false;
return (
<Fragment key={res.id}>
<div className={"flex items-center gap-2"}>
<Badge
useHover={true}
data-cy={"group-badge"}
variant={"gray-ghost"}
className={cn("transition-all group whitespace-nowrap")}
onClick={(e) => {
e.preventDefault();
}}
>
{res.type === "host" && (
<WorkflowIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
{res.type === "domain" && (
<GlobeIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
{res.type === "subnet" && (
<NetworkIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
<TextWithTooltip text={res?.name || ""} maxChars={20} />
</Badge>
</div>
<div className={"flex items-center gap-5"}>
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
}
>
<Checkbox checked={isSelected} />
</div>
</div>
</Fragment>
);
}}
/>
)
);
};
+5 -2
View File
@@ -39,7 +39,7 @@ const Tabs = React.forwardRef<
Tabs.displayName = TabsPrimitive.Root.displayName; Tabs.displayName = TabsPrimitive.Root.displayName;
type TabListProps = { type TabListProps = {
justify?: "start" | "end" | "center"; justify?: "start" | "end" | "center" | "between";
}; };
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
@@ -54,6 +54,7 @@ const TabsList = React.forwardRef<
justify == "center" && "justify-center justify-items-end", justify == "center" && "justify-center justify-items-end",
justify == "start" && "justify-start", justify == "start" && "justify-start",
justify == "end" && "justify-end", justify == "end" && "justify-end",
justify == "between" && "justify-between",
)} )}
{...props} {...props}
> >
@@ -63,7 +64,9 @@ const TabsList = React.forwardRef<
} }
/> />
<ScrollArea> <ScrollArea>
<div className={"relative z-[1] flex flex-nowrap"}>{props.children}</div> <div className={"relative z-[1] flex flex-nowrap w-full "}>
{props.children}
</div>
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
</ScrollArea> </ScrollArea>
</TabsPrimitive.List> </TabsPrimitive.List>
+26 -7
View File
@@ -11,17 +11,36 @@ type Props = {
onChange: (value: string) => void; onChange: (value: string) => void;
children: React.ReactNode; children: React.ReactNode;
}; };
const TabSwitchContext = React.createContext<{
switchTab: (value: string) => void;
}>({
switchTab: () => {},
});
export const useTabSwitchContext = () => {
return React.useContext(TabSwitchContext);
};
function VerticalTabs({ value, onChange, children }: Props) { function VerticalTabs({ value, onChange, children }: Props) {
return ( return (
<TabContext.Provider value={value || ""}> <TabContext.Provider value={value || ""}>
<Tabs.Root <TabSwitchContext.Provider
orientation={"vertical"} value={{
className={"block lg:flex bg-nb-gray"} switchTab: (value: string) => {
value={value} onChange(value);
onValueChange={(value) => onChange(value)} },
}}
> >
{children} <Tabs.Root
</Tabs.Root> orientation={"vertical"}
className={"block lg:flex bg-nb-gray"}
value={value}
onValueChange={(value) => onChange(value)}
>
{children}
</Tabs.Root>
</TabSwitchContext.Provider>
</TabContext.Provider> </TabContext.Provider>
); );
} }
+12 -3
View File
@@ -11,6 +11,7 @@ interface Props extends IconVariant {
margin?: string; margin?: string;
truncate?: boolean; truncate?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
center?: boolean;
} }
export default function ModalHeader({ export default function ModalHeader({
icon, icon,
@@ -21,13 +22,21 @@ export default function ModalHeader({
margin = "mt-0", margin = "mt-0",
truncate = false, truncate = false,
children, children,
center,
}: Props) { }: Props) {
return ( return (
<div className={cn(className, "min-w-0")}> <div className={cn(className, "min-w-0")}>
<div className={"flex items-start gap-5 pr-10 min-w-0"}> <div className={"flex items-start gap-5 min-w-0"}>
{icon && <SquareIcon color={color} icon={icon} />} {icon && <SquareIcon color={color} icon={icon} />}
<div className={"min-w-0"}> <div className={cn("min-w-0", center && "text-center")}>
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2> <h2
className={cn(
"text-lg my-0 leading-[1.5]",
center && "text-center",
)}
>
{title}
</h2>
{children ? ( {children ? (
<>{children}</> <>{children}</>
) : ( ) : (
+2 -2
View File
@@ -101,11 +101,11 @@ const TableRow = React.forwardRef<
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
" transition-colors data-[state=selected]:bg-neutral-100 dark:data-[state=selected]:bg-nb-gray-930/70", " 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", "dark:data-[state=selected]:border-nb-gray-900",
minimal minimal
? "dark:hover:bg-nb-gray-900/10" ? "dark:hover:bg-nb-gray-900/10"
: "border-b dark:border-zinc-700/40 dark:hover:bg-nb-gray-900/20 hover:bg-neutral-100/50", : "border-b dark:border-zinc-700/40 dark:hover:bg-nb-gray-940 hover:bg-neutral-100/50",
className, className,
)} )}
{...props} {...props}
+2 -2
View File
@@ -33,8 +33,8 @@ export default function UserDropdown() {
logout("/", { client_id: config.clientId }).then(); logout("/", { client_id: config.clientId }).then();
}; };
useHotkeys("shift+mod+l", () => logout(), []); useHotkeys("shift+mod+l", () => logoutSession(), []);
const { isOwnerOrAdmin, permission, isBillingAdmin } = useLoggedInUser(); const { permission } = useLoggedInUser();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
return ( return (
+4 -2
View File
@@ -24,7 +24,7 @@ type DialogOptions = {
description?: string | React.ReactNode; description?: string | React.ReactNode;
confirmText?: string; confirmText?: string;
cancelText?: string; cancelText?: string;
type?: "default" | "warning" | "danger"; type?: "default" | "warning" | "danger" | "center";
children?: React.ReactNode; children?: React.ReactNode;
}; };
@@ -51,6 +51,7 @@ export default function DialogProvider({ children }: Props) {
default: "", default: "",
warning: <AlertCircle size={18} />, warning: <AlertCircle size={18} />,
danger: <AlertTriangle size={18} />, danger: <AlertTriangle size={18} />,
center: "",
}; };
return ( return (
@@ -61,8 +62,9 @@ export default function DialogProvider({ children }: Props) {
onOpenChange={(open) => fn.current && fn.current(open)} onOpenChange={(open) => fn.current && fn.current(open)}
> >
{dialogOptions && ( {dialogOptions && (
<ModalContent maxWidthClass={"max-w-lg"} showClose={false}> <ModalContent maxWidthClass={"max-w-[400px]"} showClose={false}>
<ModalHeader <ModalHeader
center={dialogOptions.type == "center"}
title={dialogOptions.title || "Confirmation"} title={dialogOptions.title || "Confirmation"}
margin={"mt-1"} margin={"mt-1"}
description={ description={
Loaded 20 of 72 files, more files were not shown because too many files have changed in this diff. Show more