fix: reset service editor on switch + add service-page settings shortcut

Settings.tsx: ServiceConfigEditor derived editable state (name, config,
secrets) from the instance prop via useState, but the parent rendered it
without a key. Switching services in the rail reused the same component, so
name/config stayed pinned to the previously selected service while
instance.id/service_type (read live from props) pointed at the new one —
saving then wrote the stale values onto the wrong row (e.g. saving qBittorrent
renamed it "Jellyfin" with Jellyfin's URL). Add key={selectedService.id} so
the editor remounts and resets on switch.

ServicePage: add a Settings shortcut in the header that deep-links to
/settings?tab=services&service=<id>. Settings now reads tab + service query
params (useSearchParams) to open the Services tab with that service
pre-selected, via a new initialServiceId prop on ServicesAdminCard.

Tests: new Settings.services.test.tsx regression test (fails without the key,
passes with it); wrap existing Settings tests in MemoryRouter since Settings
now uses useSearchParams. 166/166 frontend tests pass; typecheck + ESLint clean.
This commit is contained in:
Developer
2026-07-11 11:54:18 +00:00
parent 84dcf9e010
commit dad2202756
8 changed files with 214 additions and 27 deletions
+20 -4
View File
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import type {
MonitoringMachine,
MonitoringMachineInput,
@@ -893,7 +894,11 @@ export function Settings() {
const saveMachine = useSaveMonitoringMachine();
const deleteMachine = useDeleteMonitoringMachine();
const testMachineSSH = useTestMonitoringMachineSSH();
const [tab, setTab] = useState<SettingsTab>("machines");
const [searchParams] = useSearchParams();
const [tab, setTab] = useState<SettingsTab>(
(searchParams.get("tab") as SettingsTab | null) ?? "machines",
);
const initialServiceId = searchParams.get("service") ?? "";
const [deleteMachineId, setDeleteMachineId] = useState<string | null>(null);
const [selectedSSHKeyId, setSelectedSSHKeyId] = useState("");
const [sshValidationMessage, setSSHValidationMessage] = useState("");
@@ -1195,7 +1200,9 @@ export function Settings() {
onSelectKeyId={setSelectedSSHKeyId}
/>
)}
{tab === "services" && <ServicesAdminCard />}
{tab === "services" && (
<ServicesAdminCard initialServiceId={initialServiceId} />
)}
{tab === "danger" && <ResetLocalDatabaseCard />}
</TabbedCard>
{isMobile ? (
@@ -1338,10 +1345,14 @@ export function Settings() {
* old ServicePage ConfigBody — the service page is now a pure operational
* view; all administration lives here.
*/
function ServicesAdminCard() {
function ServicesAdminCard({
initialServiceId = "",
}: {
initialServiceId?: string;
}) {
const { data: services = [] } = useServiceInstances();
const { data: types = [] } = useServiceTypes();
const [selectedServiceId, setSelectedServiceId] = useState("");
const [selectedServiceId, setSelectedServiceId] = useState(initialServiceId);
const sortedServices = useMemo(
() =>
@@ -1416,6 +1427,11 @@ function ServicesAdminCard() {
>
{selectedService ? (
<ServiceConfigEditor
// Remount on service switch so useState initializers (name, config,
// secrets) re-run for the new instance. Without this key, switching
// services in the rail keeps the previous service's editable state
// and a save writes stale name/config onto the new service's row.
key={selectedService.id}
instance={selectedService}
typeInfo={selectedTypeInfo}
/>