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")} -

-
-
- - - -
- - )} ); } 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 && (