feat: plugins only see their own host settings in the browser

This commit is contained in:
LukeGus committed 2026-09-29 13:38:11 -05:00
1 parent 50d43e29fb
commit fb9c168b6b
17 files changed
+488 -52

No files matched your search

@@ -42,13 +42,12 @@ export function useConsoleLook(
appTheme: string,
): ConsoleLook {
const [look, setLook] = useState<ConsoleLook>(FALLBACK_CONSOLE_LOOK);
const hostKey = host
? `${host.id}:${JSON.stringify(host.pluginSettings ?? {})}`
: "";
// The terminal's settings live with the terminal, so it resolves them from
// the host id.
const hostId = host?.id;
useEffect(() => {
let active = true;
invokeAction("terminal.resolveTheme", { host, appTheme })
invokeAction("terminal.resolveTheme", { hostId, appTheme })
.then((resolved) => {
if (active) setLook((resolved as ConsoleLook) ?? FALLBACK_CONSOLE_LOOK);
})
@@ -58,8 +57,7 @@ export function useConsoleLook(
return () => {
active = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hostKey, appTheme]);
}, [hostId, appTheme]);
return look;
}
@@ -48,6 +48,10 @@ function app(): TermixApp {
return current;
}
export function listHosts(): ReturnType<TermixApp["listHosts"]> {
return app().listHosts();
}
export function fileManagerApi(): FileManagerClient {
return app().api as unknown as FileManagerClient;
}
@@ -1,4 +1,4 @@
import { listHosts } from "@termix/plugin-sdk/frontend";
import { listHosts } from "./client";
import type { SSHHost } from "../host-types";
export async function getSSHHosts(): Promise<SSHHost[]> {
@@ -11,8 +11,12 @@ import {
DialogFooter,
} from "@termix/plugin-sdk/ui";
import { Select2 } from "@termix/plugin-sdk/ui";
import { discoverProxmoxGuestsStream, importProxmoxHosts } from "./proxmox-api";
import { listHosts, type PluginHostRecord } from "@termix/plugin-sdk/frontend";
import {
discoverProxmoxGuestsStream,
importProxmoxHosts,
listHosts,
} from "./proxmox-api";
import type { PluginHostRecord } from "@termix/plugin-sdk/frontend";
import type { ProxmoxGuest } from "./types";
import { resolveProxmoxImportAuth } from "./proxmox-import-auth";
@@ -13,6 +13,10 @@ function app(): TermixApp {
return current;
}
export function listHosts(): ReturnType<TermixApp["listHosts"]> {
return app().listHosts();
}
/**
* Runs a guest discovery over the server-sent event stream and reports
* progress. Returns a function that stops listening.
+30 -7
View File
@@ -1,4 +1,4 @@
import type { ComponentType, Ref } from "react";
import { useEffect, useState, type ComponentType, type Ref } from "react";
import {
Info,
MessagesSquare,
@@ -7,6 +7,7 @@ import {
MousePointerClick,
} from "lucide-react";
import {
invokeAction,
useTranslation,
type HostEditorSectionProps,
type PluginHostRecord,
@@ -175,21 +176,42 @@ function ToolbarCard({
);
}
/** The Wake-on-LAN address a host keeps, the default for a session's wake packet. */
function wakeOnLanMac(host: PluginHostRecord | undefined): string | null {
const value = host?.pluginSettings?.["wake-on-lan"]?.macAddress;
return typeof value === "string" && value ? value : null;
/**
* The Wake-on-LAN address a host keeps, the default for a session's wake
* packet. Null while that plugin is off.
*/
function useWakeOnLanMac(hostId: string | undefined): string | null {
const [mac, setMac] = useState<string | null>(null);
useEffect(() => {
let active = true;
if (!hostId) {
setMac(null);
return;
}
invokeAction("wakeOnLan.macAddress", hostId)
.then((value) => {
if (active) setMac(typeof value === "string" && value ? value : null);
})
.catch(() => {
if (active) setMac(null);
});
return () => {
active = false;
};
}, [hostId]);
return mac;
}
function RdpSection(props: HostEditorSectionProps) {
const { form, setField, setGuacField } = remoteDesktopForm(props);
const macAddress = useWakeOnLanMac(props.host?.id);
return (
<>
<HostEditorRdpTab
form={form}
setField={setField}
setGuacField={setGuacField}
host={{ macAddress: wakeOnLanMac(props.host) }}
host={{ macAddress }}
credentials={props.credentials as never}
/>
<ToolbarCard form={form} setField={setField} />
@@ -200,13 +222,14 @@ function RdpSection(props: HostEditorSectionProps) {
function VncSection(props: HostEditorSectionProps) {
const { form, setField, setGuacField } = remoteDesktopForm(props);
const macAddress = useWakeOnLanMac(props.host?.id);
return (
<>
<HostEditorVncTab
form={form}
setField={setField}
setGuacField={setGuacField}
host={{ macAddress: wakeOnLanMac(props.host) }}
host={{ macAddress }}
credentials={props.credentials as never}
/>
<ToolbarCard form={form} setField={setField} />
+5 -4
View File
@@ -7,7 +7,6 @@ import {
SquareTerminal,
Terminal,
} from "lucide-react";
import { listHosts } from "@termix/plugin-sdk/frontend";
import type {
PanelProps,
PluginHostRecord,
@@ -309,9 +308,11 @@ export function activate(app: TermixApp): void {
);
let host: PluginHostRecord | null | undefined = request.host;
if (!host && request.hostId !== undefined) {
host = (await listHosts().catch(() => [])).find(
(entry) => String(entry.id) === String(request.hostId),
);
host =
app.getHost(request.hostId) ??
(await app.listHosts().catch(() => [])).find(
(entry) => String(entry.id) === String(request.hostId),
);
}
return resolveTerminalLook({ ...request, host }, settings?.user);
}) as never);
@@ -5,11 +5,19 @@ import type {
TermixApp,
} from "@termix/plugin-sdk/frontend";
export function macAddressOf(
host: Pick<PluginHostRecord, "pluginSettings"> | null | undefined,
): string | null {
const macAddress = host?.pluginSettings?.["wake-on-lan"]?.macAddress;
return typeof macAddress === "string" && macAddress.trim()
? macAddress.trim()
: null;
}
export function hasMacAddress(
host: Pick<PluginHostRecord, "pluginSettings">,
): boolean {
const macAddress = host.pluginSettings?.["wake-on-lan"]?.macAddress;
return typeof macAddress === "string" && macAddress.trim().length > 0;
return macAddressOf(host) !== null;
}
function errorMessage(error: unknown, fallback: string): string {
+5 -1
View File
@@ -1,6 +1,6 @@
import { Zap } from "lucide-react";
import type { TermixApp } from "@termix/plugin-sdk/frontend";
import { hasMacAddress, wakeHost } from "./host-action.js";
import { hasMacAddress, macAddressOf, wakeHost } from "./host-action.js";
export function activate(app: TermixApp): void {
app.registerHostAction({
@@ -12,4 +12,8 @@ export function activate(app: TermixApp): void {
when: (host) => hasMacAddress(host),
run: (host) => wakeHost(app.api, app.t, String(host.id)),
});
// A saved host's MAC address, for plugins that send their own wake packet.
app.registerAction("wakeOnLan.macAddress", ((hostId: string | number) =>
macAddressOf(app.getHost(hostId))) as never);
}
@@ -1,7 +1,11 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { toast } from "sonner";
import type { PluginApiClient } from "@termix/plugin-sdk/frontend";
import { hasMacAddress, wakeHost } from "../../src/frontend/host-action.js";
import {
hasMacAddress,
macAddressOf,
wakeHost,
} from "../../src/frontend/host-action.js";
vi.mock("sonner", () => ({
toast: { success: vi.fn(), error: vi.fn() },
@@ -24,6 +28,26 @@ function stubApi(overrides: Partial<PluginApiClient> = {}): PluginApiClient {
const t = (key: string) => key;
describe("macAddressOf", () => {
it("returns the trimmed MAC address a host keeps", () => {
expect(
macAddressOf({
pluginSettings: {
"wake-on-lan": { macAddress: " aa:bb:cc:dd:ee:ff " },
},
}),
).toBe("aa:bb:cc:dd:ee:ff");
});
it("is null without a host or an address", () => {
expect(macAddressOf(undefined)).toBeNull();
expect(macAddressOf({ pluginSettings: {} })).toBeNull();
expect(
macAddressOf({ pluginSettings: { "wake-on-lan": { macAddress: " " } } }),
).toBeNull();
});
});
describe("hasMacAddress", () => {
it("is true when the host's wake-on-lan settings carry a MAC address", () => {
expect(