generated from Luis/nextjs-python-web-template
added mermaid diagram
This commit is contained in:
committed by
Sara Pervana
parent
47700b7bd0
commit
7b2b675c2c
1
pkgs/ui/_document.js
Normal file
1
pkgs/ui/_document.js
Normal file
@@ -0,0 +1 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
@@ -3,18 +3,27 @@
|
||||
import { DLGResolutionTableConfig, DLGSummaryDetails } from "@/config/dlg";
|
||||
import CustomTable from "@/components/table";
|
||||
import SummaryDetails from "@/components/summary_card";
|
||||
import useFetch from "@/components/hooks/useFetch";
|
||||
import { useEffect } from "react";
|
||||
import { useGetAllResolutions } from "@/api/resolution/resolution";
|
||||
import { mutate } from "swr";
|
||||
|
||||
export default function DLG() {
|
||||
|
||||
const {
|
||||
data: resolutionData,
|
||||
loading: loadingResolutions,
|
||||
fetch,
|
||||
} = useFetch("/get_resolutions");
|
||||
isLoading: loadingResolutions,
|
||||
swrKey: resolutionsKeyFunc,
|
||||
} = useGetAllResolutions();
|
||||
|
||||
const onRefresh = () => {
|
||||
fetch();
|
||||
const resolutionsKey =
|
||||
typeof resolutionsKeyFunc === "function"
|
||||
? resolutionsKeyFunc()
|
||||
: resolutionsKeyFunc;
|
||||
|
||||
if (resolutionsKey) {
|
||||
mutate(resolutionsKey);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -41,7 +50,7 @@ export default function DLG() {
|
||||
<h4>DID Resolution View</h4>
|
||||
<CustomTable
|
||||
loading={loadingResolutions}
|
||||
data={resolutionData}
|
||||
data={resolutionData?.data}
|
||||
configuration={DLGResolutionTableConfig}
|
||||
tkey="resolution_table"
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useAppState } from "@/components/hooks/useAppContext";
|
||||
import { NoDataOverlay } from "@/components/noDataOverlay";
|
||||
import SummaryDetails from "@/components/summary_card";
|
||||
import CustomTable from "@/components/table";
|
||||
import { HomeTableConfig } from "@/config/home";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect } from "react";
|
||||
import { mutate } from "swr";
|
||||
|
||||
const NoSSRSequenceDiagram = dynamic(() => import('../../components/sequence_diagram'), { ssr: false })
|
||||
|
||||
|
||||
export default function Home() {
|
||||
const { data } = useAppState();
|
||||
|
||||
@@ -51,7 +54,7 @@ export default function Home() {
|
||||
|
||||
<div>
|
||||
<h4>Sequence Diagram</h4>
|
||||
<NoDataOverlay label="No Activity yet" />
|
||||
<NoSSRSequenceDiagram />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -50,6 +50,16 @@ export default function RootLayout({
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Service Aware Networks" />
|
||||
<link rel="icon" href="tub-favicon.ico" sizes="any" />
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
<script
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `mermaid.initialize({startOnLoad: true});`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={userPrefersDarkmode ? darkTheme : lightTheme}>
|
||||
@@ -71,9 +81,8 @@ export default function RootLayout({
|
||||
onClose={() => setShowSidebar(false)}
|
||||
/>
|
||||
<div
|
||||
className={tw`${
|
||||
!showSidebarDerived && translate
|
||||
} flex h-full w-full flex-col overflow-y-scroll transition-[margin] duration-150 ease-in-out`}
|
||||
className={tw`${!showSidebarDerived && translate
|
||||
} flex h-full w-full flex-col overflow-y-scroll transition-[margin] duration-150 ease-in-out`}
|
||||
>
|
||||
<div className="grid grid-cols-3">
|
||||
<div className="col-span-1">
|
||||
|
||||
133
pkgs/ui/src/components/sequence_diagram/helpers.ts
Normal file
133
pkgs/ui/src/components/sequence_diagram/helpers.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
export const generateMermaidString = (data: any) => {
|
||||
|
||||
if (!data || data.length === 0)
|
||||
return '';
|
||||
|
||||
// Extract unique participants
|
||||
const participants = Array.from(new Set(data.map((item: any) => item.src_did).concat(data.map((item: any) => item.des_did))));
|
||||
|
||||
// Begin the sequence diagram definition
|
||||
let mermaidString = "sequenceDiagram\n";
|
||||
|
||||
// Add participants to the diagram
|
||||
participants.forEach((participant, index) => {
|
||||
mermaidString += ` participant ${String.fromCharCode(65 + index)} as ${participant}\n`;
|
||||
});
|
||||
|
||||
// Add messages to the diagram
|
||||
data.forEach((item: any, index: number) => {
|
||||
const srcParticipant = String.fromCharCode(65 + participants.indexOf(item.src_did));
|
||||
const desParticipant = String.fromCharCode(65 + participants.indexOf(item.des_did));
|
||||
const timestamp = new Date(item.timestamp * 1000).toLocaleString(); // Convert Unix timestamp to readable date
|
||||
const message = item.msg.text || `Event message ${index + 1}`;
|
||||
|
||||
// If group_name or group_id exists, start an 'alt' block
|
||||
if (item.group_id != null) {
|
||||
mermaidString += ` alt ${item.group_name || item.group_id}\n`;
|
||||
mermaidString += ` rect rgb(191, 223, 255)\n`;
|
||||
}
|
||||
|
||||
// Add the message interaction
|
||||
mermaidString += ` ${srcParticipant}->>${desParticipant}: [${timestamp}] ${message}\n`;
|
||||
|
||||
// If there was an 'alt' block, close it
|
||||
if (item.group_id != null) {
|
||||
mermaidString += ` end\n`;
|
||||
mermaidString += ` end\n`;
|
||||
}
|
||||
});
|
||||
|
||||
return mermaidString;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Dummy Data
|
||||
|
||||
export const dataFromBE = [
|
||||
{
|
||||
"id": 12,
|
||||
"timestamp": 1704892813,
|
||||
"group": 0,
|
||||
"group_id": 12,
|
||||
// "group_name": "Data",
|
||||
"msg_type": 4,
|
||||
"src_did": "did:sov:test:121",
|
||||
// "src_name": "Entity A",
|
||||
"des_did": "did:sov:test:120",
|
||||
// "des_name": "Entity B",
|
||||
"msg": {
|
||||
text: 'Hello World'
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"timestamp": 1704892823,
|
||||
"group": 1,
|
||||
"group_id": 19,
|
||||
"msg_type": 4,
|
||||
"src_did": "did:sov:test:122",
|
||||
"des_did": "did:sov:test:121",
|
||||
"msg": {}
|
||||
},
|
||||
{
|
||||
"id": 30162,
|
||||
"timestamp": 1704892817,
|
||||
"group": 1,
|
||||
"group_id": 53,
|
||||
"msg_type": 2,
|
||||
"src_did": "did:sov:test:121",
|
||||
"des_did": "did:sov:test:122",
|
||||
"msg": {}
|
||||
},
|
||||
{
|
||||
"id": 63043,
|
||||
"timestamp": 1704892809,
|
||||
"group": 0,
|
||||
"group_id": 12,
|
||||
"msg_type": 3,
|
||||
"src_did": "did:sov:test:121",
|
||||
"des_did": "did:sov:test:120",
|
||||
"msg": {}
|
||||
},
|
||||
{
|
||||
"id": 66251,
|
||||
"timestamp": 1704892805,
|
||||
"group": 0,
|
||||
"group_id": 51,
|
||||
"msg_type": 1,
|
||||
"src_did": "did:sov:test:120",
|
||||
"des_did": "did:sov:test:121",
|
||||
"msg": {}
|
||||
},
|
||||
{
|
||||
"id": 85434,
|
||||
"timestamp": 1704892807,
|
||||
"group": 0,
|
||||
"group_id": 51,
|
||||
"msg_type": 2,
|
||||
"src_did": "did:sov:test:120",
|
||||
"des_did": "did:sov:test:121",
|
||||
"msg": {}
|
||||
},
|
||||
{
|
||||
"id": 124842,
|
||||
"timestamp": 1704892819,
|
||||
"group": 1,
|
||||
"group_id": 19,
|
||||
"msg_type": 3,
|
||||
"src_did": "did:sov:test:122",
|
||||
"des_did": "did:sov:test:121",
|
||||
"msg": {}
|
||||
},
|
||||
{
|
||||
"id": 246326,
|
||||
"timestamp": 1704892815,
|
||||
"group": 1,
|
||||
"group_id": 53,
|
||||
"msg_type": 1,
|
||||
"src_did": "did:sov:test:121",
|
||||
"des_did": "did:sov:test:122",
|
||||
"msg": {}
|
||||
}
|
||||
]
|
||||
178
pkgs/ui/src/components/sequence_diagram/index.tsx
Normal file
178
pkgs/ui/src/components/sequence_diagram/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import mermaid from 'mermaid';
|
||||
import { IconButton } from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
|
||||
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import ResetIcon from '@mui/icons-material/Autorenew';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { NoDataOverlay } from '../noDataOverlay';
|
||||
import { useGetAllEventmessages } from '@/api/eventmessages/eventmessages';
|
||||
import { mutate } from 'swr';
|
||||
import { LoadingOverlay } from '../join/loadingOverlay';
|
||||
import { generateMermaidString, dataFromBE } from './helpers';
|
||||
|
||||
const SequenceDiagram = () => {
|
||||
|
||||
const {
|
||||
data: eventMessagesData,
|
||||
isLoading: loadingEventMessages,
|
||||
swrKey: eventMessagesKeyFunc,
|
||||
} = useGetAllEventmessages();
|
||||
|
||||
const mermaidRef: any = useRef(null);
|
||||
const [scale, setScale] = useState(1);
|
||||
const hasData = eventMessagesData?.data && eventMessagesData?.data.length > 0;
|
||||
|
||||
const mermaidString = generateMermaidString(eventMessagesData?.data);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!loadingEventMessages && hasData)
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
sequence: {
|
||||
mirrorActors: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (mermaidRef.current) {
|
||||
mermaidRef.current.innerHTML = mermaidString;
|
||||
mermaid.init(undefined, mermaidRef.current);
|
||||
}
|
||||
}, [loadingEventMessages, hasData, mermaidString]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mermaidRef.current) {
|
||||
const svg = mermaidRef.current.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${scale})`;
|
||||
svg.style.transformOrigin = 'top left'; // Set transform origin to top left
|
||||
mermaidRef.current.style.width = `${svg.getBoundingClientRect().width * scale}px`;
|
||||
mermaidRef.current.style.height = `${svg.getBoundingClientRect().height * scale}px`;
|
||||
}
|
||||
}
|
||||
}, [scale]);
|
||||
|
||||
|
||||
const onRefresh = () => {
|
||||
const eventMessagesKey =
|
||||
typeof eventMessagesKeyFunc === "function"
|
||||
? eventMessagesKeyFunc()
|
||||
: eventMessagesKeyFunc;
|
||||
|
||||
if (eventMessagesKey) {
|
||||
mutate(eventMessagesKey);
|
||||
}
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
setScale((scale) => scale * 1.1);
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
setScale((scale) => scale / 1.1);
|
||||
};
|
||||
|
||||
const resetZoom = () => {
|
||||
setScale(1);
|
||||
};
|
||||
|
||||
const viewInFullScreen = () => {
|
||||
if (mermaidRef.current) {
|
||||
const svg = mermaidRef.current.querySelector('svg');
|
||||
const serializer = new XMLSerializer();
|
||||
const svgBlob = new Blob([serializer.serializeToString(svg)], {
|
||||
type: 'image/svg+xml',
|
||||
});
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAsPng = () => {
|
||||
if (mermaidRef.current) {
|
||||
const svg = mermaidRef.current.querySelector('svg');
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
// Create a canvas element to convert SVG to PNG
|
||||
const canvas = document.createElement('canvas');
|
||||
const svgSize = svg.getBoundingClientRect();
|
||||
canvas.width = svgSize.width;
|
||||
canvas.height = svgSize.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const img = document.createElement('img');
|
||||
|
||||
img.onload = () => {
|
||||
ctx?.drawImage(img, 0, 0);
|
||||
const pngData = canvas.toDataURL('image/png');
|
||||
|
||||
// Trigger download
|
||||
const link = document.createElement('a');
|
||||
link.download = 'sequence-diagram.png';
|
||||
link.href = pngData;
|
||||
link.click();
|
||||
};
|
||||
|
||||
img.src =
|
||||
'data:image/svg+xml;base64,' +
|
||||
btoa(unescape(encodeURIComponent(svgData)));
|
||||
}
|
||||
};
|
||||
|
||||
if (loadingEventMessages)
|
||||
return <LoadingOverlay title="Loading Diagram" subtitle="Please wait..." />
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end w-full">
|
||||
{
|
||||
hasData ? <>
|
||||
<div className='flex justify-end gap-2.5 mb-5 w-full'>
|
||||
<Tooltip placement='top' title='Refresh Diagram'>
|
||||
<IconButton color="default" onClick={onRefresh}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Zoom In" placement='top'>
|
||||
<IconButton color="primary" onClick={zoomIn}>
|
||||
<ZoomInIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Zoom Out" placement='top'>
|
||||
<IconButton color="primary" onClick={zoomOut}>
|
||||
<ZoomOutIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reset" placement='top'>
|
||||
<IconButton color="primary" onClick={resetZoom}>
|
||||
<ResetIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="View in Fullscreen" placement='top'>
|
||||
<IconButton color="primary" onClick={viewInFullScreen}>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Download as PNG" placement='top'>
|
||||
<IconButton color="primary" onClick={downloadAsPng}>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="w-full h-500 overflow-auto p-2.5 box-border h-full">
|
||||
<div className='mermaid' ref={mermaidRef}></div>
|
||||
</div>
|
||||
</> : <div className="flex justify-center items-center w-full h-500">
|
||||
<NoDataOverlay label="No Activity yet" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SequenceDiagram;
|
||||
21
pkgs/ui/src/components/sequence_diagram/style.css
Normal file
21
pkgs/ui/src/components/sequence_diagram/style.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.diagram-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
overflow: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -49,8 +49,8 @@ const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 700 }} aria-label="customized table">
|
||||
<TableContainer component={Paper} style={{ maxHeight: 350 }}>
|
||||
<Table stickyHeader sx={{ minWidth: 700 }} aria-label="customized table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{configuration.map((header: CustomTableConfiguration) => (
|
||||
|
||||
Reference in New Issue
Block a user