mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-13 17:13:41 +08:00
feat: Add connection
This commit is contained in:
@@ -6,20 +6,16 @@
|
|||||||
<title>Task Status Dashboard</title>
|
<title>Task Status Dashboard</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<style>
|
<style>
|
||||||
/* Custom scrollbar styles */
|
|
||||||
.scrollbar {
|
.scrollbar {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #D1D5DB #E5E7EB;
|
scrollbar-color: #D1D5DB #E5E7EB;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar::-webkit-scrollbar {
|
.scrollbar::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar::-webkit-scrollbar-track {
|
.scrollbar::-webkit-scrollbar-track {
|
||||||
background: #E5E7EB;
|
background: #E5E7EB;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar::-webkit-scrollbar-thumb {
|
.scrollbar::-webkit-scrollbar-thumb {
|
||||||
background-color: #D1D5DB;
|
background-color: #D1D5DB;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
@@ -27,11 +23,9 @@
|
|||||||
svg path {
|
svg path {
|
||||||
transition: fill 0.3s ease;
|
transition: fill 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg path:hover {
|
svg path:hover {
|
||||||
opacity: 0.8; /* Slightly fade when hovered for a modern touch */
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -43,11 +37,9 @@
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover.visible {
|
.popover.visible {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dag-diagram {
|
#dag-diagram {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -120,179 +112,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/socket.js"></script>
|
<script src="/js/socket.js"></script>
|
||||||
<script>
|
<script src="/js/app.js"></script>
|
||||||
(function(SS) {
|
|
||||||
'use strict';
|
|
||||||
let uiContent
|
|
||||||
function loadSVG(url) {
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(svgContent => {
|
|
||||||
const container = document.getElementById('svg-container');
|
|
||||||
container.src = url;
|
|
||||||
uiContent = svgContent;
|
|
||||||
})
|
|
||||||
.catch(err => console.error('Failed to load SVG:', err));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = function() {
|
|
||||||
loadSVG('http://localhost:8083/ui');
|
|
||||||
};
|
|
||||||
document.getElementById('send-request').addEventListener('click', function() {
|
|
||||||
const input = document.getElementById('payload');
|
|
||||||
const payloadData = JSON.parse(input.value);
|
|
||||||
const data = { payload: payloadData };
|
|
||||||
|
|
||||||
fetch('http://localhost:8083/request', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => console.log('Success:', data))
|
|
||||||
.catch((error) => console.error('Error:', error));
|
|
||||||
});
|
|
||||||
|
|
||||||
const tasks = {};
|
|
||||||
|
|
||||||
function attachSVGNodeEvents() {
|
|
||||||
const svgNodes = document.querySelectorAll('g.node'); // Adjust selector as per your SVG structure
|
|
||||||
svgNodes.forEach(node => {
|
|
||||||
node.classList.add("cursor-pointer")
|
|
||||||
node.addEventListener('click', handleSVGNodeClick);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle the click on an SVG node and show the popover
|
|
||||||
function handleSVGNodeClick(event) {
|
|
||||||
const nodeId = event.currentTarget.id; // Get the node ID (e.g., 'node_store:data')
|
|
||||||
const nodeData = findNodeDataById(nodeId); // Fetch data related to the node (status, result, etc.)
|
|
||||||
if (nodeData) {
|
|
||||||
showSVGPopover(event, nodeData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to show the popover next to the clicked SVG node
|
|
||||||
function showSVGPopover(event, nodeData) {
|
|
||||||
const popover = document.getElementById('svg-popover');
|
|
||||||
document.getElementById('task-id').innerHTML = nodeData.task_id
|
|
||||||
popover.classList.add('visible');
|
|
||||||
popover.innerHTML = `
|
|
||||||
<div class="text-sm text-gray-600 space-y-1">
|
|
||||||
<div class="grid grid-cols-5 gap-x-4">
|
|
||||||
<strong class="text-gray-800 col-span-1">Node:</strong>
|
|
||||||
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.topic}</span>
|
|
||||||
|
|
||||||
<strong class="text-gray-800 col-span-1">Status:</strong>
|
|
||||||
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.status}</span>
|
|
||||||
|
|
||||||
<strong class="text-gray-800 col-span-1">Result:</strong>
|
|
||||||
<pre class="text-gray-700 col-span-4 p-2 border border-gray-300 rounded-md max-h-32 overflow-auto bg-gray-50">${JSON.stringify(nodeData.payload, null, 2)}
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<strong class="text-gray-800 col-span-1">Error:</strong>
|
|
||||||
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.error || 'N/A'}</span>
|
|
||||||
|
|
||||||
<strong class="text-gray-800 col-span-1">Created At:</strong>
|
|
||||||
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.created_at}</span>
|
|
||||||
|
|
||||||
<strong class="text-gray-800 col-span-1">Processed At:</strong>
|
|
||||||
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.processed_at}</span>
|
|
||||||
|
|
||||||
<strong class="text-gray-800 col-span-1">Latency:</strong>
|
|
||||||
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.latency}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to find node data (status, result, error) by the node's ID
|
|
||||||
function findNodeDataById(nodeId) {
|
|
||||||
for (const taskId in tasks) {
|
|
||||||
const task = tasks[taskId];
|
|
||||||
const node = task.nodes.find(n => `node_${n.topic}` === nodeId); // Ensure the ID format matches your SVG
|
|
||||||
if (node) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null; // Return null if no matching node is found
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOrUpdateTask(message, isFinal = false) {
|
|
||||||
const taskTableBody = document.getElementById('taskTableBody');
|
|
||||||
const taskId = message.task_id;
|
|
||||||
const rowId = `row-${taskId}`;
|
|
||||||
let existingRow = document.getElementById(rowId);
|
|
||||||
|
|
||||||
if (!existingRow) {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.id = rowId;
|
|
||||||
taskTableBody.insertBefore(row, taskTableBody.firstChild);
|
|
||||||
existingRow = row;
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks[taskId] = tasks[taskId] || { nodes: [], final: null };
|
|
||||||
if (isFinal) tasks[taskId].final = message;
|
|
||||||
else tasks[taskId].nodes.push(message);
|
|
||||||
|
|
||||||
const latestStatus = isFinal ? message.status : message.status;
|
|
||||||
const statusColor = latestStatus === 'success' ? 'bg-green-100 text-green-700' :
|
|
||||||
latestStatus === 'fail' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700';
|
|
||||||
|
|
||||||
existingRow.innerHTML = `
|
|
||||||
<td class="px-4 py-2 border border-gray-300">${taskId}</td>
|
|
||||||
<td class="px-4 py-2 border border-gray-300">${new Date(message.created_at).toLocaleString()}</td>
|
|
||||||
<td class="px-4 py-2 border border-gray-300">${new Date(message.processed_at).toLocaleString()}</td>
|
|
||||||
<td class="px-4 py-2 border border-gray-300">${message.latency}</td>
|
|
||||||
<td class="px-4 py-2 border border-gray-300 ${statusColor}">${latestStatus}</td>
|
|
||||||
<td class="px-4 py-2 border border-gray-300">
|
|
||||||
<button class="view-btn text-blue-600 hover:underline" data-task-id='${taskId}'>View</button>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
attachViewButtonEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachViewButtonEvent() {
|
|
||||||
const buttons = document.querySelectorAll('.view-btn');
|
|
||||||
buttons.forEach(button => {
|
|
||||||
button.removeEventListener('click', handleViewButtonClick);
|
|
||||||
button.addEventListener('click', handleViewButtonClick);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleViewButtonClick(event) {
|
|
||||||
document.getElementById("task-svg").innerHTML = uiContent
|
|
||||||
attachSVGNodeEvents();
|
|
||||||
const taskId = event.target.getAttribute('data-task-id');
|
|
||||||
const task = tasks[taskId];
|
|
||||||
updateSVGNodes(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSVGNodes(task) {
|
|
||||||
console.log(task)
|
|
||||||
task.nodes.forEach((node) => {
|
|
||||||
const svgNode = document.querySelector(`#node_${node.topic.replace(':', '\\:')}`);
|
|
||||||
console.log(svgNode)
|
|
||||||
if (svgNode) {
|
|
||||||
const fillColor = node.status === 'success' ? '#A5D6A7' : node.status === 'fail' ? '#EF9A9A' : '#FFE082';
|
|
||||||
const path = svgNode.querySelector('path');
|
|
||||||
if (path) path.setAttribute('fill', fillColor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let ss = new SS('ws://' + window.location.host + '/notify');
|
|
||||||
ss.onConnect(() => ss.emit('join', "global"));
|
|
||||||
ss.onDisconnect(() => alert('chat disconnected'));
|
|
||||||
ss.on('message', msg => addOrUpdateTask(msg, false));
|
|
||||||
ss.on('final-message', msg => addOrUpdateTask(msg, true));
|
|
||||||
})(window.SS);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
171
examples/webroot/js/app.js
Normal file
171
examples/webroot/js/app.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
(function(SS) {
|
||||||
|
'use strict';
|
||||||
|
let uiContent
|
||||||
|
function loadSVG(url) {
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(svgContent => {
|
||||||
|
const container = document.getElementById('svg-container');
|
||||||
|
container.src = url;
|
||||||
|
uiContent = svgContent;
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Failed to load SVG:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
loadSVG('http://localhost:8083/ui');
|
||||||
|
};
|
||||||
|
document.getElementById('send-request').addEventListener('click', function() {
|
||||||
|
const input = document.getElementById('payload');
|
||||||
|
const payloadData = JSON.parse(input.value);
|
||||||
|
const data = { payload: payloadData };
|
||||||
|
|
||||||
|
fetch('http://localhost:8083/request', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => console.log('Success:', data))
|
||||||
|
.catch((error) => console.error('Error:', error));
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = {};
|
||||||
|
|
||||||
|
function attachSVGNodeEvents() {
|
||||||
|
const svgNodes = document.querySelectorAll('g.node'); // Adjust selector as per your SVG structure
|
||||||
|
svgNodes.forEach(node => {
|
||||||
|
node.classList.add("cursor-pointer")
|
||||||
|
node.addEventListener('click', handleSVGNodeClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle the click on an SVG node and show the popover
|
||||||
|
function handleSVGNodeClick(event) {
|
||||||
|
const nodeId = event.currentTarget.id; // Get the node ID (e.g., 'node_store:data')
|
||||||
|
const nodeData = findNodeDataById(nodeId); // Fetch data related to the node (status, result, etc.)
|
||||||
|
if (nodeData) {
|
||||||
|
showSVGPopover(event, nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to show the popover next to the clicked SVG node
|
||||||
|
function showSVGPopover(event, nodeData) {
|
||||||
|
const popover = document.getElementById('svg-popover');
|
||||||
|
document.getElementById('task-id').innerHTML = nodeData.task_id
|
||||||
|
popover.classList.add('visible');
|
||||||
|
popover.innerHTML = `
|
||||||
|
<div class="text-sm text-gray-600 space-y-1">
|
||||||
|
<div class="grid grid-cols-5 gap-x-4">
|
||||||
|
<strong class="text-gray-800 col-span-1">Node:</strong>
|
||||||
|
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.topic}</span>
|
||||||
|
|
||||||
|
<strong class="text-gray-800 col-span-1">Status:</strong>
|
||||||
|
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.status}</span>
|
||||||
|
|
||||||
|
<strong class="text-gray-800 col-span-1">Result:</strong>
|
||||||
|
<pre class="text-gray-700 col-span-4 p-2 border border-gray-300 rounded-md max-h-32 overflow-auto bg-gray-50">${JSON.stringify(nodeData.payload, null, 2)}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<strong class="text-gray-800 col-span-1">Error:</strong>
|
||||||
|
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.error || 'N/A'}</span>
|
||||||
|
|
||||||
|
<strong class="text-gray-800 col-span-1">Created At:</strong>
|
||||||
|
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.created_at}</span>
|
||||||
|
|
||||||
|
<strong class="text-gray-800 col-span-1">Processed At:</strong>
|
||||||
|
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.processed_at}</span>
|
||||||
|
|
||||||
|
<strong class="text-gray-800 col-span-1">Latency:</strong>
|
||||||
|
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.latency}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to find node data (status, result, error) by the node's ID
|
||||||
|
function findNodeDataById(nodeId) {
|
||||||
|
for (const taskId in tasks) {
|
||||||
|
const task = tasks[taskId];
|
||||||
|
const node = task.nodes.find(n => `node_${n.topic}` === nodeId); // Ensure the ID format matches your SVG
|
||||||
|
if (node) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null; // Return null if no matching node is found
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOrUpdateTask(message, isFinal = false) {
|
||||||
|
const taskTableBody = document.getElementById('taskTableBody');
|
||||||
|
const taskId = message.task_id;
|
||||||
|
const rowId = `row-${taskId}`;
|
||||||
|
let existingRow = document.getElementById(rowId);
|
||||||
|
|
||||||
|
if (!existingRow) {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.id = rowId;
|
||||||
|
taskTableBody.insertBefore(row, taskTableBody.firstChild);
|
||||||
|
existingRow = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks[taskId] = tasks[taskId] || { nodes: [], final: null };
|
||||||
|
if (isFinal) tasks[taskId].final = message;
|
||||||
|
else tasks[taskId].nodes.push(message);
|
||||||
|
|
||||||
|
const latestStatus = isFinal ? message.status : message.status;
|
||||||
|
const statusColor = latestStatus === 'success' ? 'bg-green-100 text-green-700' :
|
||||||
|
latestStatus === 'fail' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700';
|
||||||
|
|
||||||
|
existingRow.innerHTML = `
|
||||||
|
<td class="px-4 py-2 border border-gray-300">${taskId}</td>
|
||||||
|
<td class="px-4 py-2 border border-gray-300">${new Date(message.created_at).toLocaleString()}</td>
|
||||||
|
<td class="px-4 py-2 border border-gray-300">${new Date(message.processed_at).toLocaleString()}</td>
|
||||||
|
<td class="px-4 py-2 border border-gray-300">${message.latency}</td>
|
||||||
|
<td class="px-4 py-2 border border-gray-300 ${statusColor}">${latestStatus}</td>
|
||||||
|
<td class="px-4 py-2 border border-gray-300">
|
||||||
|
<button class="view-btn text-blue-600 hover:underline" data-task-id='${taskId}'>View</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
attachViewButtonEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachViewButtonEvent() {
|
||||||
|
const buttons = document.querySelectorAll('.view-btn');
|
||||||
|
buttons.forEach(button => {
|
||||||
|
button.removeEventListener('click', handleViewButtonClick);
|
||||||
|
button.addEventListener('click', handleViewButtonClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewButtonClick(event) {
|
||||||
|
document.getElementById("task-svg").innerHTML = uiContent
|
||||||
|
attachSVGNodeEvents();
|
||||||
|
const taskId = event.target.getAttribute('data-task-id');
|
||||||
|
const task = tasks[taskId];
|
||||||
|
updateSVGNodes(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSVGNodes(task) {
|
||||||
|
console.log(task)
|
||||||
|
task.nodes.forEach((node) => {
|
||||||
|
const svgNode = document.querySelector(`#node_${node.topic.replace(':', '\\:')}`);
|
||||||
|
console.log(svgNode)
|
||||||
|
if (svgNode) {
|
||||||
|
const fillColor = node.status === 'success' ? '#A5D6A7' : node.status === 'fail' ? '#EF9A9A' : '#FFE082';
|
||||||
|
const path = svgNode.querySelector('path');
|
||||||
|
if (path) path.setAttribute('fill', fillColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let ss = new SS('ws://' + window.location.host + '/notify');
|
||||||
|
ss.onConnect(() => ss.emit('join', "global"));
|
||||||
|
ss.onDisconnect(() => alert('chat disconnected'));
|
||||||
|
ss.on('message', msg => addOrUpdateTask(msg, false));
|
||||||
|
ss.on('final-message', msg => addOrUpdateTask(msg, true));
|
||||||
|
})(window.SS);
|
Reference in New Issue
Block a user