推进 INI 面板 control page controller helper

This commit is contained in:
2026-06-14 23:08:19 +08:00
parent 2844fd25c3
commit 59ffacb64e
7 changed files with 272 additions and 45 deletions

View File

@@ -29,6 +29,9 @@ renders the current view into a supplied DOM container.
For a ready-made read-only page, open
`runtime/ui/ini-panel/control-page.html`. It embeds the panel in a hidden iframe
and mirrors the same control view into the visible page.
The page uses `createMachineSessionControlPageController(...)` from
`runtime/ui/ini-panel/control-page-controller.js` so other read-only shells can
reuse the same iframe/API polling contract.
## `createIniPanelStateSummary(input)`

View File

@@ -0,0 +1,80 @@
export function createMachineSessionControlPageController({
panelFrame,
statusNode,
viewNode,
renderControlView,
pollMs = 200,
setIntervalFn = globalThis.setInterval,
} = {}) {
if (!panelFrame) {
throw new Error("A panel iframe is required.");
}
if (!statusNode) {
throw new Error("A control page status node is required.");
}
if (!viewNode) {
throw new Error("A control page view node is required.");
}
if (typeof renderControlView !== "function") {
throw new Error("A control view renderer is required.");
}
let refreshTimer = null;
function getPanelWindow() {
return panelFrame.contentWindow ?? null;
}
function getPanelApi() {
return getPanelWindow()?.linuxCncIniPanelApi ?? null;
}
function getControlView() {
return getPanelApi()?.getMachineSessionControlView?.() ?? null;
}
function render() {
const view = getControlView();
if (!view) {
statusNode.textContent = "waiting for panel";
viewNode.textContent = "";
return null;
}
statusNode.textContent = [
view.status.lastAction ?? "-",
view.status.runStatus ?? "-",
view.status.error ?? "-",
].join(" | ");
return renderControlView(viewNode, view);
}
function startPolling() {
if (!refreshTimer) {
refreshTimer = setIntervalFn(render, pollMs);
}
return refreshTimer;
}
function mount({ panelSrc = "./index.html" } = {}) {
panelFrame.addEventListener("load", () => {
render();
startPolling();
});
panelFrame.src = panelSrc;
render();
startPolling();
return controller;
}
const controller = {
getControlView,
getPanelApi,
getPanelWindow,
mount,
render,
startPolling,
};
return controller;
}

View File

@@ -1,58 +1,25 @@
import {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
import {
createMachineSessionControlPageController,
} from "./control-page-controller.js";
const panelFrameNode = document.getElementById("panel-frame");
const statusNode = document.getElementById("control-page-status");
const viewNode = document.getElementById("control-page-view");
let refreshTimer = null;
function getPanelWindow() {
return panelFrameNode?.contentWindow ?? null;
}
function getPanelApi() {
return getPanelWindow()?.linuxCncIniPanelApi ?? null;
}
function getControlView() {
return getPanelApi()?.getMachineSessionControlView?.() ?? null;
}
function renderControlPage() {
const view = getControlView();
if (!view) {
statusNode.textContent = "waiting for panel";
viewNode.textContent = "";
return null;
}
statusNode.textContent = [
view.status.lastAction ?? "-",
view.status.runStatus ?? "-",
view.status.error ?? "-",
].join(" | ");
return renderMachineSessionControlView(viewNode, view);
}
function startPolling() {
if (!refreshTimer) {
refreshTimer = setInterval(renderControlPage, 200);
}
}
panelFrameNode.addEventListener("load", () => {
renderControlPage();
startPolling();
const controller = createMachineSessionControlPageController({
panelFrame: panelFrameNode,
statusNode,
viewNode,
renderControlView: renderMachineSessionControlView,
});
panelFrameNode.src = "./index.html";
renderControlPage();
startPolling();
controller.mount();
window.linuxCncIniPanelControlPageApi = {
getControlView,
getPanelApi,
renderControlPage,
getControlView: controller.getControlView,
getPanelApi: controller.getPanelApi,
renderControlPage: controller.render,
};

View File

@@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import {
createMachineSessionControlPageController,
} from "../../../runtime/ui/ini-panel/control-page-controller.js";
function createPanelFrame(panelApi = null) {
const listeners = new Map();
return {
contentWindow: panelApi ? { linuxCncIniPanelApi: panelApi } : null,
src: null,
addEventListener(type, listener) {
listeners.set(type, listener);
},
dispatch(type) {
listeners.get(type)?.();
},
};
}
const rendered = [];
const viewNode = { textContent: "" };
const statusNode = { textContent: "" };
const expectedView = {
status: {
lastAction: "run-gcode",
runStatus: "ok",
error: null,
},
sections: [
{
id: "run",
rows: [
{ label: "Canonical events", value: 2 },
],
},
],
};
let intervalCallback = null;
const panelFrame = createPanelFrame();
const controller = createMachineSessionControlPageController({
panelFrame,
statusNode,
viewNode,
pollMs: 250,
setIntervalFn: (callback, ms) => {
intervalCallback = callback;
return `timer:${ms}`;
},
renderControlView: (container, view) => {
rendered.push({ container, view });
return { statusNode: { textContent: "rendered" } };
},
});
assert.equal(controller.getPanelWindow(), null);
assert.equal(controller.getPanelApi(), null);
assert.equal(controller.getControlView(), null);
assert.equal(controller.render(), null);
assert.equal(statusNode.textContent, "waiting for panel");
assert.equal(viewNode.textContent, "");
assert.equal(controller.startPolling(), "timer:250");
assert.equal(controller.startPolling(), "timer:250");
assert.equal(typeof intervalCallback, "function");
const panelApi = {
getMachineSessionControlView: () => expectedView,
};
panelFrame.contentWindow = { linuxCncIniPanelApi: panelApi };
assert.equal(controller.getPanelApi(), panelApi);
assert.equal(controller.getControlView(), expectedView);
const renderResult = controller.render();
assert.deepEqual(renderResult, { statusNode: { textContent: "rendered" } });
assert.equal(statusNode.textContent, "run-gcode | ok | -");
assert.deepEqual(rendered, [{ container: viewNode, view: expectedView }]);
const mountedFrame = createPanelFrame(panelApi);
const mountedStatusNode = { textContent: "" };
const mountedViewNode = { textContent: "" };
let mountedIntervalMs = null;
const mountedController = createMachineSessionControlPageController({
panelFrame: mountedFrame,
statusNode: mountedStatusNode,
viewNode: mountedViewNode,
setIntervalFn: (_callback, ms) => {
mountedIntervalMs = ms;
return "mounted-timer";
},
renderControlView: () => "mounted-render",
});
assert.equal(mountedController.mount({ panelSrc: "./custom-panel.html" }), mountedController);
assert.equal(mountedFrame.src, "./custom-panel.html");
assert.equal(mountedIntervalMs, 200);
mountedFrame.dispatch("load");
assert.equal(mountedStatusNode.textContent, "run-gcode | ok | -");
assert.throws(
() => createMachineSessionControlPageController(),
/panel iframe/,
);
console.log("ini_panel_control_page_controller_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_controller.mjs"

View File

@@ -9,5 +9,6 @@ ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_session_workflow.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_machine_file_summary.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_state_summary.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_controller.sh"
echo "ui_node_smokes=ok"