mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42cd088c5d | ||
|
|
7400ac806e | ||
|
|
240ff5af9a | ||
|
|
dc86c30463 | ||
|
|
e58f75ae3c |
No files matched your search
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Clusters - Reverse Proxy - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
@@ -0,0 +1,66 @@
|
||||
"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 { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { REVERSE_PROXY_CLUSTERS_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const ClustersTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/clusters/ClustersTable"),
|
||||
);
|
||||
|
||||
export default function ReverseProxyClustersPage() {
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/services"}
|
||||
label={"Reverse Proxy"}
|
||||
icon={<ReverseProxyIcon size={16} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/clusters"}
|
||||
label={"Clusters"}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Clusters</h1>
|
||||
<Paragraph>
|
||||
Proxy clusters that route inbound traffic to your services. Shared
|
||||
clusters are deployed at the server level; account clusters are
|
||||
self-hosted on your own infrastructure.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink href={REVERSE_PROXY_CLUSTERS_DOCS_LINK} target={"_blank"}>
|
||||
Clusters
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Clusters"}
|
||||
hasAccess={permission?.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ClustersTable headingTarget={portalTarget} />
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -3,36 +3,103 @@ import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
export default function LoadingIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 919 669"
|
||||
fill="none"
|
||||
width="135"
|
||||
height="140"
|
||||
viewBox="0 0 135 140"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fillOpacity="0.8"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes nb-loader-pulse {
|
||||
0%, 100% { transform: scale(0.85); opacity: 0.35; }
|
||||
50% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.nb-loader-bird {
|
||||
animation: nb-loader-pulse 1.75s ease-in-out infinite;
|
||||
transform-origin: 50% 50%;
|
||||
transform-box: fill-box;
|
||||
}
|
||||
`}</style>
|
||||
<g className={"nb-loader-bird"}>
|
||||
<path
|
||||
d="M653.25 0C542.297 10.255 487.216 74.132 466.346 106.359L456.959 122.609C456.311 123.963 455.886 124.865 455.886 124.865L455.774 124.685L142.056 668.021H533.256L918.947 0H653.25Z"
|
||||
fill="#F68330"
|
||||
<rect y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<path
|
||||
d="M533.256 668.02L0 102.296C0 102.296 603.149 -59.6172 661.827 445.893L533.256 668.02Z"
|
||||
fill="#F68330"
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<path
|
||||
d="M454.3 127.229L290.658 410.664L533.256 668.02L661.757 445.475C641.46 271.63 556.721 176.843 454.3 127.229Z"
|
||||
fill="#F35E32"
|
||||
</rect>
|
||||
<rect x="30" y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</g>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="60" width="15" height="140" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="90" y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="120" y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import useCopyToClipboard from "@hooks/useCopyToClipboard";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Copy } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
type CardTableProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTable({ children, className }: CardTableProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-nb-gray-940 rounded-md border border-nb-gray-900 w-full overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<table className={"w-full border-collapse text-sm"}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTableHeader({ children, className }: CardTableProps) {
|
||||
return (
|
||||
<thead>
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b border-nb-gray-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
type CardTableHeaderCellProps = {
|
||||
children: React.ReactNode;
|
||||
width?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableHeaderCell({
|
||||
children,
|
||||
width,
|
||||
className,
|
||||
}: CardTableHeaderCellProps) {
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
"px-4 py-2.5 text-left text-sm font-normal",
|
||||
className,
|
||||
)}
|
||||
style={width ? { width } : undefined}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTableBody({ children, className }: CardTableProps) {
|
||||
return <tbody className={className}>{children}</tbody>;
|
||||
}
|
||||
|
||||
type CardTableRowProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableRow({ children, className }: CardTableRowProps) {
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b border-nb-gray-900 last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
type CardTableCellProps = {
|
||||
children: React.ReactNode;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
width?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableCell({
|
||||
children,
|
||||
copy = false,
|
||||
copyText,
|
||||
width,
|
||||
className,
|
||||
}: CardTableCellProps) {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(copyText ?? "");
|
||||
return (
|
||||
<td
|
||||
className={cn("px-4 py-3", className)}
|
||||
style={width ? { width } : undefined}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"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} has been copied to clipboard.`)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
{copy && <Copy size={13} className={"shrink-0"} />}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
CardTable.Header = CardTableHeader;
|
||||
CardTable.HeaderCell = CardTableHeaderCell;
|
||||
CardTable.Body = CardTableBody;
|
||||
CardTable.Row = CardTableRow;
|
||||
CardTable.Cell = CardTableCell;
|
||||
|
||||
export default CardTable;
|
||||
@@ -12,7 +12,7 @@ export default function FullScreenLoading({ fullScreen = true }: Props) {
|
||||
fullScreen && "h-screen",
|
||||
)}
|
||||
>
|
||||
<LoadingIcon className="fill-netbird" size={68} />
|
||||
<LoadingIcon className="fill-netbird" size={44} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { notify } from "@components/Notification";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { wrapIPv6 } from "@utils/ip";
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
@@ -15,6 +16,8 @@ import { Network, NetworkResource } from "@/interfaces/Network";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import {
|
||||
ReverseProxy,
|
||||
ReverseProxyCluster,
|
||||
ReverseProxyClusterType,
|
||||
ReverseProxyDomain,
|
||||
ReverseProxyFlatTarget,
|
||||
ReverseProxyTarget,
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
} from "@/interfaces/ReverseProxy";
|
||||
import ReverseProxyModal from "@/modules/reverse-proxy/ReverseProxyModal";
|
||||
import ReverseProxyTargetModal from "@/modules/reverse-proxy/targets/ReverseProxyTargetModal";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
|
||||
type ReverseProxiesContextValue = {
|
||||
reverseProxies: ReverseProxy[] | undefined;
|
||||
@@ -51,6 +55,9 @@ type ReverseProxiesContextValue = {
|
||||
domain: string,
|
||||
targetCluster: string,
|
||||
) => Promise<ReverseProxyDomain>;
|
||||
clusters: ReverseProxyCluster[] | undefined;
|
||||
isClustersLoading: boolean;
|
||||
isSelfHostedCluster: (clusterAddress?: string) => boolean;
|
||||
};
|
||||
|
||||
type OpenModalOptions = {
|
||||
@@ -90,10 +97,14 @@ export default function ReverseProxiesProvider({
|
||||
}: Readonly<Props>) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { confirm } = useDialog();
|
||||
const { permission } = usePermissions();
|
||||
|
||||
// Reverse Proxies
|
||||
const { data: rawReverseProxies, isLoading } = useFetchApi<ReverseProxy[]>(
|
||||
"/reverse-proxies/services",
|
||||
false,
|
||||
true,
|
||||
permission?.services.read,
|
||||
);
|
||||
const request = useApiCall<ReverseProxy>("/reverse-proxies/services", true);
|
||||
|
||||
@@ -101,6 +112,9 @@ export default function ReverseProxiesProvider({
|
||||
const { data: peers } = useFetchApi<Peer[]>("/peers");
|
||||
const { data: resources } = useFetchApi<NetworkResource[]>(
|
||||
"/networks/resources",
|
||||
false,
|
||||
true,
|
||||
permission?.services.read,
|
||||
);
|
||||
|
||||
const resolveDestination = useCallback(
|
||||
@@ -125,12 +139,28 @@ export default function ReverseProxiesProvider({
|
||||
// Domains
|
||||
const { data: domains, isLoading: isLoadingDomains } = useFetchApi<
|
||||
ReverseProxyDomain[]
|
||||
>("/reverse-proxies/domains");
|
||||
>("/reverse-proxies/domains", false, true, permission.services?.read);
|
||||
const domainRequest = useApiCall<ReverseProxyDomain>(
|
||||
"/reverse-proxies/domains",
|
||||
true,
|
||||
);
|
||||
|
||||
// Clusters
|
||||
const { data: clusters, isLoading: isClustersLoading } = useFetchApi<
|
||||
ReverseProxyCluster[]
|
||||
>("/reverse-proxies/clusters", false, true, permission.services?.read);
|
||||
|
||||
const isSelfHostedCluster = useCallback(
|
||||
(clusterAddress?: string) => {
|
||||
if (!clusterAddress) return false;
|
||||
return (
|
||||
clusters?.find((c) => c.address === clusterAddress)?.type ===
|
||||
ReverseProxyClusterType.ACCOUNT
|
||||
);
|
||||
},
|
||||
[clusters],
|
||||
);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [currentProxy, setCurrentProxy] = useState<ReverseProxy | undefined>();
|
||||
const [initialTab, setInitialTab] = useState<string | undefined>();
|
||||
@@ -483,6 +513,9 @@ export default function ReverseProxiesProvider({
|
||||
createDomain,
|
||||
validateDomain,
|
||||
deleteDomain,
|
||||
clusters,
|
||||
isClustersLoading,
|
||||
isSelfHostedCluster,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -604,7 +637,7 @@ function formatTargetDestination(
|
||||
target: ReverseProxyTarget,
|
||||
resolvedHost?: string,
|
||||
): string {
|
||||
const host = target.host || resolvedHost || "localhost";
|
||||
const host = wrapIPv6(target.host || resolvedHost || "localhost");
|
||||
const isDefault =
|
||||
(target.protocol === "http" && target.port === 80) ||
|
||||
(target.protocol === "https" && target.port === 443) ||
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Account {
|
||||
auto_update_version: string;
|
||||
auto_update_always: boolean;
|
||||
local_auth_disabled?: boolean;
|
||||
local_mfa_enabled?: boolean;
|
||||
ipv6_enabled_groups?: string[];
|
||||
network_range_v6?: string;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface ReverseProxy {
|
||||
proxy_cluster?: string;
|
||||
targets: ReverseProxyTarget[];
|
||||
enabled: boolean;
|
||||
terminated?: boolean;
|
||||
pass_host_header?: boolean;
|
||||
rewrite_redirects?: boolean;
|
||||
auth?: ReverseProxyAuth;
|
||||
@@ -162,6 +163,33 @@ export interface ReverseProxyEvent {
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export enum ReverseProxyClusterType {
|
||||
ACCOUNT = "account",
|
||||
SHARED = "shared",
|
||||
}
|
||||
|
||||
export interface ReverseProxyCluster {
|
||||
id?: string;
|
||||
address: string;
|
||||
type: ReverseProxyClusterType;
|
||||
online: boolean;
|
||||
connected_proxies: number;
|
||||
supports_custom_ports?: boolean;
|
||||
require_subdomain?: boolean;
|
||||
supports_crowdsec?: boolean;
|
||||
}
|
||||
|
||||
export interface ReverseProxyClusterToken {
|
||||
id?: string;
|
||||
name: string;
|
||||
plain_token?: string;
|
||||
expires_at?: string;
|
||||
expires_in?: number;
|
||||
created_at?: string;
|
||||
last_used?: string;
|
||||
revoked?: boolean;
|
||||
}
|
||||
|
||||
export function isL4Event(event: ReverseProxyEvent): boolean {
|
||||
return (
|
||||
event.protocol === EventProtocol.TCP ||
|
||||
@@ -198,19 +226,19 @@ export const REVERSE_PROXY_SETTINGS_DOCS_LINK =
|
||||
"https://docs.netbird.io/manage/reverse-proxy#step-4-configure-advanced-settings";
|
||||
|
||||
export const REVERSE_PROXY_CLUSTERS_DOCS_LINK =
|
||||
"https://docs.netbird.io/manage/reverse-proxy#self-hosted-proxy-setup";
|
||||
"https://docs.netbird.io/manage/reverse-proxy/bring-your-own-proxy#shared-and-account-clusters";
|
||||
|
||||
export const REVERSE_PROXY_CUSTOM_DOMAINS_DOCS_LINK =
|
||||
"https://docs.netbird.io/manage/reverse-proxy/custom-domains";
|
||||
|
||||
export const REVERSE_PROXY_DOMAIN_VERIFICATION_LINK =
|
||||
"https://docs.netbird.io/manage/reverse-proxy/custom-domains#validating-a-custom-domain";
|
||||
"https://docs.netbird.io/manage/reverse-proxy/custom-domains#verifying-a-custom-domain";
|
||||
|
||||
export const REVERSE_PROXY_EVENTS_DOCS_LINK =
|
||||
"https://docs.netbird.io/manage/reverse-proxy/access-logs";
|
||||
|
||||
export const REVERSE_PROXY_ACCESS_CONTROL_DOCS_LINK =
|
||||
"https://docs.netbird.io/manage/reverse-proxy";
|
||||
"https://docs.netbird.io/manage/reverse-proxy#step-3b-configure-access-control";
|
||||
|
||||
export const REVERSE_PROXY_TROUBLESHOOTING_DOCS_LINK =
|
||||
"https://docs.netbird.io/manage/reverse-proxy#troubleshooting";
|
||||
"https://docs.netbird.io/manage/reverse-proxy/troubleshooting";
|
||||
@@ -37,6 +37,7 @@ export default function Navigation({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-navigation
|
||||
className={cn(
|
||||
"whitespace-nowrap md:border-r dark:border-zinc-700/40 bg-gray-50 dark:bg-nb-gray relative group/navigation transition-all",
|
||||
hideOnMobile ? "hidden md:block" : "",
|
||||
@@ -165,6 +166,13 @@ export default function Navigation({
|
||||
exactPathMatch={true}
|
||||
visible={permission?.services?.read}
|
||||
/>
|
||||
<SidebarItem
|
||||
label="Clusters"
|
||||
isChild
|
||||
href={"/reverse-proxy/clusters"}
|
||||
exactPathMatch={true}
|
||||
visible={permission?.services?.read}
|
||||
/>
|
||||
</SidebarItem>
|
||||
|
||||
<SidebarItem
|
||||
|
||||
@@ -19,6 +19,7 @@ import { HelpTooltip } from "@components/HelpTooltip";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { normalizeHostCIDR } from "@utils/ip";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { usePolicies } from "@/contexts/PoliciesProvider";
|
||||
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
|
||||
@@ -172,7 +173,7 @@ export function ResourceModalContent({
|
||||
const promise = create({
|
||||
name,
|
||||
description,
|
||||
address,
|
||||
address: normalizeHostCIDR(address),
|
||||
groups: savedGroups ? savedGroups.map((g) => g.id) : undefined,
|
||||
enabled,
|
||||
}).then(async (r) => {
|
||||
@@ -196,7 +197,7 @@ export function ResourceModalContent({
|
||||
const promise = update({
|
||||
name,
|
||||
description,
|
||||
address,
|
||||
address: normalizeHostCIDR(address),
|
||||
groups: savedGroups ? savedGroups.map((g) => g.id) : undefined,
|
||||
enabled,
|
||||
}).then(async (r) => {
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ResourceSingleAddressInput = ({
|
||||
|
||||
// Case 2: If it's not a valid domain, check if it's a valid CIDR
|
||||
if (!cidr.isValidAddress(value)) {
|
||||
return "Please enter a valid IP or CIDR, e.g., 10.0.0.21, 192.168.1.0/24";
|
||||
return "Please enter a valid IP or CIDR, e.g., 10.0.0.21, 192.168.1.0/24, 2001:db8::1 or 2001:db8::/64";
|
||||
}
|
||||
|
||||
return ""; // Valid CIDR
|
||||
|
||||
@@ -2,6 +2,7 @@ import Button from "@components/Button";
|
||||
import { notify } from "@components/Notification";
|
||||
import { RadioCard, RadioCardGroup } from "@components/RadioCard";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { normalizeHostCIDR } from "@utils/ip";
|
||||
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -67,7 +68,7 @@ export const OnboardingAddResource = ({
|
||||
{
|
||||
name: resourceType === "subnet" ? "My Subnet" : "My Resource",
|
||||
description: "Created during onboarding",
|
||||
address: resourceAddress,
|
||||
address: normalizeHostCIDR(resourceAddress),
|
||||
enabled: true,
|
||||
groups: [],
|
||||
},
|
||||
@@ -178,15 +179,15 @@ export const OnboardingAddResource = ({
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (resourceType === "ip")
|
||||
return "Enter a single IPv4 address of your resource";
|
||||
return "Enter a single IPv4 or IPv6 address of your resource";
|
||||
if (resourceType === "subnet") return "Enter a CIDR range of your network";
|
||||
if (resourceType === "domain")
|
||||
return "Enter a domain name of your resource";
|
||||
}, [resourceType]);
|
||||
|
||||
const placeholder = useMemo(() => {
|
||||
if (resourceType === "ip") return "e.g., 192.168.31.45";
|
||||
if (resourceType === "subnet") return "e.g., 192.168.1.0/24";
|
||||
if (resourceType === "ip") return "e.g., 192.168.31.45 or 2001:db8::1";
|
||||
if (resourceType === "subnet") return "e.g., 192.168.1.0/24 or 2001:db8::/64";
|
||||
if (resourceType === "domain")
|
||||
return "e.g., service.internal or *.services.internal";
|
||||
}, [resourceType]);
|
||||
@@ -211,13 +212,13 @@ export const OnboardingAddResource = ({
|
||||
value={"ip"}
|
||||
title={"Single IP Address"}
|
||||
icon={<WorkflowIcon size={12} />}
|
||||
description={"IPv4 address like 192.168.31.45"}
|
||||
description={"IPv4 or IPv6 address like 192.168.31.45"}
|
||||
/>
|
||||
<RadioCard
|
||||
value={"subnet"}
|
||||
title={"Entire Subnet"}
|
||||
icon={<NetworkIcon size={12} />}
|
||||
description={"CIDR range like 192.168.0.0/24"}
|
||||
description={"CIDR range like 192.168.0.0/24 or 2001:db8::/64"}
|
||||
/>
|
||||
<RadioCard
|
||||
value={"domain"}
|
||||
|
||||
@@ -32,8 +32,9 @@ export const OnboardingTestResource = ({
|
||||
|
||||
const pingAddress = useMemo(() => {
|
||||
let a = resource?.address || "";
|
||||
if (isHost && a.endsWith("/32")) {
|
||||
a = a.slice(0, -3);
|
||||
if (isHost) {
|
||||
if (a.endsWith("/32")) a = a.slice(0, -3);
|
||||
else if (a.endsWith("/128")) a = a.slice(0, -4);
|
||||
}
|
||||
if (isWildCard) return `(any subdomain of ${a})`;
|
||||
return isSubnet ? `(resource ip in your subnet)` : a;
|
||||
|
||||
@@ -4,6 +4,7 @@ import HelpText from "@components/HelpText";
|
||||
import Button from "@components/Button";
|
||||
import { Input } from "@components/Input";
|
||||
import cidr from "ip-cidr";
|
||||
import { isIPv6 } from "@utils/ip";
|
||||
import {
|
||||
FlagIcon,
|
||||
MinusCircleIcon,
|
||||
@@ -180,13 +181,17 @@ type Props = {
|
||||
function validateRule(rule: AccessRule): string {
|
||||
if (rule.type === "country" || !rule.value) return "";
|
||||
if (rule.type === "ip") {
|
||||
const val = rule.value.includes("/") ? rule.value : `${rule.value}/32`;
|
||||
let val = rule.value;
|
||||
if (!val.includes("/")) {
|
||||
const suffix = isIPv6(val) ? 128 : 32;
|
||||
val = `${val}/${suffix}`;
|
||||
}
|
||||
if (!cidr.isValidAddress(val)) {
|
||||
return "Please enter a valid IP address, e.g., 85.203.15.42";
|
||||
return "Please enter a valid IP address, e.g., 85.203.15.42 or 2001:db8::1";
|
||||
}
|
||||
} else {
|
||||
if (!rule.value.includes("/") || !cidr.isValidAddress(rule.value)) {
|
||||
return "Please enter a valid CIDR block, e.g., 74.125.0.0/16";
|
||||
return "Please enter a valid CIDR block, e.g., 74.125.0.0/16 or 2001:db8::/64";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
@@ -312,8 +317,8 @@ export const ReverseProxyAccessControlRules = ({
|
||||
<Input
|
||||
placeholder={
|
||||
rule.type === "ip"
|
||||
? "e.g., 85.203.15.42"
|
||||
: "e.g., 74.125.0.0/16"
|
||||
? "e.g., 85.203.15.42 or 2001:db8::1"
|
||||
: "e.g., 74.125.0.0/16 or 2001:db8::/64"
|
||||
}
|
||||
value={rule.value}
|
||||
onChange={(e) =>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ServerIcon, UserCog } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
ReverseProxyCluster,
|
||||
ReverseProxyClusterType,
|
||||
} from "@/interfaces/ReverseProxy";
|
||||
|
||||
type Props = {
|
||||
cluster: ReverseProxyCluster;
|
||||
};
|
||||
|
||||
// ClusterTypeIndicator renders a small icon next to the cluster name —
|
||||
// same pattern as EphemeralPeerIndicator — so the source of the
|
||||
// cluster is visible at a glance without a dedicated column.
|
||||
export const ClusterTypeIndicator = ({ cluster }: Props) => {
|
||||
if (cluster.type === ReverseProxyClusterType.ACCOUNT) {
|
||||
return (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
<span className={"font-medium text-white"}>Account cluster.</span>{" "}
|
||||
Self-hosted on your own infrastructure — you operate the proxy
|
||||
nodes and control where traffic terminates.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<UserCog size={12} className={"shrink-0 text-netbird"} />
|
||||
</FullTooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
<span className={"font-medium text-white"}>Shared cluster.</span>{" "}
|
||||
Deployed at the server level and available to every account on this
|
||||
instance.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ServerIcon size={12} className={"shrink-0 text-nb-gray-300"} />
|
||||
</FullTooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import Button from "@components/Button";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import {
|
||||
ReverseProxyCluster,
|
||||
ReverseProxyClusterType,
|
||||
} from "@/interfaces/ReverseProxy";
|
||||
|
||||
type Props = {
|
||||
cluster: ReverseProxyCluster;
|
||||
};
|
||||
|
||||
export default function ClustersActionCell({ cluster }: Readonly<Props>) {
|
||||
const { confirm } = useDialog();
|
||||
const request = useApiCall<ReverseProxyCluster>("/reverse-proxies/clusters");
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
|
||||
// Shared clusters are operated by NetBird; only account-owned (BYOP)
|
||||
// clusters can be deleted from this page. Rendering nothing for
|
||||
// shared rows keeps the cell column-aligned without an inert button.
|
||||
if (cluster.type !== ReverseProxyClusterType.ACCOUNT) {
|
||||
return <div className={"pr-4"} />;
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Delete '${cluster.address}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this proxy cluster? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
maxWidthClass: "max-w-md",
|
||||
});
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: cluster.address,
|
||||
description: "Proxy cluster was successfully deleted",
|
||||
promise: request
|
||||
.del({}, `/${encodeURIComponent(cluster.address)}`)
|
||||
.then(() => {
|
||||
mutate("/reverse-proxies/clusters");
|
||||
}),
|
||||
loadingMessage: "Deleting the proxy cluster...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"flex justify-end pr-4"}>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={handleDelete}
|
||||
disabled={!permission?.services?.delete}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Badge from "@components/Badge";
|
||||
import { Server } from "lucide-react";
|
||||
import { ReverseProxyCluster } from "@/interfaces/ReverseProxy";
|
||||
|
||||
type Props = {
|
||||
cluster: ReverseProxyCluster;
|
||||
};
|
||||
|
||||
export default function ClustersConnectedCell({ cluster }: Readonly<Props>) {
|
||||
const count = cluster.connected_proxies;
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"gray"}>
|
||||
<Server size={11} />
|
||||
<div>
|
||||
<span className={"font-medium text-xs"}>{count}</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ShieldAlert, SlidersHorizontal, Globe } from "lucide-react";
|
||||
import { ReverseProxyCluster } from "@/interfaces/ReverseProxy";
|
||||
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
|
||||
|
||||
type Props = {
|
||||
cluster: ReverseProxyCluster;
|
||||
};
|
||||
|
||||
type Feature = {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
// ClustersFeaturesCell renders one badge per supported capability.
|
||||
// Only "true" flags get a badge; nil and false are omitted (the
|
||||
// backend distinguishes "unsupported" from "not yet reported" via
|
||||
// nullable booleans, but visually both mean "not available here").
|
||||
export default function ClustersFeaturesCell({ cluster }: Readonly<Props>) {
|
||||
const features: Feature[] = [];
|
||||
if (cluster.supports_custom_ports) {
|
||||
features.push({
|
||||
key: "custom-ports",
|
||||
label: "Custom Ports",
|
||||
description: "Cluster can bind arbitrary TCP/UDP ports for services.",
|
||||
icon: <SlidersHorizontal size={14} className={"text-netbird"} />,
|
||||
});
|
||||
}
|
||||
if (cluster.require_subdomain) {
|
||||
features.push({
|
||||
key: "subdomain",
|
||||
label: "Subdomain Required",
|
||||
description:
|
||||
"Services on this cluster must use a subdomain — the bare cluster domain is not addressable.",
|
||||
icon: <Globe size={14} className={"text-nb-gray-300"} />,
|
||||
});
|
||||
}
|
||||
if (cluster.supports_crowdsec) {
|
||||
features.push({
|
||||
key: "crowdsec",
|
||||
label: "CrowdSec",
|
||||
description:
|
||||
"Cluster has CrowdSec IP reputation configured across all active proxies.",
|
||||
icon: <ShieldAlert size={14} className={"text-green-500"} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (features.length === 0) {
|
||||
return <EmptyRow />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{features.map((f) => (
|
||||
<FullTooltip
|
||||
key={f.key}
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
<div className={"font-medium text-white"}>{f.label}</div>
|
||||
<div className={"text-nb-gray-300 mt-1"}>{f.description}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Badge variant={"gray"} className={"h-[34px] cursor-help"}>
|
||||
{f.icon}
|
||||
<span className="font-medium text-xs">{f.label}</span>
|
||||
</Badge>
|
||||
</FullTooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import { notify } from "@components/Notification";
|
||||
import CardTable from "@components/CardTable";
|
||||
import Code from "@components/Code";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import {
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@components/modal/Modal";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
GlobeIcon,
|
||||
ListIcon,
|
||||
Loader2,
|
||||
ServerIcon,
|
||||
SquareTerminalIcon,
|
||||
} from "lucide-react";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useApiCall } from "@/utils/api";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import { GRPC_API_ORIGIN, isNetBirdHosted } from "@/utils/netbird";
|
||||
import {
|
||||
REVERSE_PROXY_CLUSTERS_DOCS_LINK,
|
||||
ReverseProxyClusterToken,
|
||||
} from "@/interfaces/ReverseProxy";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const ClustersModal = ({ open, onOpenChange }: Props) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const [tab, setTab] = useState("domain");
|
||||
const [domain, setDomain] = useState("");
|
||||
const [token, setToken] = useState("");
|
||||
const [isGeneratingToken, setIsGeneratingToken] = useState(true);
|
||||
|
||||
const tokenRequest = useApiCall<ReverseProxyClusterToken>(
|
||||
"/reverse-proxies/proxy-tokens",
|
||||
);
|
||||
|
||||
const domainError = useMemo(() => {
|
||||
if (!domain) return "";
|
||||
const isValid = validator.isValidDomain(domain, {
|
||||
allowWildcard: false,
|
||||
allowOnlyTld: false,
|
||||
preventLeadingAndTrailingDots: true,
|
||||
});
|
||||
if (!isValid) {
|
||||
return "Please enter a valid TLD domain, e.g., company.com";
|
||||
}
|
||||
return "";
|
||||
}, [domain]);
|
||||
|
||||
const managementUrl = isNetBirdHosted()
|
||||
? "https://api.netbird.io"
|
||||
: GRPC_API_ORIGIN || "";
|
||||
|
||||
const dockerCommand = `docker run -d \\
|
||||
-v /var/lib/certs:/certs \\
|
||||
-e NB_PROXY_CERTIFICATE_DIRECTORY=/certs \\
|
||||
-e NB_PROXY_ALLOW_INSECURE=true \\
|
||||
-e NB_PROXY_MANAGEMENT_ADDRESS=${managementUrl} \\
|
||||
-e NB_PROXY_ACME_CERTIFICATES=true \\
|
||||
-e NB_PROXY_DOMAIN=${domain} \\
|
||||
-e NB_PROXY_LOG_LEVEL=info \\
|
||||
-e NB_PROXY_TOKEN=${token || "<TOKEN>"} \\
|
||||
-p 80:80 -p 443:443 \\
|
||||
netbirdio/reverse-proxy:latest`;
|
||||
|
||||
const generateToken = useCallback(async () => {
|
||||
setIsGeneratingToken(true);
|
||||
const promise = tokenRequest
|
||||
.post({
|
||||
name: domain,
|
||||
expires_in: 0,
|
||||
})
|
||||
.then((res) => {
|
||||
setToken(res?.plain_token ?? "");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsGeneratingToken(false);
|
||||
});
|
||||
|
||||
notify({
|
||||
title: "Proxy Token",
|
||||
description: "Failed to generate proxy token",
|
||||
promise,
|
||||
loadingMessage: "Generating proxy token...",
|
||||
showOnlyError: true,
|
||||
preventSuccessToast: true,
|
||||
});
|
||||
return promise;
|
||||
}, [domain, tokenRequest]);
|
||||
|
||||
const goToInstall = useCallback(() => {
|
||||
setTab("install");
|
||||
if (!token) generateToken();
|
||||
}, [token, generateToken]);
|
||||
|
||||
const finishSetup = () => {
|
||||
onOpenChange(false);
|
||||
mutate("/reverse-proxies/clusters");
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent maxWidthClass={"relative max-w-[600px]"} showClose={true}>
|
||||
<ModalHeader
|
||||
icon={<ServerIcon size={16} />}
|
||||
title={"Setup Cluster"}
|
||||
description={"Setup a proxy cluster"}
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
value={tab}
|
||||
onValueChange={(v) => (v === "install" ? goToInstall() : setTab(v))}
|
||||
>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"domain"}>
|
||||
<GlobeIcon size={14} />
|
||||
Domain
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value={"dns"}
|
||||
disabled={!domain.trim() || !!domainError}
|
||||
>
|
||||
<ListIcon size={14} />
|
||||
DNS Records
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value={"install"}
|
||||
disabled={!domain.trim() || !!domainError}
|
||||
>
|
||||
<SquareTerminalIcon size={14} />
|
||||
Run the Proxy
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={"domain"} className={"pb-8"}>
|
||||
<div className={"px-8 flex flex-col gap-6"}>
|
||||
<div>
|
||||
<Label>Domain</Label>
|
||||
<HelpText>
|
||||
Enter a domain name that will be used for your cluster.
|
||||
</HelpText>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
placeholder={"e.g., proxy.company.com"}
|
||||
value={domain}
|
||||
error={domainError}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setDomain(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Callout variant={"info"}>
|
||||
In order to run the proxy, please make sure your machine meets
|
||||
the following requirements:
|
||||
<ul className={"list-disc pl-4 mt-2 flex flex-col gap-1"}>
|
||||
<li>
|
||||
<span className={"text-white font-medium"}>
|
||||
Publicly accessible IP address
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={"text-white font-medium"}>Docker</span>{" "}
|
||||
installed and running
|
||||
</li>
|
||||
<li>
|
||||
<span className={"text-white font-medium"}>
|
||||
Port 80 and 443
|
||||
</span>{" "}
|
||||
open and not in use
|
||||
</li>
|
||||
</ul>
|
||||
</Callout>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"dns"} className={"pb-8"}>
|
||||
<div className={"px-8 flex flex-col"}>
|
||||
<div>
|
||||
<Label>Configure DNS</Label>
|
||||
<HelpText>
|
||||
Add the following DNS records pointing to your machine's
|
||||
public IP address.
|
||||
</HelpText>
|
||||
</div>
|
||||
<CardTable>
|
||||
<CardTable.Header>
|
||||
<CardTable.HeaderCell width={100}>Type</CardTable.HeaderCell>
|
||||
<CardTable.HeaderCell>Name</CardTable.HeaderCell>
|
||||
<CardTable.HeaderCell>Content</CardTable.HeaderCell>
|
||||
</CardTable.Header>
|
||||
<CardTable.Body>
|
||||
<CardTable.Row>
|
||||
<CardTable.Cell>A</CardTable.Cell>
|
||||
<CardTable.Cell copy copyText={domain}>
|
||||
{domain}
|
||||
</CardTable.Cell>
|
||||
<CardTable.Cell className={"italic"}>
|
||||
Your machine's IP
|
||||
</CardTable.Cell>
|
||||
</CardTable.Row>
|
||||
<CardTable.Row>
|
||||
<CardTable.Cell>CNAME</CardTable.Cell>
|
||||
<CardTable.Cell copy copyText={`*.${domain}`}>
|
||||
{`*.${domain}`}
|
||||
</CardTable.Cell>
|
||||
<CardTable.Cell copy copyText={domain}>
|
||||
{domain}
|
||||
</CardTable.Cell>
|
||||
</CardTable.Row>
|
||||
</CardTable.Body>
|
||||
</CardTable>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"install"} className={"pb-8"}>
|
||||
<div className={"px-8 flex flex-col"}>
|
||||
<div>
|
||||
<Label>Run the Proxy with Docker</Label>
|
||||
<HelpText>
|
||||
Run the following command on your machine to start the proxy.
|
||||
</HelpText>
|
||||
</div>
|
||||
<Code
|
||||
codeToCopy={dockerCommand}
|
||||
className={cn(
|
||||
"overflow-hidden",
|
||||
isGeneratingToken && "!border-nb-gray-930",
|
||||
)}
|
||||
showCopyIcon={!isGeneratingToken}
|
||||
>
|
||||
{isGeneratingToken && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 text-nb-gray-100 bg-nb-gray-950/90">
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
Generating proxy token...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Code.Line>docker run -d \</Code.Line>
|
||||
<Code.Line> -v /var/lib/certs:/certs \</Code.Line>
|
||||
<Code.Line>
|
||||
{" "}
|
||||
-e NB_PROXY_CERTIFICATE_DIRECTORY=/certs \
|
||||
</Code.Line>
|
||||
<Code.Line> -e NB_PROXY_ALLOW_INSECURE=true \</Code.Line>
|
||||
<Code.Line>
|
||||
{" "}
|
||||
-e NB_PROXY_MANAGEMENT_ADDRESS=
|
||||
<span className={"text-netbird"}>{managementUrl}</span> \
|
||||
</Code.Line>
|
||||
<Code.Line> -e NB_PROXY_ACME_CERTIFICATES=true \</Code.Line>
|
||||
<Code.Line>
|
||||
{" "}
|
||||
-e NB_PROXY_DOMAIN=
|
||||
<span className={"text-netbird"}>{domain}</span> \
|
||||
</Code.Line>
|
||||
<Code.Line> -e NB_PROXY_LOG_LEVEL=info \</Code.Line>
|
||||
<Code.Line>
|
||||
{" "}
|
||||
-e NB_PROXY_TOKEN=
|
||||
<span className={"text-netbird"}>{token || "<TOKEN>"}</span> \
|
||||
</Code.Line>
|
||||
<Code.Line> -p 80:80 -p 443:443 \</Code.Line>
|
||||
<Code.Line> netbirdio/reverse-proxy:latest</Code.Line>
|
||||
</Code>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={REVERSE_PROXY_CLUSTERS_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Proxy Cluster
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
{tab === "domain" && (
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("dns")}
|
||||
disabled={!domain.trim() || !!domainError}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{tab === "dns" && (
|
||||
<>
|
||||
<Button variant={"secondary"} onClick={() => setTab("domain")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant={"primary"} onClick={goToInstall}>
|
||||
Continue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{tab === "install" && (
|
||||
<>
|
||||
<Button variant={"secondary"} onClick={() => setTab("dns")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant={"primary"} onClick={finishSetup}>
|
||||
Finish Setup
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import CopyToClipboardText from "@components/CopyToClipboardText";
|
||||
import CircleIcon from "@/assets/icons/CircleIcon";
|
||||
import { ReverseProxyCluster } from "@/interfaces/ReverseProxy";
|
||||
import { ClusterTypeIndicator } from "@/modules/reverse-proxy/clusters/ClusterTypeIndicator";
|
||||
|
||||
type Props = {
|
||||
cluster: ReverseProxyCluster;
|
||||
};
|
||||
|
||||
export default function ClustersNameCell({ cluster }: Readonly<Props>) {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 ml-2">
|
||||
<CircleIcon active={cluster.online} size={8} inactiveDot={"gray"} />
|
||||
<CopyToClipboardText
|
||||
message={`${cluster.address} has been copied to clipboard`}
|
||||
>
|
||||
<span className="font-medium">{cluster.address}</span>
|
||||
</CopyToClipboardText>
|
||||
<ClusterTypeIndicator cluster={cluster} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loaded 20 of 30 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user