provisioning-platform/crates/provisioning-daemon/ui/templates/pages/workspace_ontology.html

103 lines
6.1 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% import "macros/ui.html" as m %}
{% block title %}{{ ws_name }} — Ontology{% endblock %}
{% block nav_workspaces %}btn-active{% endblock %}
{% block main_class %}p-0{% endblock %}
{% block head %}
<style>
#graph-root { display:flex; height:calc(100vh - 64px - 88px); min-height:500px; }
#cy-wrapper { flex:1 1 auto; min-width:0; position:relative; }
#cy { width:100%; height:100%; }
#cy-controls { position:absolute; bottom:14px; right:14px; z-index:10; display:flex; flex-direction:column; gap:4px; }
#cy-controls button { width:30px; height:30px; border-radius:6px; border:1px solid rgba(255,255,255,0.1); background:rgba(0,0,0,0.6); color:#fff; font-size:16px; cursor:pointer; }
#cy-controls button:hover { background:rgba(80,80,80,0.8); }
#sidebar { width:280px; flex-shrink:0; overflow-y:auto; background:oklch(var(--b2,20% 0 0)); border-left:1px solid oklch(var(--b3,15% 0 0)); padding:1rem; font-size:0.75rem; }
</style>
{% endblock %}
{% block content %}
<div class="px-4 pt-3 pb-1">
{{ m::ws_tabs(ws_name=ws_name, active=ws_active_tab, ws_envs=ws_envs, ws_has_clusters=ws_has_clusters, ws_has_workflows=ws_has_workflows, current_env=current_env) }}
</div>
<div class="px-4 pb-2 flex items-center gap-4">
<h2 class="text-base font-bold font-mono">
Ontology
<span class="text-base-content/30 font-normal text-sm ml-2">{{ ws_name }}</span>
</h2>
<div class="flex gap-2 text-xs">
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" id="show-axioms" checked class="checkbox checkbox-xs">
<span class="badge badge-primary badge-xs">Axioms</span>
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" id="show-practices" checked class="checkbox checkbox-xs">
<span class="badge badge-success badge-xs">Practices</span>
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" id="show-tensions" checked class="checkbox checkbox-xs">
<span class="badge badge-warning badge-xs">Tensions</span>
</label>
</div>
{% if not has_root %}
<div class="alert alert-warning alert-xs py-1 px-3 text-xs">Workspace path not resolved.</div>
{% endif %}
</div>
<div id="graph-root">
<div id="cy-wrapper">
<div id="cy"></div>
<div id="cy-controls">
<button onclick="cy.fit()" title="Fit"></button>
<button onclick="cy.zoom(cy.zoom()*1.25)" title="Zoom in">+</button>
<button onclick="cy.zoom(cy.zoom()*0.8)" title="Zoom out"></button>
<button onclick="resetLayout()" title="Re-layout"></button>
</div>
</div>
<div id="sidebar">
<p class="text-base-content/40 text-xs mb-3">Click a node to inspect.</p>
<div id="node-detail"></div>
</div>
</div>
<script>
const GRAPH = {{ graph_json | safe }};
let cy = null;
function levelColor(l){return l==='Axiom'?'#6366f1':l==='Practice'?'#22c55e':l==='Tension'?'#f59e0b':'#6b7280';}
function buildElements(){
const nodes=(GRAPH.nodes||[]).map(n=>({data:{id:n.id,label:n.name||n.id,level:n.level,description:n.description||'',color:levelColor(n.level),invariant:n.invariant||false,artifact_paths:n.artifact_paths||[]}}));
const edges=(GRAPH.edges||[]).map((e,i)=>({data:{id:'e'+i,source:e.from,target:e.to,kind:e.kind,note:e.note||'',weight:e.weight}}));
return[...nodes,...edges];
}
function initCytoscape(){
cy=cytoscape({container:document.getElementById('cy'),elements:buildElements(),
style:[
{selector:'node',style:{'background-color':'data(color)','label':'data(label)','color':'#f1f5f9','font-size':'8px','font-family':'ui-monospace,monospace','text-valign':'center','text-halign':'center','text-wrap':'wrap','text-max-width':'56px','text-overflow-wrap':'anywhere','width':72,'height':72,'border-width':2,'border-color':'rgba(255,255,255,0.15)'}},
{selector:'node[?invariant]',style:{'border-width':3,'border-color':'#a5b4fc','border-style':'double'}},
{selector:'edge',style:{'width':1.5,'line-color':'rgba(156,163,175,0.4)','target-arrow-color':'rgba(156,163,175,0.6)','target-arrow-shape':'triangle','curve-style':'bezier','arrow-scale':0.8}},
{selector:'node:selected',style:{'border-color':'#818cf8','border-width':3}},
],
layout:{name:'cose',animate:false,nodeRepulsion:12000,idealEdgeLength:140,gravity:0.15,numIter:1000,fit:true,padding:30}
});
cy.on('tap','node',function(evt){
const d=evt.target.data();
const paths=d.artifact_paths.length?d.artifact_paths.map(p=>`<code class="font-mono">${p}</code>`).join('<br>'):'<span class="text-base-content/30"></span>';
document.getElementById('node-detail').innerHTML=`<div class="mb-2"><span class="badge badge-xs" style="background:${d.color}">${d.level}</span><span class="font-mono font-semibold ml-1">${d.id}</span></div><p class="font-semibold text-sm mb-1">${d.label}</p><p class="text-base-content/60 mb-3">${d.description}</p><div class="text-base-content/40 text-xs">${paths}</div>`;
});
cy.on('tap',function(evt){if(evt.target===cy)document.getElementById('node-detail').innerHTML='';});
}
function resetLayout(){if(cy)cy.layout({name:'cose',animate:true,nodeRepulsion:12000,idealEdgeLength:140,gravity:0.15,numIter:1000}).run();}
function applyFilters(){
if(!cy)return;
const sa=document.getElementById('show-axioms').checked;
const sp=document.getElementById('show-practices').checked;
const st=document.getElementById('show-tensions').checked;
cy.nodes().forEach(n=>{const l=n.data('level');n.style('display',(l==='Axiom'&&sa)||(l==='Practice'&&sp)||(l==='Tension'&&st)||!['Axiom','Practice','Tension'].includes(l)?'element':'none');});
cy.edges().forEach(e=>{e.style('display',e.source().style('display')!=='none'&&e.target().style('display')!=='none'?'element':'none');});
}
['show-axioms','show-practices','show-tensions'].forEach(id=>{document.getElementById(id).addEventListener('change',applyFilters);});
if(typeof cytoscape!=='undefined'){initCytoscape();}else{const s=document.createElement('script');s.src='https://cdn.jsdelivr.net/npm/cytoscape@3.30.2/dist/cytoscape.min.js';s.onload=initCytoscape;document.head.appendChild(s);}
</script>
{% endblock %}