feat: add dynamic Mermaid diagram support and configuration and add diagrams for legacy staff application

This commit is contained in:
Achintha Isuru
2025-11-13 18:22:47 -05:00
parent 7d924a7670
commit 0970218682
4 changed files with 465 additions and 36 deletions

View File

@@ -9,6 +9,8 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<!-- Mermaid -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>
<!-- Custom CSS -->
<style>
body {
@@ -25,6 +27,7 @@
background-color: #ffffff;
border-right: 1px solid #dee2e6;
padding: 1.5rem;
overflow-y: auto;
}
.sidebar .nav-link {
@@ -54,14 +57,33 @@
margin-bottom: 0.5rem;
}
.sidebar-subheading {
font-size: 0.7rem;
font-weight: 600;
color: #868e96;
padding: 0 1rem 0 2rem;
margin-top: 0.75rem;
margin-bottom: 0.25rem;
}
.nav-link.sub-item {
padding-left: 2.5rem;
font-size: 0.9rem;
}
.nav-link.sub-sub-item {
padding-left: 3.5rem;
font-size: 0.85rem;
}
.content-area {
padding: 2rem;
overflow-y: auto;
}
#diagram-viewer {
display: none; /* Hidden by default */
height: calc(100vh - 4rem); /* Full height minus padding */
display: none;
height: calc(100vh - 4rem);
display: flex;
flex-direction: column;
}
@@ -74,9 +96,31 @@
background-color: #ffffff;
border-radius: 0.5rem;
border: 1px solid #dee2e6;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
#diagram-container:focus { outline: none; }
#diagram-container:active {
cursor: grabbing;
}
#diagram-container:focus {
outline: none;
}
#diagram-container svg {
max-width: 100%;
height: auto;
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>
</head>
@@ -85,27 +129,30 @@
<!-- Sidebar -->
<nav class="sidebar">
<div class="d-flex justify-content-center align-items-center mb-4">
<img src="logo.svg" alt="Krow Logo" style="height: 60px;">
<img src="logo.svg" alt="Krow Logo" style="height: 60px;" onerror="this.style.display='none'">
</div>
<div class="nav flex-column nav-pills">
<div class="nav flex-column nav-pills" id="sidebar-nav">
<a class="nav-link active" href="#" id="nav-home" onclick="showView('home', this)">
<i class="bi bi-house-door"></i>Home
</a>
<div class="sidebar-heading">Diagrams</div>
<div class="sidebar-heading">Static Diagrams</div>
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/high-level-overview.svg', 'Apps High-Level Overview')">
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/high-level-overview.svg', 'Apps High-Level Overview', 'svg')">
<i class="bi bi-diagram-3"></i>High-Level Overview
</a>
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/shift-lifecycle-workflow.svg', 'Core Workflow - Shift Lifecycle')">
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/shift-lifecycle-workflow.svg', 'Core Workflow - Shift Lifecycle', 'svg')">
<i class="bi bi-arrow-repeat"></i>Shift Lifecycle
</a>
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/invoice-workflow.svg', 'Invoice Workflow - Complete')">
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/invoice-workflow.svg', 'Invoice Workflow - Complete', 'svg')">
<i class="bi bi-receipt"></i>Invoice Workflow
</a>
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/complete-workflow.svg', 'Complete Workflow')">
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/complete-workflow.svg', 'Complete Workflow', 'svg')">
<i class="bi bi-infinity"></i>Complete Workflow
</a>
<!-- Dynamic Mermaid diagrams will be inserted here -->
<div id="dynamic-diagrams-section"></div>
</div>
</nav>
@@ -187,6 +234,8 @@
<script src="https://cdn.jsdelivr.net/npm/@panzoom/panzoom@4.5.1/dist/panzoom.min.js"></script>
<script>
let mermaidDiagrams = [];
const homeView = document.getElementById('home-view');
const diagramViewer = document.getElementById('diagram-viewer');
const diagramContainer = document.getElementById('diagram-container');
@@ -197,6 +246,123 @@
const resetBtn = document.getElementById('resetBtn');
let panzoomInstance = null;
let currentScale = 1;
// Initialize Mermaid
mermaid.initialize({
startOnLoad: false,
theme: 'default',
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
}
});
// Build hierarchical structure from paths
function buildDiagramHierarchy(diagrams) {
const hierarchy = {};
diagrams.forEach(diagram => {
const parts = diagram.path.split('/');
// Remove 'assets/diagrams/' prefix
const relevantParts = parts.slice(2, -1); // Everything except filename
const filename = parts[parts.length - 1].replace('.mermaid', '');
let current = hierarchy;
relevantParts.forEach(part => {
if (!current[part]) {
current[part] = { _items: [], _children: {} };
}
current = current[part]._children;
});
// Add the item to the last level
const lastLevel = relevantParts.length > 0 ?
relevantParts.reduce((acc, part) => acc[part]._children, hierarchy) :
hierarchy;
if (relevantParts.length > 0) {
const parentKey = relevantParts[relevantParts.length - 1];
if (!hierarchy[relevantParts[0]]) {
hierarchy[relevantParts[0]] = { _items: [], _children: {} };
}
let parent = hierarchy[relevantParts[0]];
for (let i = 1; i < relevantParts.length; i++) {
parent = parent._children[relevantParts[i]];
}
parent._items.push(diagram);
} else {
if (!hierarchy._root) {
hierarchy._root = { _items: [], _children: {} };
}
hierarchy._root._items.push(diagram);
}
});
return hierarchy;
}
// Create navigation from hierarchy
function createNavigation(hierarchy, parentElement, level = 0) {
Object.keys(hierarchy).forEach(key => {
if (key === '_items' || key === '_children' || key === '_root') return;
const section = hierarchy[key];
const heading = document.createElement('div');
heading.className = level === 0 ? 'sidebar-heading' : 'sidebar-subheading';
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
parentElement.appendChild(heading);
// Add items in this section
if (section._items && section._items.length > 0) {
section._items.forEach(diagram => {
const link = document.createElement('a');
link.className = `nav-link ${level > 0 ? 'sub-item' : ''}`;
link.href = '#';
link.onclick = (e) => {
e.preventDefault();
showView('diagram', link, diagram.path, diagram.title, 'mermaid');
};
link.innerHTML = `<i class="bi bi-file-earmark-code"></i>${diagram.title}`;
parentElement.appendChild(link);
});
}
// Recursively add children
if (section._children && Object.keys(section._children).length > 0) {
createNavigation(section._children, parentElement, level + 1);
}
});
}
// Load and render dynamic diagrams navigation
async function loadDynamicDiagrams() {
const dynamicSection = document.getElementById('dynamic-diagrams-section');
try {
// Fetch the diagrams configuration from JSON file
const response = await fetch('./assets/diagrams/diagrams-config.json');
if (!response.ok) {
throw new Error(`Failed to load diagrams config: ${response.status}`);
}
mermaidDiagrams = await response.json();
if (mermaidDiagrams.length > 0) {
const mainHeading = document.createElement('div');
mainHeading.className = 'sidebar-heading';
mainHeading.textContent = 'Mermaid Diagrams';
dynamicSection.appendChild(mainHeading);
const hierarchy = buildDiagramHierarchy(mermaidDiagrams);
createNavigation(hierarchy, dynamicSection);
}
} catch (error) {
console.error('Error loading diagrams configuration:', error);
dynamicSection.innerHTML = `<div class="alert alert-warning m-2 small">No Mermaid diagrams configuration found</div>`;
}
}
function setActiveNav(activeLink) {
document.querySelectorAll('.sidebar .nav-link').forEach(link => {
@@ -205,10 +371,14 @@
activeLink.classList.add('active');
}
async function showView(viewName, navLink, svgPath, title) {
async function showView(viewName, navLink, filePath, title, type = 'svg') {
setActiveNav(navLink);
if (panzoomInstance) panzoomInstance.destroy();
if (panzoomInstance) {
panzoomInstance.destroy();
panzoomInstance = null;
}
diagramContainer.innerHTML = '';
currentScale = 1;
if (viewName === 'home') {
homeView.style.display = 'block';
@@ -217,30 +387,59 @@
homeView.style.display = 'none';
diagramViewer.style.display = 'flex';
diagramTitle.textContent = title;
diagramContainer.innerHTML = '<div class="d-flex justify-content-center align-items-center h-100"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>';
diagramContainer.innerHTML = '<div class="loading-spinner"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>';
try {
const response = await fetch(svgPath);
if (!response.ok) throw new Error(`Network error: ${response.status}`);
const svgContent = await response.text();
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgContent, "image/svg+xml");
const svgElement = svgDoc.querySelector('svg');
if (type === 'svg') {
// Load static SVG files
const response = await fetch(filePath);
if (!response.ok) throw new Error(`Network error: ${response.status}`);
const svgContent = await response.text();
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgContent, "image/svg+xml");
const svgElement = svgDoc.querySelector('svg');
if (svgElement) {
diagramContainer.innerHTML = '';
diagramContainer.appendChild(svgElement);
panzoomInstance = Panzoom(svgElement, {
canvas: true,
maxScale: 10,
minScale: 0.3,
zoomOnDblClick: false
});
diagramContainer.addEventListener('wheel', panzoomInstance.zoomWithWheel);
diagramContainer.focus();
} else {
throw new Error('No SVG element found.');
if (svgElement) {
diagramContainer.innerHTML = '';
diagramContainer.appendChild(svgElement);
panzoomInstance = Panzoom(svgElement, {
canvas: true,
maxScale: 10,
minScale: 0.3,
startScale: 1
});
diagramContainer.addEventListener('wheel', panzoomInstance.zoomWithWheel);
diagramContainer.focus();
} else {
throw new Error('No SVG element found.');
}
} else if (type === 'mermaid') {
// Load and render Mermaid files
const response = await fetch(filePath);
if (!response.ok) throw new Error(`Network error: ${response.status}`);
const mermaidCode = await response.text();
const { svg } = await mermaid.render('mermaidDiagram_' + Date.now(), mermaidCode);
diagramContainer.innerHTML = svg;
const svgElement = diagramContainer.querySelector('svg');
if (svgElement) {
svgElement.style.maxWidth = 'none';
svgElement.style.height = 'auto';
panzoomInstance = Panzoom(svgElement, {
canvas: true,
maxScale: 10,
minScale: 0.3,
startScale: 1
});
diagramContainer.addEventListener('wheel', panzoomInstance.zoomWithWheel);
diagramContainer.focus();
} else {
throw new Error('Failed to render Mermaid diagram.');
}
}
} catch (error) {
diagramContainer.innerHTML = `<div class="alert alert-danger m-3">Failed to load diagram: ${error.message}</div>`;
@@ -248,11 +447,29 @@
}
}
zoomInBtn.addEventListener('click', () => { panzoomInstance?.zoomIn(); diagramContainer.focus(); });
zoomOutBtn.addEventListener('click', () => { panzoomInstance?.zoomOut(); diagramContainer.focus(); });
resetBtn.addEventListener('click', () => { panzoomInstance?.reset(); diagramContainer.focus(); });
zoomInBtn.addEventListener('click', () => {
if (panzoomInstance) {
panzoomInstance.zoomIn();
diagramContainer.focus();
}
});
zoomOutBtn.addEventListener('click', () => {
if (panzoomInstance) {
panzoomInstance.zoomOut();
diagramContainer.focus();
}
});
resetBtn.addEventListener('click', () => {
if (panzoomInstance) {
panzoomInstance.reset();
diagramContainer.focus();
}
});
document.addEventListener('DOMContentLoaded', () => {
loadDynamicDiagrams();
showView('home', document.getElementById('nav-home'));
});
</script>