Initial artdag-common shared library
Shared components for L1 and L2 servers: - Jinja2 template system with base template and components - Middleware for auth and content negotiation - Pydantic models for requests/responses - Utility functions for pagination, media, formatting - Constants for Tailwind/HTMX/Cytoscape CDNs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
artdag_common/templates/components/dag.html
Normal file
176
artdag_common/templates/components/dag.html
Normal file
@@ -0,0 +1,176 @@
|
||||
{#
|
||||
Cytoscape.js DAG visualization component.
|
||||
|
||||
Usage:
|
||||
{% from "components/dag.html" import dag_container, dag_scripts, dag_legend %}
|
||||
|
||||
{# In head block #}
|
||||
{{ dag_scripts() }}
|
||||
|
||||
{# In content #}
|
||||
{{ dag_container(id="plan-dag", height="400px") }}
|
||||
{{ dag_legend() }}
|
||||
|
||||
{# In scripts block #}
|
||||
<script>
|
||||
initDag('plan-dag', {{ nodes | tojson }}, {{ edges | tojson }});
|
||||
</script>
|
||||
#}
|
||||
|
||||
{% macro dag_scripts() %}
|
||||
<script src="{{ CYTOSCAPE_CDN }}"></script>
|
||||
<script src="{{ DAGRE_CDN }}"></script>
|
||||
<script src="{{ CYTOSCAPE_DAGRE_CDN }}"></script>
|
||||
<script>
|
||||
// Global Cytoscape instance for WebSocket updates
|
||||
window.artdagCy = null;
|
||||
|
||||
function initDag(containerId, nodes, edges) {
|
||||
const nodeColors = {{ NODE_COLORS | tojson }};
|
||||
|
||||
window.artdagCy = cytoscape({
|
||||
container: document.getElementById(containerId),
|
||||
elements: {
|
||||
nodes: nodes,
|
||||
edges: edges
|
||||
},
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'label': 'data(label)',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'background-color': function(ele) {
|
||||
return nodeColors[ele.data('nodeType')] || nodeColors['default'];
|
||||
},
|
||||
'color': '#fff',
|
||||
'font-size': '10px',
|
||||
'width': 80,
|
||||
'height': 40,
|
||||
'shape': 'round-rectangle',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '70px',
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node[status="cached"], node[status="completed"]',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#22c55e'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node[status="running"]',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#eab308',
|
||||
'border-style': 'dashed'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node:selected',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#3b82f6'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 2,
|
||||
'line-color': '#6b7280',
|
||||
'target-arrow-color': '#6b7280',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier'
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'dagre',
|
||||
rankDir: 'TB',
|
||||
nodeSep: 50,
|
||||
rankSep: 80,
|
||||
padding: 20
|
||||
},
|
||||
userZoomingEnabled: true,
|
||||
userPanningEnabled: true,
|
||||
boxSelectionEnabled: false
|
||||
});
|
||||
|
||||
// Click handler for node details
|
||||
window.artdagCy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
const data = node.data();
|
||||
showNodeDetails(data);
|
||||
});
|
||||
|
||||
return window.artdagCy;
|
||||
}
|
||||
|
||||
function showNodeDetails(data) {
|
||||
const panel = document.getElementById('node-details');
|
||||
if (!panel) return;
|
||||
|
||||
panel.innerHTML = `
|
||||
<h4 class="font-medium text-white mb-2">${data.label || data.id}</h4>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Type</dt>
|
||||
<dd class="text-white">${data.nodeType || 'Unknown'}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Status</dt>
|
||||
<dd class="text-white">${data.status || 'pending'}</dd>
|
||||
</div>
|
||||
${data.cacheId ? `
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Cache ID</dt>
|
||||
<dd class="text-white font-mono text-xs">${data.cacheId.substring(0, 16)}...</dd>
|
||||
</div>
|
||||
` : ''}
|
||||
${data.level !== undefined ? `
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Level</dt>
|
||||
<dd class="text-white">${data.level}</dd>
|
||||
</div>
|
||||
` : ''}
|
||||
</dl>
|
||||
`;
|
||||
panel.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Future WebSocket support: update node status in real-time
|
||||
function updateNodeStatus(stepId, status, cacheId) {
|
||||
if (!window.artdagCy) return;
|
||||
const node = window.artdagCy.getElementById(stepId);
|
||||
if (node && node.length > 0) {
|
||||
node.data('status', status);
|
||||
if (cacheId) {
|
||||
node.data('cacheId', cacheId);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dag_container(id="dag-container", height="400px", class="") %}
|
||||
<div id="{{ id }}" class="w-full bg-dark-700 rounded-lg {{ class }}" style="height: {{ height }};"></div>
|
||||
<div id="node-details" class="hidden mt-4 p-4 bg-dark-600 rounded-lg"></div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dag_legend(node_types=None) %}
|
||||
{% set types = node_types or ["SOURCE", "EFFECT", "_LIST"] %}
|
||||
<div class="flex gap-4 text-sm flex-wrap mt-4">
|
||||
{% for type in types %}
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded" style="background-color: {{ NODE_COLORS.get(type, NODE_COLORS.default) }}"></span>
|
||||
{{ type }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded border-2 border-green-500 bg-dark-600"></span>
|
||||
Cached
|
||||
</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user