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 -10
View File
@@ -1,17 +1,16 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { Settings } from "../Settings";
import type { MonitoringMachine } from "../../types";
const saveMachineMutate = vi.fn().mockResolvedValue({});
const deleteMachineMutate = vi.fn();
const testSSHMutate = vi
.fn()
.mockResolvedValue({
message: "SSH auth succeeded",
known_hosts_updated: true,
});
const testSSHMutate = vi.fn().mockResolvedValue({
message: "SSH auth succeeded",
known_hosts_updated: true,
});
let machines: MonitoringMachine[] = [];
@@ -66,14 +65,21 @@ beforeEach(() => {
describe("Settings", () => {
it("renders the machine list from the mocked store", () => {
machines = [localMachine()];
render(<Settings />);
// The rail row caption (mode · enabled) is unique to the selection rail.
render(
<MemoryRouter>
<Settings />
</MemoryRouter>,
);
expect(screen.getByText("local · Enabled")).toBeInTheDocument();
});
it("saves a machine via the editor dialog (controlled useState parity)", async () => {
machines = [localMachine()];
render(<Settings />);
render(
<MemoryRouter>
<Settings />
</MemoryRouter>,
);
// The detail-pane "Edit" has visible text "Edit"; the rail hover edit
// affordance is icon-only (aria-label "Edit") — disambiguate by text.
@@ -99,7 +105,11 @@ describe("Settings", () => {
it("deletes a machine through the confirm dialog", async () => {
machines = [localMachine()];
render(<Settings />);
render(
<MemoryRouter>
<Settings />
</MemoryRouter>,
);
// Detail-pane "Delete" opens the confirm dialog.
await userEvent.click(screen.getByRole("button", { name: "Delete" }));