{#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(`
  • $\{topic}
  • `) }); mermaidGraphDefinition.details.topicSinks.sort().forEach(topic => { $('#topic-sinks-list').append(`
  • $\{topic}
  • `) }); mermaidGraphDefinition.details.stateStores.sort().forEach(store => { $('#state-stores-list').append(`
  • $\{store}
  • `) }); $("#topology-graph-visualization-toggle, #topology-description-visualization-toggle").click(() => { $("#topology-description-wrapper").toggleClass("d-none"); $("#topology-description-visualization-toggle").toggleClass("btn-primary"); $("#topology-description-visualization-toggle").toggleClass("btn-outline-primary"); $("#topology-graph-wrapper").toggleClass("d-none"); $("#topology-graph-visualization-toggle").toggleClass("btn-outline-primary"); $("#topology-graph-visualization-toggle").toggleClass("btn-primary"); }); $("#topology-copy-button").click(() => { var isDescriptionVisible = !$("#topology-description-wrapper").hasClass("d-none"); if (isDescriptionVisible) { navigator.clipboard.writeText($('#topology-description').val().trim() + '\n'); } else { canvas.toBlob(blob => { navigator.clipboard.write([new ClipboardItem({ ['image/png']: blob })]); }) } }); $("#topology-copy-button").popover({ trigger:"manual" }).click(function() { var pop = $(this); pop.popover("show") pop.on('shown.bs.popover',() => setTimeout(() => pop.popover("hide"), 1000)); }); $("#topology-download-button").click(() => { var downloadLink = document.createElement('a'); var isDescriptionVisible = !$("#topology-description-wrapper").hasClass("d-none"); if (isDescriptionVisible) { var description= $('#topology-description').val().trim() + '\n'; downloadLink.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(description); downloadLink.download = 'topology.txt'; } else { downloadLink.href = canvas.toDataURL('image/png'); downloadLink.download = 'topology.png'; } downloadLink.style.display = 'none'; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); }); }); {/script} {#title}Topology{/title} {#body}
    {#if info:topology}

    Details

    Description

    {#else} {#topologyNotFound /} {/if}
    {/body} {/include}