{#include main fluid=true}
{#style}
#kafka-streams-topology svg {
height: 100%;
}
#topology-description {
resize: none;
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
}
#kafka-streams-topology .no-outline {
box-shadow: none !important;
outline: 0;
}
#kafka-streams-topology .caret {
border: solid #000;
border-width: 0 2px 2px 0;
display: inline;
cursor: pointer;
padding: 3px;
position: absolute;
right: 0;
margin-top: 10px;
}
#kafka-streams-topology .list-group-item .collapsed .caret {
transform: rotate(40deg);
-webkit-transform: rotate(40deg);
transition: .3s transform ease-in-out;
}
#kafka-streams-topology .list-group-item .caret {
transform: rotate(-140deg);
-webkit-transform: rotate(-140deg);
transition: .3s transform ease-in-out;
}
#kafka-streams-topology .w-15 {
width: 15%!important;
}
{/style}
{#scriptref}
{/scriptref}
{#script}
function toMermaid(topology) {
var lines = topology.split('\n');
var subTopologies = [];
var outside = [];
var currentGraphNodeName;
var subTopologiesList = [];
var topicSourcesList = [];
var topicSinksList = [];
var stateStoresList = [];
var name = (value) => value.replaceAll("-", "-
");
var subTopology = {
pattern: /Sub-topology: ([0-9]*)/,
startFormatter: (subTopology) => `subgraph Sub-Topology: $\{subTopology}`,
endFormatter: () => `end`,
visit: function(line) {
var match = line.match(this.pattern);
// Close the previous sub-topology before opening a new one;
if(subTopologies.length) {
subTopologies.push(this.endFormatter());
}
subTopologies.push(this.startFormatter(match[1]));
subTopologiesList.push(match[1]);
}
}
var source = {
pattern: /Source:\s+(\S+)\s+\(topics:\s+\[(.*)\]\)/,
formatter: (source, topic) => `$\{topic}[$\{topic}] --> $\{source}($\{name(source)})`,
visit: function(line) {
var match = line.match(this.pattern);
currentGraphNodeName = match[1].trim();
var topics = match[2]
topics.split(',').filter(String).map(topic => topic.trim()).forEach(topic => {
outside.push(this.formatter(currentGraphNodeName, topic));
topicSourcesList.push(topic);
});
}
};
var processor = {
pattern: /Processor:\s+(\S+)\s+\(stores:\s+\[(.*)\]\)/,
formatter: (processor, store) => (processor.includes("JOIN")) ? `$\{store}[($\{name(store)})] --> $\{processor}($\{name(processor)})` : `$\{processor}($\{name(processor)}) --> $\{store}[($\{name(store)})]`,
visit: function(line) {
var match = line.match(this.pattern);
currentGraphNodeName = match[1].trim();
var stores = match[2];
stores.split(',').filter(String).map(store => store.trim()).forEach(store => {
outside.push(this.formatter(currentGraphNodeName, store));
stateStoresList.push(store);
});
}
};
var sink = {
pattern: /Sink:\s+(\S+)\s+\(topic:\s+(.*)\)/,
formatter: (sink, topic) => `$\{sink}($\{name(sink)}) --> $\{topic}[$\{topic}]`,
visit: function(line) {
var match = line.match(this.pattern);
currentGraphNodeName = match[1].trim();
var topic = match[2].trim();
outside.push(this.formatter(currentGraphNodeName, topic));
topicSinksList.push(topic);
}
}
var rightArrow = {
pattern: /\s*-->\s+(.*)/,
formatter: (src, dst) => `$\{src}($\{name(src)}) --> $\{dst}($\{name(dst)})`,
visit: function(line) {
var match = line.match(this.pattern);
match[1].split(',').filter(String).map(target => target.trim()).filter(target => target !== "none").forEach(target => {
subTopologies.push(this.formatter(currentGraphNodeName, target))
});
}
};
for(const line of lines) {
switch(true) {
case subTopology.pattern.test(line):
subTopology.visit(line);
break;
case source.pattern.test(line):
source.visit(line);
break;
case processor.pattern.test(line):
processor.visit(line);
break;
case sink.pattern.test(line):
sink.visit(line);
break;
case rightArrow.pattern.test(line):
rightArrow.visit(line);
break;
default:
break;
}
}
if(subTopologies.length) {
subTopologies.push(subTopology.endFormatter());
}
var description = ["graph TD"].concat(outside).concat(subTopologies).concat(topicSourcesList).concat(topicSinksList).concat(stateStoresList).join('\n');
return {
description: description,
details: {
subTopologies: subTopologiesList,
topicSources: topicSourcesList,
topicSinks: topicSinksList,
stateStores: stateStoresList
}
};
}
function svgToCanvas(svgCode) {
var canvas = document.createElement('canvas');
try {
var ctx = canvas.getContext("2d");
var img = new Image();
img.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svgCode)));
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
};
} catch (err) {
console.error('Failed to generate canvas')
}
return canvas;
}
mermaid.initialize(\{startOnLoad:false});
$(function(){
var canvas = document.createElement("canvas");
var topologyDescription = $('#topology-description').val();
var mermaidGraphDefinition = toMermaid(topologyDescription);
console.log(mermaidGraphDefinition.description);
var id = "mermaid-graph-" + Date.now();
mermaid.mermaidAPI.render(id, mermaidGraphDefinition.description, (svgCode, bindFunctions) => {
canvas = svgToCanvas(svgCode);
$('#topology-graph-wrapper').html(svgCode);
$(`#$\{id}`).addClass('mx-auto d-block'); // center the graph
});
$('#sub-topologies-details').html(mermaidGraphDefinition.details.subTopologies.length);
$('#topic-sources-details').text(mermaidGraphDefinition.details.topicSources.length);
$('#topic-sinks-details').text(mermaidGraphDefinition.details.topicSinks.length);
$('#state-stores-details').text(mermaidGraphDefinition.details.stateStores.length);
mermaidGraphDefinition.details.topicSources.sort().forEach(topic => {
$('#topic-sources-list').append(`