Merge pull request 'Added Sequence Diagram with Mermaid' (#51) from mermaid into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m13s
assets1 / test (push) Has been cancelled

Reviewed-on: #51
This commit was merged in pull request #51.
This commit is contained in:
Sara Pervana
2024-01-16 22:14:46 +01:00
17 changed files with 3924 additions and 65 deletions

View File

@@ -1,10 +1,19 @@
{
"root": true,
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:@typescript-eslint/recommended"],
"extends": [
"next/core-web-vitals",
"plugin:tailwindcss/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["**/src/api/*"],
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": [
"**/src/api/*"
],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"tailwindcss/no-custom-classname": "off"
}
}
}

1
pkgs/ui/_document.js Normal file
View File

@@ -0,0 +1 @@
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>;

File diff suppressed because it is too large Load Diff

1067
pkgs/ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@
"axios": "^1.4.0",
"classnames": "^2.3.2",
"hex-rgb": "^5.0.0",
"mermaid": "^10.6.1",
"next": "13.4.12",
"postcss": "8.4.27",
"pretty-bytes": "^6.1.1",

View File

@@ -154,6 +154,8 @@ export default function Client({
setSnackbarOpen(false);
};
console.log("entity", entity);
if (services_loading) return <Skeleton height={500} />;
if (!services) return <Alert severity="error">Client not found</Alert>;
@@ -193,7 +195,7 @@ export default function Client({
IP: <code>{entity?.ip}</code>
</Typography>
<Typography color="text.primary" gutterBottom>
Network: <code>{entity?.other?.network}</code>
Network: <code>{entity?.network}</code>
</Typography>
</CardContent>
</Card>

View File

@@ -3,18 +3,26 @@
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 +49,7 @@ export default function DLG() {
<h4>DID Resolution View</h4>
<CustomTable
loading={loadingResolutions}
data={resolutionData}
data={resolutionData?.data}
configuration={DLGResolutionTableConfig}
tkey="resolution_table"
/>

View File

@@ -1,13 +1,18 @@
"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 +56,7 @@ export default function Home() {
<div>
<h4>Sequence Diagram</h4>
<NoDataOverlay label="No Activity yet" />
<NoSSRSequenceDiagram />
</div>
</div>
);

View File

@@ -6,6 +6,7 @@ import {
CssBaseline,
IconButton,
ThemeProvider,
Tooltip,
useMediaQuery,
} from "@mui/material";
import { StyledEngineProvider } from "@mui/material/styles";
@@ -50,6 +51,13 @@ 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 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}>
@@ -77,13 +85,15 @@ export default function RootLayout({
>
<div className="grid grid-cols-3">
<div className="col-span-1">
<IconButton
style={{ padding: "12px" }}
hidden={true}
onClick={() => setShowSidebar((c) => !c)}
>
{!showSidebar && <MenuIcon />}
</IconButton>
<Tooltip placement="right" title="Expand Sidebar">
<IconButton
style={{ padding: "12px" }}
hidden={true}
onClick={() => setShowSidebar((c) => !c)}
>
{!showSidebar && <MenuIcon />}
</IconButton>
</Tooltip>
</div>
</div>

View File

@@ -0,0 +1,136 @@
import { Eventmessage } from "@/api/model";
export const generateMermaidString = (data: Eventmessage[] | undefined) => {
if (!data || data.length === 0) return "";
const participants = Array.from(
new Set(data.flatMap((item) => [item.src_did, item.des_did])),
);
let mermaidString = "sequenceDiagram\n";
participants.forEach((participant, index) => {
mermaidString += ` participant ${String.fromCharCode(
65 + index,
)} as ${participant}\n`;
});
let currentGroupId: number | null = null;
data.forEach((item, index) => {
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();
const message = item.msg.text || `Event message ${index + 1}`;
if (item.group_id !== currentGroupId) {
if (currentGroupId !== null) {
mermaidString += ` end\n`;
}
mermaidString += ` alt Group ${item.group_id}\n`;
currentGroupId = item.group_id;
}
mermaidString += ` ${srcParticipant}->>${desParticipant}: [${timestamp}] ${message}\n`;
});
if (currentGroupId !== null) {
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: {},
},
];

View File

@@ -0,0 +1,181 @@
"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 } 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";
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">
{hasData ? (
<>
<div className="flex justify-end">
<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 p-2.5">
<div className="mermaid" ref={mermaidRef}></div>
</div>
</>
) : (
<div className="flex w-full justify-center">
<NoDataOverlay label="No Activity yet" />
</div>
)}
</div>
);
};
export default SequenceDiagram;

View File

@@ -6,6 +6,7 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
Tooltip,
useMediaQuery,
} from "@mui/material";
import Image from "next/image";
@@ -137,9 +138,14 @@ export function Sidebar(props: SidebarProps) {
/>
</div>
<div className="lg:absolute lg:right-0 lg:top-0">
<IconButton size="large" className="text-white" onClick={onClose}>
<ChevronLeftIcon fontSize="inherit" />
</IconButton>
<Tooltip
placement="right"
title={collapseMenuOpen ? "Close Sidebar" : "Expand Sidebar"}
>
<IconButton size="large" className="text-white" onClick={onClose}>
<ChevronLeftIcon fontSize="inherit" />
</IconButton>
</Tooltip>
</div>
</div>
<Divider

View File

@@ -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) => (

View File

@@ -59,22 +59,31 @@ export const APServiceRepositoryTableConfig = [
{
key: "status",
label: "Status",
},
{
key: "other",
label: "Type",
render: (value: any) => {
let renderedValue: any = "";
if (typeof value === "object") {
const label = Object.keys(value)[0];
const info = value[label];
renderedValue = (
<code>
{label} {info}
</code>
);
if (Array.isArray(value.data)) {
renderedValue = value.data.join(", ");
} else {
console.error("Status is not an array", value);
}
return renderedValue;
},
},
// {
// key: "other",
// label: "Type",
// render: (value: any) => {
// let renderedValue: any = "";
// if (typeof value === "object") {
// const label = Object.keys(value)[0];
// const info = value[label];
// renderedValue = (
// <code>
// {label} {info}
// </code>
// );
// }
// return renderedValue;
// },
// },
];

View File

@@ -1,4 +1,7 @@
import { Button } from "@mui/material";
import { Button, IconButton, Tooltip } from "@mui/material";
import AddCircleIcon from "@mui/icons-material/AddCircle";
import RemoveCircleIcon from "@mui/icons-material/RemoveCircle";
import DeleteIcon from "@mui/icons-material/Delete";
export const ClientTableConfig = [
{
@@ -51,6 +54,24 @@ export const ServiceTableConfig = [
key: "entity_did",
label: "Entity DID",
},
{
key: "usage",
label: "Usage",
render: (value: any) => {
let renderedValue = "";
if (value.length > 0) {
renderedValue = value.map((item: any, index: number) => {
return (
<div key={index}>
{item.consumer_entity_did} ({item.times_consumed})
</div>
);
});
}
return renderedValue;
},
},
{
key: "status",
label: "Status",
@@ -66,21 +87,40 @@ export const ServiceTableConfig = [
},
{
key: "action",
label: "Action",
render: (value: any) => {
let renderedValue: any = "";
console.log("value", value.data);
if (typeof value === "object")
renderedValue = (
<>
{value.data.map((actionType: any) => (
<>
<Button>{actionType.name}</Button>
</>
))}
</>
);
return renderedValue;
label: "Actions",
render: () => {
return (
<>
<Tooltip title="Register" placement="top">
<IconButton disabled size="small">
<AddCircleIcon />
</IconButton>
</Tooltip>
<Tooltip title="De-register" placement="top">
<IconButton disabled size="small">
<RemoveCircleIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete" placement="top">
<IconButton disabled size="small" color="secondary">
<DeleteIcon />
</IconButton>
</Tooltip>
</>
);
// let renderedValue: any = "";
// if (typeof value === "object")
// renderedValue = (
// <>
// {[...value.data, { name: 'Delete', endpoint: '' }].map((actionType: any) => (
// <>
// <Button disabled style={{ marginRight: 8 }} variant="outlined" size="small">{actionType.name}</Button>
// </>
// ))}
// </>
// );
// return renderedValue;
},
},
];

View File

@@ -10,10 +10,6 @@ export const HomeTableConfig = [
{
key: "network",
label: "Network",
render: (value: any) => {
const renderedValue = typeof value === "object" ? value?.network : "-";
return renderedValue;
},
},
{
key: "ip",
@@ -22,11 +18,6 @@ export const HomeTableConfig = [
{
key: "roles",
label: "Roles",
render: (value: any) => {
const renderedValue =
typeof value === "object" ? value?.roles?.join(", ") : "-";
return renderedValue;
},
},
{
key: "attached",

View File

@@ -30,6 +30,7 @@ module.exports = {
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
safelist: ["mermaid"],
important: "#__next",
theme: {
colors: {