From cfbbdedaf510f5ce22f770a7650574cecaa9930d Mon Sep 17 00:00:00 2001
From: Owen
Date: Tue, 30 Jun 2026 17:44:35 -0400
Subject: [PATCH] rework ui
---
messages/en-US.json | 14 +-
.../[remoteExitNodeId]/networking/page.tsx | 206 +++++++++---------
.../settings/sites/[niceId]/general/page.tsx | 64 ------
src/components/SitesTable.tsx | 55 +++++
4 files changed, 167 insertions(+), 172 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index b8cf7d592..993c2fb45 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -2388,23 +2388,21 @@
"sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Secret",
+ "remoteExitNodeNetworkingTitle": "Network Settings",
+ "remoteExitNodeNetworkingDescription": "Configure how this remote exit node routes traffic and which sites prefer to connect through it.",
+ "remoteExitNodeNetworkingSave": "Save Settings",
+ "remoteExitNodeNetworkingSaveSuccessTitle": "Network settings saved",
+ "remoteExitNodeNetworkingSaveSuccessDescription": "Network settings have been updated successfully.",
+ "remoteExitNodeNetworkingSaveError": "Failed to save network settings",
"remoteExitNodeNetworkingSubnetsTitle": "Remote Subnets",
"remoteExitNodeNetworkingSubnetsDescription": "Define the CIDR ranges that this remote exit node will route traffic to. Type a valid CIDR (e.g. 10.0.0.0/8) and press Enter to add.",
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsSave": "Save Subnets",
- "remoteExitNodeNetworkingSubnetsSaveSuccessTitle": "Subnets saved",
- "remoteExitNodeNetworkingSubnetsSaveSuccessDescription": "Remote subnets have been updated successfully.",
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
- "remoteExitNodeNetworkingSubnetsSaveError": "Failed to save subnets",
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced to connect through this remote exit node.",
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
- "remoteExitNodeNetworkingLabelsSave": "Save Labels",
- "remoteExitNodeNetworkingLabelsSaveSuccessTitle": "Labels saved",
- "remoteExitNodeNetworkingLabelsSaveSuccessDescription": "Preference labels have been updated successfully.",
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
- "remoteExitNodeNetworkingLabelsSaveError": "Failed to save labels",
"remoteExitNodeCreate": {
"title": "Create Remote Node",
"description": "Create a new self-hosted remote relay and proxy server node",
diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/networking/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/networking/page.tsx
index abac12488..92eba552b 100644
--- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/networking/page.tsx
+++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/networking/page.tsx
@@ -3,14 +3,18 @@
import { useEffect, useMemo, useState } from "react";
import {
SettingsContainer,
+ SettingsFormCell,
+ SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
+ SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { Button } from "@app/components/ui/button";
+import { Label } from "@app/components/ui/label";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
@@ -47,13 +51,13 @@ export default function NetworkingPage() {
const [subnets, setSubnets] = useState([]);
const [activeTagIndex, setActiveTagIndex] = useState(null);
const [loadingSubnets, setLoadingSubnets] = useState(true);
- const [savingSubnets, setSavingSubnets] = useState(false);
// Labels state
const [selectedLabels, setSelectedLabels] = useState([]);
const [labelSearchQuery, setLabelSearchQuery] = useState("");
const [loadingLabels, setLoadingLabels] = useState(true);
- const [savingLabels, setSavingLabels] = useState(false);
+
+ const [saving, setSaving] = useState(false);
const [debouncedLabelQuery] = useDebounce(labelSearchQuery, 150);
@@ -135,17 +139,27 @@ export default function NetworkingPage() {
loadLabels();
}, [remoteExitNode.remoteExitNodeId]);
- const handleSaveSubnets = async () => {
- setSavingSubnets(true);
+ const handleSave = async () => {
+ setSaving(true);
try {
- await api.post>(
- `/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/resources`,
- { destinations: subnets.map((s) => s.text) }
- );
+ await Promise.all([
+ api.post<
+ AxiosResponse
+ >(
+ `/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/resources`,
+ { destinations: subnets.map((s) => s.text) }
+ ),
+ api.post<
+ AxiosResponse
+ >(
+ `/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/preference-labels`,
+ { labelIds: selectedLabels.map((l) => parseInt(l.id)) }
+ )
+ ]);
toast({
- title: t("remoteExitNodeNetworkingSubnetsSaveSuccessTitle"),
+ title: t("remoteExitNodeNetworkingSaveSuccessTitle"),
description: t(
- "remoteExitNodeNetworkingSubnetsSaveSuccessDescription"
+ "remoteExitNodeNetworkingSaveSuccessDescription"
)
});
} catch (error) {
@@ -154,38 +168,10 @@ export default function NetworkingPage() {
title: t("error"),
description:
formatAxiosError(error) ||
- t("remoteExitNodeNetworkingSubnetsSaveError")
+ t("remoteExitNodeNetworkingSaveError")
});
} finally {
- setSavingSubnets(false);
- }
- };
-
- const handleSaveLabels = async () => {
- setSavingLabels(true);
- try {
- await api.post<
- AxiosResponse
- >(
- `/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/preference-labels`,
- { labelIds: selectedLabels.map((l) => parseInt(l.id)) }
- );
- toast({
- title: t("remoteExitNodeNetworkingLabelsSaveSuccessTitle"),
- description: t(
- "remoteExitNodeNetworkingLabelsSaveSuccessDescription"
- )
- });
- } catch (error) {
- toast({
- variant: "destructive",
- title: t("error"),
- description:
- formatAxiosError(error) ||
- t("remoteExitNodeNetworkingLabelsSaveError")
- });
- } finally {
- setSavingLabels(false);
+ setSaving(false);
}
};
@@ -194,73 +180,93 @@ export default function NetworkingPage() {
- {t("remoteExitNodeNetworkingSubnetsTitle")}
+ {t("remoteExitNodeNetworkingTitle")}
- {t.rich("remoteExitNodeNetworkingSubnetsDescription", {
- code: (chunks) => {chunks}
- })}{" "}
-
- {t("learnMore")}
-
-
+ {t("remoteExitNodeNetworkingDescription")}
- cidrRegex.test(tag.trim())}
- activeTagIndex={activeTagIndex}
- setActiveTagIndex={setActiveTagIndex}
- disabled={loadingSubnets}
- allowDuplicates={false}
- inlineTags={true}
- />
+
+
+
+
+
+
+ cidrRegex.test(tag.trim())
+ }
+ activeTagIndex={activeTagIndex}
+ setActiveTagIndex={setActiveTagIndex}
+ disabled={loadingSubnets}
+ allowDuplicates={false}
+ size="sm"
+ inlineTags={true}
+ />
+
+ {t.rich(
+ "remoteExitNodeNetworkingSubnetsDescription",
+ {
+ code: (chunks) => (
+ {chunks}
+ )
+ }
+ )}{" "}
+
+ {t("learnMore")}
+
+
+
+
+
+
+
+
+
+
+ {t(
+ "remoteExitNodeNetworkingLabelsDescription"
+ )}
+
+
+
+
+
-
-
-
-
-
-
-
- {t("remoteExitNodeNetworkingLabelsTitle")}
-
-
- {t("remoteExitNodeNetworkingLabelsDescription")}
-
-
-
-
-
-
-
diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
index 37a92c409..fdbfc387d 100644
--- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
+++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
@@ -43,7 +43,6 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
import { Button as ButtonUI } from "@/components/ui/button";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
-import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required"),
@@ -73,23 +72,6 @@ export default function GeneralPage() {
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState(
null
);
- const [isRestartDialogOpen, setIsRestartDialogOpen] = useState(false);
-
- async function restartSite() {
- try {
- await api.post(`/site/${site?.siteId}/restart`);
- toast({
- title: t("siteRestarted"),
- description: t("siteRestartedDescription")
- });
- } catch (e) {
- toast({
- variant: "destructive",
- title: t("siteErrorRestart"),
- description: formatAxiosError(e, t("siteErrorRestartDescription"))
- });
- }
- }
const orgAutoUpdate = org.org.settingsEnableGlobalNewtAutoUpdate ?? false;
@@ -367,52 +349,6 @@ export default function GeneralPage() {
- {site && site.type === "newt" && (
- <>
-
- {t.rich("siteRestartDialogMessage", {
- name: site.name,
- b: (chunks) => {chunks}
- })}
-
- }
- buttonText={t("siteRestartButton")}
- onConfirm={restartSite}
- string={site.name}
- warningText={t("siteRestartWarning")}
- title={t("siteRestartTitle")}
- />
-
-
-
- {t("siteRestartTitle")}
-
-
- {t("siteRestartDescription")}
-
-
-
-
-
- {t("siteRestartBody")}
-
-
-
-
- setIsRestartDialogOpen(true)}
- >
- {t("siteRestartButton")}
-
-
-
- >
- )}
);
}
diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx
index 3dc7a56da..efcf83e72 100644
--- a/src/components/SitesTable.tsx
+++ b/src/components/SitesTable.tsx
@@ -107,6 +107,7 @@ export default function SitesTable({
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deleteWithResources, setDeleteWithResources] = useState(false);
const [selectedSite, setSelectedSite] = useState(null);
+ const [restartingSite, setRestartingSite] = useState(null);
const [resourcesDialogSite, setResourcesDialogSite] =
useState(null);
const [isRefreshing, startTransition] = useTransition();
@@ -159,6 +160,24 @@ export default function SitesTable({
});
}
+ async function restartSite(siteId: number) {
+ try {
+ await api.post(`/site/${siteId}/restart`);
+ toast({
+ title: t("siteRestarted"),
+ description: t("siteRestartedDescription")
+ });
+ } catch (e) {
+ toast({
+ variant: "destructive",
+ title: t("siteErrorRestart"),
+ description: formatAxiosError(e, t("siteErrorRestartDescription"))
+ });
+ } finally {
+ setRestartingSite(null);
+ }
+ }
+
function deleteSite(siteId: number, withResources: boolean) {
startTransition(async () => {
await api
@@ -526,6 +545,20 @@ export default function SitesTable({
+ {siteRow.type === "newt" && (
+ <>
+
+ setRestartingSite(siteRow)
+ }
+ >
+
+ {t("siteRestartButton")}
+
+
+
+ >
+ )}
{
setSelectedSite(siteRow);
@@ -654,6 +687,28 @@ export default function SitesTable({
+ {restartingSite && (
+ {
+ if (!val) setRestartingSite(null);
+ }}
+ dialog={
+
+ {t.rich("siteRestartDialogMessage", {
+ name: restartingSite.name,
+ b: (chunks) => {chunks}
+ })}
+
+ }
+ buttonText={t("siteRestartButton")}
+ onConfirm={() => restartSite(restartingSite.id)}
+ string={restartingSite.name}
+ warningText={t("siteRestartWarning")}
+ title={t("siteRestartTitle")}
+ />
+ )}
+
{selectedSite && (