diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index 052796d..0770f21 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -455,9 +455,9 @@ export class PiWebApp extends LitElement { setNamespacedQueryKey(TERMINAL_ROUTE_NAMESPACE, "terminal", surface.selectedTerminalId, { replace: true }); } - private async selectMachineWithMemory(machine: Machine): Promise { + private async selectMachineWithMemory(machine: Machine, options: { rememberCurrent?: boolean } = {}): Promise { if (this.state.selectedMachine?.id === machine.id) return; - if (!this.routeRestoreInProgress) this.rememberCurrentMachineNavigation(); + if (options.rememberCurrent !== false && !this.routeRestoreInProgress) this.rememberCurrentMachineNavigation(); const seq = ++this.machineNavigationRestoreSeq; const snapshot = this.machineNavigation.latest(machine.id) ?? emptyMachineNavigationSnapshot(machine.id); await this.restoreRouteFor(routeFromMachineNavigationSnapshot(snapshot), false, snapshot.surface, snapshot.view); @@ -1044,7 +1044,11 @@ export class PiWebApp extends LitElement { private async removeMachine(machine: Machine | undefined = this.state.selectedMachine): Promise { if (machine === undefined || machine.kind === "local") return; if (!window.confirm(`Remove ${machine.name}?\n\nThis only removes it from this PI WEB gateway.`)) return; - await this.machines.deleteMachine(machine); + const wasSelected = this.state.selectedMachine?.id === machine.id; + if (wasSelected) this.rememberCurrentMachineNavigation(); + const fallback = await this.machines.deleteMachine(machine, { selectFallback: !wasSelected }); + if (!this.state.machines.some((candidate) => candidate.id === machine.id)) this.machineNavigation.forget(machine.id); + if (wasSelected && fallback !== undefined) await this.selectMachineWithMemory(fallback, { rememberCurrent: false }); } private openSelectedMachine(): void { diff --git a/src/client/src/controllers/machineController.test.ts b/src/client/src/controllers/machineController.test.ts index e6a272f..f062a70 100644 --- a/src/client/src/controllers/machineController.test.ts +++ b/src/client/src/controllers/machineController.test.ts @@ -72,4 +72,42 @@ describe("MachineController", () => { expect(state.machineStatuses[remoteMachine.id]).toMatchObject({ machineId: remoteMachine.id, ok: false, status: "offline", error: "Internal Server Error" }); expect(state.error).toContain("Remote is offline"); }); + + it("returns the fallback machine without selecting it when requested", async () => { + let state: AppState = { ...initialAppState(), machines: [localMachine, remoteMachine], selectedMachine: remoteMachine }; + const setState = (patch: Partial) => { state = { ...state, ...patch }; }; + const updateUrl = vi.fn(); + const projects = { loadProjects: vi.fn() }; + + vi.spyOn(api, "deleteMachine").mockResolvedValue({ deleted: true }); + + const controller = new MachineController(() => state, setState, updateUrl, projects); + + const fallback = await controller.deleteMachine(remoteMachine, { selectFallback: false }); + + expect(fallback).toEqual(localMachine); + expect(state.machines).toEqual([localMachine]); + expect(state.selectedMachine).toEqual(remoteMachine); + expect(projects.loadProjects).not.toHaveBeenCalled(); + expect(updateUrl).not.toHaveBeenCalled(); + }); + + it("selects the fallback machine after deleting the selected machine by default", async () => { + let state: AppState = { ...initialAppState(), machines: [localMachine, remoteMachine], selectedMachine: remoteMachine, selectedProject: { id: "p1", name: "Project", path: "/repo", createdAt: "now" } }; + const setState = (patch: Partial) => { state = { ...state, ...patch }; }; + const updateUrl = vi.fn(); + const projects = { loadProjects: vi.fn() }; + + vi.spyOn(api, "deleteMachine").mockResolvedValue({ deleted: true }); + + const controller = new MachineController(() => state, setState, updateUrl, projects); + + const fallback = await controller.deleteMachine(remoteMachine); + + expect(fallback).toEqual(localMachine); + expect(state.selectedMachine).toEqual(localMachine); + expect(state.selectedProject).toBeUndefined(); + expect(projects.loadProjects).toHaveBeenCalledOnce(); + expect(updateUrl).toHaveBeenCalledOnce(); + }); }); diff --git a/src/client/src/controllers/machineController.ts b/src/client/src/controllers/machineController.ts index 3324822..683e4af 100644 --- a/src/client/src/controllers/machineController.ts +++ b/src/client/src/controllers/machineController.ts @@ -59,20 +59,27 @@ export class MachineController { } } - async deleteMachine(machine: Machine | undefined = this.getState().selectedMachine): Promise { - if (machine === undefined) return; + async deleteMachine(machine: Machine | undefined = this.getState().selectedMachine, options: { selectFallback?: boolean } = {}): Promise { + if (machine === undefined) return undefined; if (machine.kind === "local") { this.setState({ error: "The local machine cannot be removed." }); - return; + return undefined; } try { + const wasSelected = this.getState().selectedMachine?.id === machine.id; await api.deleteMachine(machine.id); const machines = this.getState().machines.filter((candidate) => candidate.id !== machine.id); const local = machines.find((candidate) => candidate.id === "local") ?? machines[0]; this.setState({ machines, machineStatuses: omitKey(this.getState().machineStatuses, machine.id) }); - if (this.getState().selectedMachine?.id === machine.id && local !== undefined) await this.selectMachine(local); + if (wasSelected && local !== undefined) { + if (options.selectFallback === false) return local; + await this.selectMachine(local); + return local; + } + return undefined; } catch (error) { this.setState({ error: String(error) }); + return undefined; } }