remote node changes

This commit is contained in:
miloschwartz
2025-12-17 16:50:39 -05:00
parent a5b203af27
commit 6f50fb8a4f
12 changed files with 103 additions and 45 deletions

View File

@@ -100,6 +100,7 @@
"siteTunnelDescription": "Determine how you want to connect to the site", "siteTunnelDescription": "Determine how you want to connect to the site",
"siteNewtCredentials": "Credentials", "siteNewtCredentials": "Credentials",
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server", "siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
"remoteNodeCredentialsDescription": "This is how the remote node will authenticate with the server",
"siteCredentialsSave": "Save the Credentials", "siteCredentialsSave": "Save the Credentials",
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", "siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
"siteInfo": "Site Information", "siteInfo": "Site Information",
@@ -1671,7 +1672,7 @@
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes", "remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud", "remoteExitNodeDescription": "Self-host your own remote relay and proxy server nodes",
"remoteExitNodes": "Nodes", "remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Search nodes...", "searchRemoteExitNodes": "Search nodes...",
"remoteExitNodeAdd": "Add Node", "remoteExitNodeAdd": "Add Node",
@@ -1681,20 +1682,22 @@
"remoteExitNodeConfirmDelete": "Confirm Delete Node", "remoteExitNodeConfirmDelete": "Confirm Delete Node",
"remoteExitNodeDelete": "Delete Node", "remoteExitNodeDelete": "Delete Node",
"sidebarRemoteExitNodes": "Remote Nodes", "sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Secret",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Create Node", "title": "Create Remote Node",
"description": "Create a new node to extend network connectivity", "description": "Create a new self-hosted remote relay and proxy server node",
"viewAllButton": "View All Nodes", "viewAllButton": "View All Nodes",
"strategy": { "strategy": {
"title": "Creation Strategy", "title": "Creation Strategy",
"description": "Choose this to manually configure the node or generate new credentials.", "description": "Select how you want to create the remote node",
"adopt": { "adopt": {
"title": "Adopt Node", "title": "Adopt Node",
"description": "Choose this if you already have the credentials for the node." "description": "Choose this if you already have the credentials for the node."
}, },
"generate": { "generate": {
"title": "Generate Keys", "title": "Generate Keys",
"description": "Choose this if you want to generate new keys for the node" "description": "Choose this if you want to generate new keys for the node."
} }
}, },
"adopt": { "adopt": {
@@ -2308,5 +2311,6 @@
"organizationLoginPageTitle": "Organization Login Page", "organizationLoginPageTitle": "Organization Login Page",
"organizationLoginPageDescription": "Customize the login page for this organization", "organizationLoginPageDescription": "Customize the login page for this organization",
"resourceLoginPageTitle": "Resource Login Page", "resourceLoginPageTitle": "Resource Login Page",
"resourceLoginPageDescription": "Customize the login page for individual resources" "resourceLoginPageDescription": "Customize the login page for individual resources",
"enterConfirmation": "Enter confirmation"
} }

View File

@@ -1,5 +1,6 @@
import { db, exitNodes, newts } from "@server/db"; import { db, exitNodes, newts } from "@server/db";
import { orgs, roleSites, sites, userSites } from "@server/db"; import { orgs, roleSites, sites, userSites } from "@server/db";
import { remoteExitNodes } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
@@ -104,12 +105,17 @@ function querySites(orgId: string, accessibleSiteIds: number[]) {
newtVersion: newts.version, newtVersion: newts.version,
exitNodeId: sites.exitNodeId, exitNodeId: sites.exitNodeId,
exitNodeName: exitNodes.name, exitNodeName: exitNodes.name,
exitNodeEndpoint: exitNodes.endpoint exitNodeEndpoint: exitNodes.endpoint,
remoteExitNodeId: remoteExitNodes.remoteExitNodeId
}) })
.from(sites) .from(sites)
.leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .leftJoin(orgs, eq(sites.orgId, orgs.orgId))
.leftJoin(newts, eq(newts.siteId, sites.siteId)) .leftJoin(newts, eq(newts.siteId, sites.siteId))
.leftJoin(exitNodes, eq(exitNodes.exitNodeId, sites.exitNodeId)) .leftJoin(exitNodes, eq(exitNodes.exitNodeId, sites.exitNodeId))
.leftJoin(
remoteExitNodes,
eq(remoteExitNodes.exitNodeId, sites.exitNodeId)
)
.where( .where(
and( and(
inArray(sites.siteId, accessibleSiteIds), inArray(sites.siteId, accessibleSiteIds),

View File

@@ -131,10 +131,10 @@ export default function CredentialsPage() {
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("generatedcredentials")} {t("credentials")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("regenerateCredentials")} {t("remoteNodeCredentialsDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@@ -143,7 +143,7 @@ export default function CredentialsPage() {
<InfoSections cols={3}> <InfoSections cols={3}>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("endpoint") || "Endpoint"} {t("endpoint")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<CopyToClipboard <CopyToClipboard
@@ -153,8 +153,7 @@ export default function CredentialsPage() {
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("remoteExitNodeId") || {t("remoteExitNodeId")}
"Remote Exit Node ID"}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{displayRemoteExitNodeId ? ( {displayRemoteExitNodeId ? (
@@ -168,7 +167,7 @@ export default function CredentialsPage() {
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("secretKey") || "Secret Key"} {t("remoteExitNodeSecretKey")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{displaySecret ? ( {displaySecret ? (

View File

@@ -43,7 +43,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
title={`Remote Exit Node ${remoteExitNode?.name || "Unknown"}`} title={`Remote Node ${remoteExitNode?.name || "Unknown"}`}
description="Manage your remote exit node settings and configuration" description="Manage your remote exit node settings and configuration"
/> />

View File

@@ -2,7 +2,9 @@ import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable"; import ExitNodesTable, {
RemoteExitNodeRow
} from "@app/components/ExitNodesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";

View File

@@ -53,7 +53,8 @@ export default async function SitesPage(props: SitesPageProps) {
newtVersion: site.newtVersion || undefined, newtVersion: site.newtVersion || undefined,
newtUpdateAvailable: site.newtUpdateAvailable || false, newtUpdateAvailable: site.newtUpdateAvailable || false,
exitNodeName: site.exitNodeName || undefined, exitNodeName: site.exitNodeName || undefined,
exitNodeEndpoint: site.exitNodeEndpoint || undefined exitNodeEndpoint: site.exitNodeEndpoint || undefined,
remoteExitNodeId: (site as any).remoteExitNodeId || undefined
}; };
}); });

View File

@@ -116,7 +116,12 @@ export default function ConfirmDeleteDialog({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input {...field} /> <Input
{...field}
placeholder={t(
"enterConfirmation"
)}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -19,7 +19,7 @@ export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
return ( return (
<Alert> <Alert>
<AlertDescription className="mt-4"> <AlertDescription>
<InfoSections cols={2}> <InfoSections cols={2}>
<> <>
<InfoSection> <InfoSection>

View File

@@ -10,6 +10,8 @@ interface DataTableProps<TData, TValue> {
createRemoteExitNode?: () => void; createRemoteExitNode?: () => void;
onRefresh?: () => void; onRefresh?: () => void;
isRefreshing?: boolean; isRefreshing?: boolean;
columnVisibility?: Record<string, boolean>;
enableColumnVisibility?: boolean;
} }
export function ExitNodesDataTable<TData, TValue>({ export function ExitNodesDataTable<TData, TValue>({
@@ -17,7 +19,9 @@ export function ExitNodesDataTable<TData, TValue>({
data, data,
createRemoteExitNode, createRemoteExitNode,
onRefresh, onRefresh,
isRefreshing isRefreshing,
columnVisibility,
enableColumnVisibility
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const t = useTranslations(); const t = useTranslations();
@@ -36,6 +40,10 @@ export function ExitNodesDataTable<TData, TValue>({
id: "name", id: "name",
desc: false desc: false
}} }}
columnVisibility={columnVisibility}
enableColumnVisibility={enableColumnVisibility}
stickyLeftColumn="name"
stickyRightColumn="actions"
/> />
); );
} }

View File

@@ -2,7 +2,7 @@
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { ExitNodesDataTable } from "./ExitNodesDataTable"; import { ExitNodesDataTable } from "@app/components/ExitNodesDataTable";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -246,12 +246,13 @@ export default function ExitNodesTable({
}, },
{ {
id: "actions", id: "actions",
header: () => <span className="p-3">{t("actions")}</span>, enableHiding: false,
header: () => <span className="p-3"></span>,
cell: ({ row }) => { cell: ({ row }) => {
const nodeRow = row.original; const nodeRow = row.original;
const remoteExitNodeId = nodeRow.id; const remoteExitNodeId = nodeRow.id;
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 justify-end">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0"> <Button variant="ghost" className="h-8 w-8 p-0">
@@ -283,7 +284,7 @@ export default function ExitNodesTable({
<Link <Link
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`} href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
> >
<Button variant={"secondary"} size="sm"> <Button variant={"outline"}>
{t("edit")} {t("edit")}
<ArrowRight className="ml-2 w-4 h-4" /> <ArrowRight className="ml-2 w-4 h-4" />
</Button> </Button>
@@ -327,6 +328,11 @@ export default function ExitNodesTable({
} }
onRefresh={refreshData} onRefresh={refreshData}
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
columnVisibility={{
type: false,
address: false,
}}
enableColumnVisibility={true}
/> />
</> </>
); );

View File

@@ -39,7 +39,8 @@ export function HorizontalTabs({
.replace("{niceId}", params.niceId as string) .replace("{niceId}", params.niceId as string)
.replace("{userId}", params.userId as string) .replace("{userId}", params.userId as string)
.replace("{clientId}", params.clientId as string) .replace("{clientId}", params.clientId as string)
.replace("{apiKeyId}", params.apiKeyId as string); .replace("{apiKeyId}", params.apiKeyId as string)
.replace("{remoteExitNodeId}", params.remoteExitNodeId as string);
} }
return ( return (

View File

@@ -13,6 +13,7 @@ import { Button } from "@app/components/ui/button";
import { import {
ArrowRight, ArrowRight,
ArrowUpDown, ArrowUpDown,
ArrowUpRight,
Check, Check,
MoreHorizontal, MoreHorizontal,
X X
@@ -46,6 +47,7 @@ export type SiteRow = {
address?: string; address?: string;
exitNodeName?: string; exitNodeName?: string;
exitNodeEndpoint?: string; exitNodeEndpoint?: string;
remoteExitNodeId?: string;
}; };
type SitesTableProps = { type SitesTableProps = {
@@ -303,27 +305,51 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
}, },
cell: ({ row }) => { cell: ({ row }) => {
const originalRow = row.original; const originalRow = row.original;
return originalRow.exitNodeName ? ( if (!originalRow.exitNodeName) {
<div className="flex items-center space-x-2"> return "-";
<span>{originalRow.exitNodeName}</span> }
{build == "saas" &&
originalRow.exitNodeName && const isCloudNode =
[ build == "saas" &&
"mercury", originalRow.exitNodeName &&
"venus", [
"earth", "mercury",
"mars", "venus",
"jupiter", "earth",
"saturn", "mars",
"uranus", "jupiter",
"neptune" "saturn",
].includes( "uranus",
originalRow.exitNodeName.toLowerCase() "neptune"
) && <Badge variant="secondary">Cloud</Badge>} ].includes(originalRow.exitNodeName.toLowerCase());
</div>
) : ( if (isCloudNode) {
"-" const capitalizedName =
); originalRow.exitNodeName.charAt(0).toUpperCase() +
originalRow.exitNodeName.slice(1).toLowerCase();
return (
<Badge variant="secondary">
Pangolin {capitalizedName}
</Badge>
);
}
// Self-hosted node
if (originalRow.remoteExitNodeId) {
return (
<Link
href={`/${originalRow.orgId}/settings/remote-exit-nodes/${originalRow.remoteExitNodeId}`}
>
<Button variant="outline">
{originalRow.exitNodeName}
<ArrowUpRight className="ml-2 h-4 w-4" />
</Button>
</Link>
);
}
// Fallback if no remoteExitNodeId
return <span>{originalRow.exitNodeName}</span>;
} }
}, },
{ {