rebuild self-hosted page as Clusters with type features (#641)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Some checks failed
build and push / build_n_push (push) Has been cancelled
* feat(reverse-proxy): rebuild self-hosted page as Clusters with type + features The Self-Hosted Proxies page was account-only by design but the underlying API already returned every cluster the account could see. Lifting that filter and renaming the page surfaces shared clusters too — operators can see what NetBird-deployed clusters are reachable alongside their own self-hosted ones, with online status and feature support visible per row. ReverseProxyCluster matches the new backend shape: `type` (account/shared), `online`, and the three capability flags. The `isSelfHostedCluster` provider hook now compares against `type === account` instead of a deprecated boolean. Page folder renamed self-hosted-proxies → clusters (history-preserving git mv). Table columns: Cluster (with an EphemeralPeerIndicator-style icon next to the name marking account vs shared and a colored dot for online status), Connected Proxies (plain numeric), Features (one tooltip-backed badge per supported capability), Actions (Delete only on account-owned rows; shared clusters render an empty action cell). Empty state shows when the list is fully empty with a doc link in the page header. Sidebar entry restored under Reverse Proxy. * Update record in modal, update doc link, update modal title * update reverse proxy documentation links to latest anchors * update cluster modal description to "proxy cluster" instead of "self-hosted cluster" --------- Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
This commit is contained in:
@@ -3,6 +3,6 @@ import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Self-Hosted Proxies - Reverse Proxy - ${globalMetaTitle}`,
|
||||
title: `Clusters - Reverse Proxy - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
@@ -13,11 +13,8 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { REVERSE_PROXY_CLUSTERS_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const SelfHostedProxiesTable = lazy(
|
||||
() =>
|
||||
import(
|
||||
"@/modules/reverse-proxy/self-hosted-proxies/SelfHostedProxiesTable"
|
||||
),
|
||||
const ClustersTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/clusters/ClustersTable"),
|
||||
);
|
||||
|
||||
export default function ReverseProxyClustersPage() {
|
||||
@@ -36,31 +33,32 @@ export default function ReverseProxyClustersPage() {
|
||||
icon={<ReverseProxyIcon size={16} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/self-hosted-proxies"}
|
||||
label={"Self-Hosted Proxies"}
|
||||
href={"/reverse-proxy/clusters"}
|
||||
label={"Clusters"}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Self-Hosted Proxies</h1>
|
||||
<h1 ref={headingRef}>Clusters</h1>
|
||||
<Paragraph>
|
||||
Setup self-hosted proxies on your own infrastructure for full control
|
||||
over traffic and geographic location.
|
||||
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"}>
|
||||
Self-Hosted Proxies
|
||||
Clusters
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Self-Hosted Proxies"}
|
||||
page={"Clusters"}
|
||||
hasAccess={permission?.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<SelfHostedProxiesTable headingTarget={portalTarget} />
|
||||
<ClustersTable headingTarget={portalTarget} />
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
@@ -17,6 +17,7 @@ import { Peer } from "@/interfaces/Peer";
|
||||
import {
|
||||
ReverseProxy,
|
||||
ReverseProxyCluster,
|
||||
ReverseProxyClusterType,
|
||||
ReverseProxyDomain,
|
||||
ReverseProxyFlatTarget,
|
||||
ReverseProxyTarget,
|
||||
@@ -152,7 +153,10 @@ export default function ReverseProxiesProvider({
|
||||
const isSelfHostedCluster = useCallback(
|
||||
(clusterAddress?: string) => {
|
||||
if (!clusterAddress) return false;
|
||||
return !!clusters?.find((c) => c.address === clusterAddress)?.self_hosted;
|
||||
return (
|
||||
clusters?.find((c) => c.address === clusterAddress)?.type ===
|
||||
ReverseProxyClusterType.ACCOUNT
|
||||
);
|
||||
},
|
||||
[clusters],
|
||||
);
|
||||
|
||||
@@ -163,11 +163,20 @@ 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;
|
||||
self_hosted: boolean;
|
||||
supports_custom_ports?: boolean;
|
||||
require_subdomain?: boolean;
|
||||
supports_crowdsec?: boolean;
|
||||
}
|
||||
|
||||
export interface ReverseProxyClusterToken {
|
||||
@@ -217,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";
|
||||
|
||||
@@ -166,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
|
||||
|
||||
45
src/modules/reverse-proxy/clusters/ClusterTypeIndicator.tsx
Normal file
45
src/modules/reverse-proxy/clusters/ClusterTypeIndicator.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -6,20 +6,28 @@ import * as React from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { ReverseProxyCluster } from "@/interfaces/ReverseProxy";
|
||||
import {
|
||||
ReverseProxyCluster,
|
||||
ReverseProxyClusterType,
|
||||
} from "@/interfaces/ReverseProxy";
|
||||
|
||||
type Props = {
|
||||
cluster: ReverseProxyCluster;
|
||||
};
|
||||
|
||||
export default function SelfHostedProxiesActionCell({
|
||||
cluster,
|
||||
}: Readonly<Props>) {
|
||||
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}'?`,
|
||||
@@ -6,18 +6,14 @@ type Props = {
|
||||
cluster: ReverseProxyCluster;
|
||||
};
|
||||
|
||||
export default function SelfHostedProxiesConnectedCell({
|
||||
cluster,
|
||||
}: Readonly<Props>) {
|
||||
export default function ClustersConnectedCell({ cluster }: Readonly<Props>) {
|
||||
const count = cluster.connected_proxies;
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"gray"}>
|
||||
<Server size={11} className={"relative -top-[0.5px]"} />
|
||||
<Server size={11} />
|
||||
<div>
|
||||
<span className="font-medium text-xs">
|
||||
{count > 0 ? count : "No Proxies Connected"}
|
||||
</span>
|
||||
<span className={"font-medium text-xs"}>{count}</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
75
src/modules/reverse-proxy/clusters/ClustersFeaturesCell.tsx
Normal file
75
src/modules/reverse-proxy/clusters/ClustersFeaturesCell.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ type Props = {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const SelfHostedProxiesModal = ({ open, onOpenChange }: Props) => {
|
||||
export const ClustersModal = ({ open, onOpenChange }: Props) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const [tab, setTab] = useState("domain");
|
||||
const [domain, setDomain] = useState("");
|
||||
@@ -119,8 +119,8 @@ export const SelfHostedProxiesModal = ({ open, onOpenChange }: Props) => {
|
||||
<ModalContent maxWidthClass={"relative max-w-[600px]"} showClose={true}>
|
||||
<ModalHeader
|
||||
icon={<ServerIcon size={16} />}
|
||||
title={"Setup Proxy"}
|
||||
description={"Setup a self-hosted reverse proxy"}
|
||||
title={"Setup Cluster"}
|
||||
description={"Setup a proxy cluster"}
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
@@ -154,7 +154,7 @@ export const SelfHostedProxiesModal = ({ open, onOpenChange }: Props) => {
|
||||
<div>
|
||||
<Label>Domain</Label>
|
||||
<HelpText>
|
||||
Enter a domain name that will be used for your proxy.
|
||||
Enter a domain name that will be used for your cluster.
|
||||
</HelpText>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
@@ -201,13 +201,13 @@ export const SelfHostedProxiesModal = ({ open, onOpenChange }: Props) => {
|
||||
</div>
|
||||
<CardTable>
|
||||
<CardTable.Header>
|
||||
<CardTable.HeaderCell width={120}>Type</CardTable.HeaderCell>
|
||||
<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 Record</CardTable.Cell>
|
||||
<CardTable.Cell>A</CardTable.Cell>
|
||||
<CardTable.Cell copy copyText={domain}>
|
||||
{domain}
|
||||
</CardTable.Cell>
|
||||
@@ -216,12 +216,12 @@ export const SelfHostedProxiesModal = ({ open, onOpenChange }: Props) => {
|
||||
</CardTable.Cell>
|
||||
</CardTable.Row>
|
||||
<CardTable.Row>
|
||||
<CardTable.Cell>A Record</CardTable.Cell>
|
||||
<CardTable.Cell>CNAME</CardTable.Cell>
|
||||
<CardTable.Cell copy copyText={`*.${domain}`}>
|
||||
{`*.${domain}`}
|
||||
</CardTable.Cell>
|
||||
<CardTable.Cell className={"italic"}>
|
||||
Your machine's IP
|
||||
<CardTable.Cell copy copyText={domain}>
|
||||
{domain}
|
||||
</CardTable.Cell>
|
||||
</CardTable.Row>
|
||||
</CardTable.Body>
|
||||
@@ -291,7 +291,7 @@ export const SelfHostedProxiesModal = ({ open, onOpenChange }: Props) => {
|
||||
href={REVERSE_PROXY_CLUSTERS_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Self-Hosted Proxies
|
||||
Proxy Cluster
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
@@ -1,20 +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 SelfHostedProxiesNameCell({ cluster }: Readonly<Props>) {
|
||||
export default function ClustersNameCell({ cluster }: Readonly<Props>) {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 ml-2">
|
||||
<CircleIcon active={true} size={8} inactiveDot={"gray"} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -10,25 +10,26 @@ import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import { PlusCircle, ServerIcon } from "lucide-react";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { ReverseProxyCluster } from "@/interfaces/ReverseProxy";
|
||||
import useFetchApi from "@/utils/api";
|
||||
import SelfHostedProxiesActionCell from "@/modules/reverse-proxy/self-hosted-proxies/SelfHostedProxiesActionCell";
|
||||
import SelfHostedProxiesConnectedCell from "@/modules/reverse-proxy/self-hosted-proxies/SelfHostedProxiesConnectedCell";
|
||||
import { SelfHostedProxiesModal } from "@/modules/reverse-proxy/self-hosted-proxies/SelfHostedProxiesModal";
|
||||
import SelfHostedProxiesNameCell from "@/modules/reverse-proxy/self-hosted-proxies/SelfHostedProxiesNameCell";
|
||||
import ClustersActionCell from "@/modules/reverse-proxy/clusters/ClustersActionCell";
|
||||
import ClustersConnectedCell from "@/modules/reverse-proxy/clusters/ClustersConnectedCell";
|
||||
import ClustersFeaturesCell from "@/modules/reverse-proxy/clusters/ClustersFeaturesCell";
|
||||
import { ClustersModal } from "@/modules/reverse-proxy/clusters/ClustersModal";
|
||||
import ClustersNameCell from "@/modules/reverse-proxy/clusters/ClustersNameCell";
|
||||
|
||||
const ClustersColumns: ColumnDef<ReverseProxyCluster>[] = [
|
||||
{
|
||||
accessorKey: "address",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Proxy Cluster</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Cluster</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <SelfHostedProxiesNameCell cluster={row.original} />,
|
||||
cell: ({ row }) => <ClustersNameCell cluster={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "connected_proxies",
|
||||
@@ -37,9 +38,14 @@ const ClustersColumns: ColumnDef<ReverseProxyCluster>[] = [
|
||||
<DataTableHeader column={column}>Connected Proxies</DataTableHeader>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<SelfHostedProxiesConnectedCell cluster={row.original} />
|
||||
),
|
||||
sortingFn: "basic",
|
||||
cell: ({ row }) => <ClustersConnectedCell cluster={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "features",
|
||||
header: () => <span className={"font-medium text-xs"}>Features</span>,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <ClustersFeaturesCell cluster={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "searchString",
|
||||
@@ -49,7 +55,7 @@ const ClustersColumns: ColumnDef<ReverseProxyCluster>[] = [
|
||||
id: "actions",
|
||||
accessorKey: "address",
|
||||
header: "",
|
||||
cell: ({ row }) => <SelfHostedProxiesActionCell cluster={row.original} />,
|
||||
cell: ({ row }) => <ClustersActionCell cluster={row.original} />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -57,9 +63,7 @@ type Props = {
|
||||
headingTarget?: HTMLHeadingElement | null;
|
||||
};
|
||||
|
||||
export default function SelfHostedProxiesTable({
|
||||
headingTarget,
|
||||
}: Readonly<Props>) {
|
||||
export default function ClustersTable({ headingTarget }: Readonly<Props>) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const path = usePathname();
|
||||
const { permission } = usePermissions();
|
||||
@@ -67,9 +71,7 @@ export default function SelfHostedProxiesTable({
|
||||
"/reverse-proxies/clusters",
|
||||
);
|
||||
|
||||
const selfHostedClusters = useMemo(() => {
|
||||
return clusters?.filter((c) => c.self_hosted) ?? [];
|
||||
}, [clusters]);
|
||||
const rows = clusters ?? [];
|
||||
|
||||
const [addModalOpen, setAddModalOpen] = useState(false);
|
||||
|
||||
@@ -85,7 +87,7 @@ export default function SelfHostedProxiesTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelfHostedProxiesModal
|
||||
<ClustersModal
|
||||
open={addModalOpen}
|
||||
onOpenChange={setAddModalOpen}
|
||||
key={addModalOpen ? 1 : 0}
|
||||
@@ -96,13 +98,13 @@ export default function SelfHostedProxiesTable({
|
||||
isLoading={isLoading}
|
||||
inset={false}
|
||||
keepStateInLocalStorage={false}
|
||||
text={"Self-Hosted Proxies"}
|
||||
text={"Clusters"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
columns={ClustersColumns}
|
||||
data={selfHostedClusters}
|
||||
data={rows}
|
||||
useRowId={true}
|
||||
searchPlaceholder={"Search by proxy cluster domain..."}
|
||||
searchPlaceholder={"Search by cluster domain..."}
|
||||
columnVisibility={{ searchString: false }}
|
||||
getStartedCard={
|
||||
<GetStartedTest
|
||||
@@ -113,9 +115,9 @@ export default function SelfHostedProxiesTable({
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Setup Your Own Self-Hosted Proxy Cluster"}
|
||||
title={"No clusters available"}
|
||||
description={
|
||||
"Setup self-hosted proxies on your own infrastructure for full control over traffic and geographic location."
|
||||
"There are no shared clusters connected to your account and no self-hosted clusters configured. Set up a self-hosted cluster to route traffic through your own infrastructure — see the documentation linked above for setup steps."
|
||||
}
|
||||
button={
|
||||
<Button
|
||||
@@ -124,14 +126,14 @@ export default function SelfHostedProxiesTable({
|
||||
disabled={!permission?.services?.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Setup Proxy
|
||||
Setup Self-Hosted Cluster
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
rightSide={() => (
|
||||
<>
|
||||
{selfHostedClusters.length > 0 && (
|
||||
{rows.length > 0 && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
@@ -139,7 +141,7 @@ export default function SelfHostedProxiesTable({
|
||||
disabled={!permission?.services?.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Setup Proxy
|
||||
Setup Self-Hosted Cluster
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -149,10 +151,10 @@ export default function SelfHostedProxiesTable({
|
||||
<>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={selfHostedClusters.length === 0}
|
||||
disabled={rows.length === 0}
|
||||
/>
|
||||
<DataTableRefreshButton
|
||||
isDisabled={selfHostedClusters.length === 0}
|
||||
isDisabled={rows.length === 0}
|
||||
onClick={() => {
|
||||
mutate("/reverse-proxies/clusters").then();
|
||||
}}
|
||||
Reference in New Issue
Block a user