added LiveDAG
This commit is contained in:
parent
654221d956
commit
b9826e3975
|
|
@ -1,84 +1,84 @@
|
||||||
// cockpit/fester-module/targets.js
|
// cockpit/fester-module/targets.js
|
||||||
|
|
||||||
/**
|
// State for available targets
|
||||||
* Fester Targets Module
|
let targets = [];
|
||||||
* Handles nodes, edges, metrics, heatmaps, and live updates for cockpit UI.
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function(global) {
|
// Fetch current targets from backend
|
||||||
|
async function fetchTargets() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/targets");
|
||||||
|
targets = await res.json();
|
||||||
|
renderTargets();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch targets:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const targetsModule = {
|
// Render target list in the sidebar
|
||||||
nodes: {},
|
function renderTargets() {
|
||||||
edges: [],
|
let sidebar = document.getElementById("sidebar");
|
||||||
metrics: {},
|
let panel = document.getElementById("targets-panel");
|
||||||
heatmaps: {},
|
|
||||||
callbacks: [],
|
if(!panel) {
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = "targets-panel";
|
||||||
|
panel.className = "panel";
|
||||||
|
sidebar.appendChild(panel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
panel.innerHTML = "<h2>Targets</h2>";
|
||||||
* Register a callback to receive updated DAG data
|
|
||||||
*/
|
|
||||||
onUpdate(callback) {
|
|
||||||
if (typeof callback === 'function') {
|
|
||||||
this.callbacks.push(callback);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
targets.forEach(target => {
|
||||||
* Update the internal state from WS or iframe message
|
const container = document.createElement("div");
|
||||||
*/
|
container.style.marginBottom = "5px";
|
||||||
update(data) {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
if (data.nodes) {
|
const checkbox = document.createElement("input");
|
||||||
data.nodes.forEach(node => this.nodes[node.id] = node);
|
checkbox.type = "checkbox";
|
||||||
}
|
checkbox.id = `target-${target.name}`;
|
||||||
if (data.edges) {
|
checkbox.checked = target.enabled;
|
||||||
this.edges = data.edges;
|
checkbox.addEventListener("change", () => toggleTarget(target.name, checkbox.checked));
|
||||||
}
|
|
||||||
if (data.metrics) {
|
|
||||||
Object.assign(this.metrics, data.metrics);
|
|
||||||
}
|
|
||||||
if (data.heatmaps) {
|
|
||||||
Object.assign(this.heatmaps, data.heatmaps);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatch();
|
const label = document.createElement("label");
|
||||||
},
|
label.htmlFor = checkbox.id;
|
||||||
|
label.innerText = target.name;
|
||||||
|
|
||||||
/**
|
container.appendChild(checkbox);
|
||||||
* Notify all registered callbacks with the updated data
|
container.appendChild(label);
|
||||||
*/
|
panel.appendChild(container);
|
||||||
dispatch() {
|
|
||||||
const snapshot = {
|
|
||||||
nodes: Object.values(this.nodes),
|
|
||||||
edges: this.edges,
|
|
||||||
metrics: this.metrics,
|
|
||||||
heatmaps: this.heatmaps
|
|
||||||
};
|
|
||||||
this.callbacks.forEach(cb => cb(snapshot));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all stored DAG state
|
|
||||||
*/
|
|
||||||
clear() {
|
|
||||||
this.nodes = {};
|
|
||||||
this.edges = [];
|
|
||||||
this.metrics = {};
|
|
||||||
this.heatmaps = {};
|
|
||||||
this.dispatch();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for iframe messages from cockpit.html
|
|
||||||
window.addEventListener('message', (event) => {
|
|
||||||
const data = event.data;
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
targetsModule.update(data);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Expose module globally
|
// Enable/disable target via backend
|
||||||
global.FesterTargets = targetsModule;
|
async function toggleTarget(targetName, enabled) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/targets/${targetName}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ enabled })
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Failed to update target", targetName);
|
||||||
|
} else {
|
||||||
|
console.log(`Target ${targetName} set to ${enabled}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error toggling target:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
})(window);
|
// WebSocket for live target state updates
|
||||||
|
const wsTargets = new WebSocket("ws://localhost:8080/ws-targets");
|
||||||
|
|
||||||
|
wsTargets.onmessage = (msg) => {
|
||||||
|
const event = JSON.parse(msg.data);
|
||||||
|
if (event.type === "target-update") {
|
||||||
|
const t = targets.find(t => t.name === event.data.name);
|
||||||
|
if (t) {
|
||||||
|
t.enabled = event.data.enabled;
|
||||||
|
renderTargets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
fetchTargets();
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,113 @@
|
||||||
|
<!-- cockpit/fester-module/ui.html -->
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Fester Cockpit</title>
|
<title>Fester Cockpit</title>
|
||||||
<link rel="stylesheet" href="../../style.css">
|
<link rel="stylesheet" href="../style.css">
|
||||||
<style>
|
<script src="targets.js"></script>
|
||||||
body { margin: 0; font-family: Arial, sans-serif; background: #1e1e1e; color: #eee; }
|
<script src="../fester.js"></script>
|
||||||
header { background: #2c2c2c; padding: 10px; font-size: 1.2em; display: flex; align-items: center; }
|
<style>
|
||||||
header h1 { flex: 1; margin: 0; font-weight: normal; }
|
body { font-family: sans-serif; margin:0; display:flex; height:100vh; }
|
||||||
#tabs { display: flex; background: #252525; }
|
#sidebar { width:250px; background:#222; color:#eee; padding:10px; overflow-y:auto; }
|
||||||
.tab { flex: 1; text-align: center; padding: 10px; cursor: pointer; }
|
#main { flex:1; padding:10px; overflow:auto; }
|
||||||
.tab.active { background: #3a3a3a; }
|
.panel { margin-bottom:20px; }
|
||||||
iframe { border: none; width: 100%; height: calc(100vh - 80px); }
|
.tab { cursor:pointer; padding:5px 10px; display:inline-block; color:#eee; background:#444; margin-right:2px; }
|
||||||
</style>
|
.tab.active { background:#666; }
|
||||||
|
#debugger-content { display:none; }
|
||||||
|
.debugger-controls button { margin-right:5px; }
|
||||||
|
.timeline { border:1px solid #ccc; height:200px; overflow-x:auto; white-space:nowrap; padding:5px; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="sidebar">
|
||||||
<header>
|
<div class="panel">
|
||||||
<h1>Fester Cockpit</h1>
|
<div class="tab active" onclick="switchTab('targets')">Targets</div>
|
||||||
</header>
|
<div class="tab" onclick="switchTab('debugger')">Debugger</div>
|
||||||
|
</div>
|
||||||
<div id="tabs">
|
<!-- Targets panel inserted by targets.js -->
|
||||||
<div class="tab active" data-target="live_dag">Live DAG</div>
|
|
||||||
<div class="tab" data-target="replay">Replay / Debugger</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<iframe id="cockpit-frame" src="../../ui/live_dag.html"></iframe>
|
<div id="main">
|
||||||
|
<div id="targets-content"></div>
|
||||||
|
<div id="debugger-content">
|
||||||
|
<h2>Debugger</h2>
|
||||||
|
<div class="debugger-controls">
|
||||||
|
<button onclick="debugStepBack()">⏮ Step Back</button>
|
||||||
|
<button onclick="debugStepForward()">⏭ Step Forward</button>
|
||||||
|
<button onclick="debugPauseResume()">⏸/▶ Pause</button>
|
||||||
|
</div>
|
||||||
|
<div class="timeline" id="debugger-timeline"></div>
|
||||||
|
<pre id="debugger-state" style="border:1px solid #ccc; padding:5px; margin-top:10px;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="./targets.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// Tab switching logic
|
// Tab switching
|
||||||
const tabs = document.querySelectorAll('.tab');
|
function switchTab(tab) {
|
||||||
const iframe = document.getElementById('cockpit-frame');
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
tabs.forEach(tab => {
|
document.querySelectorAll('#targets-content, #debugger-content').forEach(c => c.style.display = 'none');
|
||||||
tab.addEventListener('click', () => {
|
|
||||||
tabs.forEach(t => t.classList.remove('active'));
|
if(tab === 'targets') {
|
||||||
tab.classList.add('active');
|
document.querySelector('.tab[onclick*="targets"]').classList.add('active');
|
||||||
iframe.src = tab.dataset.target === 'live_dag' ? '../../ui/live_dag.html' : '../../ui/replay.html';
|
document.getElementById('targets-content').style.display = 'block';
|
||||||
});
|
} else if(tab === 'debugger') {
|
||||||
|
document.querySelector('.tab[onclick*="debugger"]').classList.add('active');
|
||||||
|
document.getElementById('debugger-content').style.display = 'block';
|
||||||
|
fetchDebuggerState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugger WebSocket
|
||||||
|
let wsDebugger = new WebSocket("ws://localhost:8080/ws-debugger");
|
||||||
|
let paused = false;
|
||||||
|
|
||||||
|
wsDebugger.onmessage = function(msg) {
|
||||||
|
const event = JSON.parse(msg.data);
|
||||||
|
if(event.type === 'timeline-update') {
|
||||||
|
renderTimeline(event.data);
|
||||||
|
updateDebuggerState(event.data.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderTimeline(events) {
|
||||||
|
const timeline = document.getElementById('debugger-timeline');
|
||||||
|
timeline.innerHTML = '';
|
||||||
|
events.forEach((ev,i) => {
|
||||||
|
const div = document.createElement('span');
|
||||||
|
div.style.display = 'inline-block';
|
||||||
|
div.style.width = '20px';
|
||||||
|
div.style.height = '20px';
|
||||||
|
div.style.marginRight = '2px';
|
||||||
|
div.style.background = ev.completed ? '#0f0' : '#555';
|
||||||
|
div.title = `${i}: ${ev.name}`;
|
||||||
|
timeline.appendChild(div);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize WebSocket connection and dispatch events to iframe
|
function updateDebuggerState(currentEvent) {
|
||||||
const ws = new WebSocket(`ws://${window.location.hostname}:8080/ws`);
|
const stateBox = document.getElementById('debugger-state');
|
||||||
ws.onopen = () => console.log("Cockpit WS connected");
|
stateBox.textContent = JSON.stringify(currentEvent, null, 2);
|
||||||
ws.onmessage = (event) => {
|
}
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
// Debugger controls
|
||||||
// Forward event to iframe
|
function debugStepBack() {
|
||||||
iframe.contentWindow.postMessage(data, '*');
|
wsDebugger.send(JSON.stringify({ action: 'step_back' }));
|
||||||
} catch (err) {
|
}
|
||||||
console.error("Invalid WS event", err);
|
function debugStepForward() {
|
||||||
}
|
wsDebugger.send(JSON.stringify({ action: 'step_forward' }));
|
||||||
};
|
}
|
||||||
|
function debugPauseResume() {
|
||||||
|
paused = !paused;
|
||||||
|
wsDebugger.send(JSON.stringify({ action: paused ? 'pause' : 'resume' }));
|
||||||
|
console.log("Debugger " + (paused ? "paused" : "resumed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
function fetchDebuggerState() {
|
||||||
|
document.getElementById('debugger-content').style.display = 'block';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue