From 8a12c37d2495196a3faf8f40d670e2a79b1601b8 Mon Sep 17 00:00:00 2001 From: Sadallah Date: Fri, 29 May 2026 20:09:20 +0200 Subject: [PATCH 1/2] feat(webapp): add draw.io support and embed integration --- webapp/README.md | 8 ++- webapp/backend/Dockerfile | 4 ++ webapp/backend/constants.py | 5 +- webapp/backend/requirements.txt | 32 +++++---- webapp/backend/services/helmService.py | 40 ++++++++++++ webapp/backend/services/helmfileService.py | 22 ++++++- webapp/backend/services/manifestService.py | 45 +++++++++++++ webapp/backend/utils/validators.py | 2 +- .../src/components/common/DiagramViewer.jsx | 65 ++++++++++++++++++- .../src/components/common/DownloadButton.jsx | 9 +-- .../src/hooks/useDiagramGeneration.js | 5 +- webapp/frontend/src/utils/constants.js | 4 ++ webapp/frontend/src/utils/tooltipContent.jsx | 1 + 13 files changed, 214 insertions(+), 28 deletions(-) diff --git a/webapp/README.md b/webapp/README.md index f0d16f8..b7868b1 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -1,3 +1,5 @@ +## Project Structure +Web Interface for generating Kubernetes diagrams from manifests, Helm charts, or Helmfile files using Kubediagrams. # KubeDiagrams Web App A modern web application for generating Kubernetes architecture diagrams from manifests, Helm charts, or Helmfile configurations using [KubeDiagrams](https://github.com/philippemerle/KubeDiagrams). @@ -70,9 +72,9 @@ The following command-line tools must be installed and available in your PATH: webapp/ ├── backend/ # Python/Flask backend │ ├── routes/ # Flask route handlers -│ │ ├── manifest.py # Manifest diagram generation endpoint -│ │ ├── helm.py # Helm chart diagram endpoint -│ │ ├── helmfile.py # Helmfile diagram endpoint +│ │ ├── manifest.py # Manifest diagram generation endpoints +│ │ ├── helm.py # Helm chart diagram endpoints +│ │ ├── helmfile.py # Helmfile diagram endpoints │ │ └── submit.py # Feedback submission endpoint │ ├── services/ # Business logic layer │ │ ├── manifestService.py # Manifest processing service diff --git a/webapp/backend/Dockerfile b/webapp/backend/Dockerfile index e425d8e..82d3e0a 100644 --- a/webapp/backend/Dockerfile +++ b/webapp/backend/Dockerfile @@ -14,9 +14,13 @@ FROM docker.io/python:3.13-alpine AS base # Install system dependencies RUN apk update && apk add --no-cache \ graphviz \ + graphviz-dev \ bash \ ttf-freefont \ curl \ + git \ + gcc \ + musl-dev \ && rm -rf /var/cache/apk/* # Copy Helm from first stage diff --git a/webapp/backend/constants.py b/webapp/backend/constants.py index 7308172..4e097a0 100644 --- a/webapp/backend/constants.py +++ b/webapp/backend/constants.py @@ -10,10 +10,11 @@ MIME_TYPES = { "svg": "image/svg+xml", "pdf": "application/pdf", "dot": "text/vnd.graphviz", - "dot_json": "application/json" + "dot_json": "application/json", + "drawio": "application/xml" } # no binary format -TEXT_FORMATS = {"svg", "dot", "dot_json"} +TEXT_FORMATS = {"svg", "dot", "dot_json", "drawio"} # Manifest_detector MANIFEST_RE = re.compile(r'^\s*apiVersion\s*:\s*.+$', re.MULTILINE) KIND_RE = re.compile(r'^\s*kind\s*:\s*.+$', re.MULTILINE) diff --git a/webapp/backend/requirements.txt b/webapp/backend/requirements.txt index 0d0be32..7861641 100644 --- a/webapp/backend/requirements.txt +++ b/webapp/backend/requirements.txt @@ -1,20 +1,26 @@ blinker==1.9.0 -cfgv==3.4.0 +cfgv==3.5.0 click==8.2.1 -diagrams==0.24.4 -filelock==3.20.3 -Flask==3.1.3 +diagrams==0.25.1 +distlib==0.4.0 +filelock==3.25.2 +Flask==3.1.2 flask-cors==6.0.1 graphviz==0.20.3 +graphviz2drawio==1.1.0 gunicorn==23.0.0 -identify==2.6.14 +identify==2.6.17 itsdangerous==2.2.0 Jinja2==3.1.6 -KubeDiagrams==0.6.0 -MarkupSafe==3.0.2 -nodeenv==1.9.1 -platformdirs==4.4.0 -pre_commit==4.3.0 -PyYAML==6.0.2 -virtualenv==20.36.1 -Werkzeug==3.1.6 +KubeDiagrams @ git+https://github.com/philippemerle/KubeDiagrams.git@main +MarkupSafe==3.0.3 +nodeenv==1.10.0 +platformdirs==4.9.4 +pre_commit==4.5.1 +puremagic==2.0.2 +pygraphviz==1.14 +python-discovery==1.1.3 +PyYAML==6.0.3 +svg.path==7.0 +virtualenv==21.2.0 +Werkzeug==3.1.3 diff --git a/webapp/backend/services/helmService.py b/webapp/backend/services/helmService.py index e6b145e..f752a4b 100644 --- a/webapp/backend/services/helmService.py +++ b/webapp/backend/services/helmService.py @@ -3,6 +3,8 @@ import subprocess import os from urllib.parse import urlparse +from graphviz2drawio.graphviz2drawio import convert as drawio_convert + from constants import MIME_TYPES from .models import DiagramResult from .file_manager import FileManager @@ -32,6 +34,44 @@ def generate_from_helm( if chart_url.startswith('oci://'): base_name = chart_url.rstrip('/').split('/')[-1] + # draw.io: generate .dot first, then convert via graphviz2drawio + if output_format == "drawio": + dot_output = os.path.abspath(f"{base_name}.dot") + try: + cmd = ["helm-diagrams", chart_url, "-o", f"{base_name}.dot"] + if extra_args.strip(): + cmd.extend(parse_extra_args(extra_args)) + proc = subprocess.run(cmd, check=False, capture_output=True, text=True) + stdout_output = proc.stdout or "" + stderr_output = proc.stderr or "" + has_error = proc.returncode != 0 or has_fatal_error(stdout_output, stderr_output) + if "Error:" in stderr_output or "execution error" in stderr_output.lower(): + has_error = True + if has_error: + FileManager.cleanup_files(dot_output) + return DiagramResult( + success=False, error="helm-diagrams failed to generate the diagram.", + command=" ".join(cmd), stdout=stdout_output, stderr=stderr_output + ) + drawio_xml = drawio_convert(dot_output) + FileManager.cleanup_files(dot_output) + return DiagramResult( + success=True, + diagram=drawio_xml, + mime_type=MIME_TYPES.get("drawio", "application/xml"), + filename=f"{base_name}.drawio", + message="Helm diagram successfully generated.", + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + except Exception as e: + FileManager.cleanup_files(dot_output) + return DiagramResult( + success=False, error=f"draw.io conversion failed: {e}", + command=" ".join(cmd) if 'cmd' in locals() else None + ) + requested_output = os.path.abspath(f"{base_name}.{output_format}") png_output = os.path.abspath(f"{base_name}.png") diff --git a/webapp/backend/services/helmfileService.py b/webapp/backend/services/helmfileService.py index ffe2a4a..4cc3019 100644 --- a/webapp/backend/services/helmfileService.py +++ b/webapp/backend/services/helmfileService.py @@ -2,6 +2,8 @@ import subprocess import os +from graphviz2drawio.graphviz2drawio import convert as drawio_convert + from constants import MIME_TYPES from .models import DiagramResult from .file_manager import FileManager @@ -26,8 +28,10 @@ def generate_from_helmfile( DiagramResult: Result of the generation """ with FileManager.create_temp_file(helmfile_content, suffix=".yaml", mode='wb') as temp_helmfile_path: - output_path = temp_helmfile_path + f".{output_format}" - + is_drawio = output_format == "drawio" + actual_format = "dot" if is_drawio else output_format + output_path = temp_helmfile_path + f".{actual_format}" + try: # Command helmfile template template_cmd = ["helmfile", "template", "-f", temp_helmfile_path] @@ -87,6 +91,20 @@ def generate_from_helmfile( stderr=stderr_output ) + if is_drawio: + drawio_xml = drawio_convert(output_path) + FileManager.cleanup_files(output_path) + return DiagramResult( + success=True, + diagram=drawio_xml, + mime_type=MIME_TYPES.get("drawio", "application/xml"), + filename="helmfile-diagram.drawio", + message="Helmfile diagram successfully generated.", + command=f"{' '.join(template_cmd)} | {' '.join(cmd)}", + stdout=stdout_output, + stderr=stderr_output + ) + content = FileManager.read_file_content(output_path, binary=True) encoded = encode_content(content, output_format) diff --git a/webapp/backend/services/manifestService.py b/webapp/backend/services/manifestService.py index 17400c4..79a4f19 100644 --- a/webapp/backend/services/manifestService.py +++ b/webapp/backend/services/manifestService.py @@ -1,6 +1,8 @@ """Service for generating diagrams from Kubernetes manifests.""" import subprocess +from graphviz2drawio.graphviz2drawio import convert as drawio_convert + from constants import MIME_TYPES from .models import DiagramResult from .file_manager import FileManager @@ -26,6 +28,49 @@ def generate_from_manifest( """ with FileManager.create_temp_file(manifest_content, suffix='.yaml') as tmp_manifest: base_name = FileManager.get_base_name_from_path(tmp_manifest) + + # draw.io: generate .dot first, then convert via graphviz2drawio + if output_format == "drawio": + dot_output, _ = FileManager.get_output_paths(tmp_manifest, "dot") + drawio_output, _ = FileManager.get_output_paths(tmp_manifest, "drawio") + try: + cmd = ["kube-diagrams", tmp_manifest, "-o", dot_output] + if without_namespace: + cmd.append("--without-namespace") + if extra_args.strip(): + cmd.extend(parse_extra_args(extra_args)) + proc = subprocess.run(cmd, check=False, capture_output=True, text=True) + stdout_output = proc.stdout or "" + stderr_output = proc.stderr or "" + if proc.returncode != 0 or has_fatal_error(stdout_output, stderr_output): + FileManager.cleanup_files(dot_output) + return DiagramResult( + success=False, + error="KubeDiagrams failed. See command output below.", + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + drawio_xml = drawio_convert(dot_output) + FileManager.cleanup_files(dot_output) + return DiagramResult( + success=True, + diagram=drawio_xml, + mime_type=MIME_TYPES.get("drawio", "application/xml"), + filename=f"{base_name}.drawio", + message="Diagram successfully generated.", + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + except Exception as e: + FileManager.cleanup_files(dot_output) + return DiagramResult( + success=False, + error=f"draw.io conversion failed: {e}", + command=" ".join(cmd) if 'cmd' in locals() else None + ) + requested_output, png_output = FileManager.get_output_paths(tmp_manifest, output_format) try: diff --git a/webapp/backend/utils/validators.py b/webapp/backend/utils/validators.py index 6ac474e..ee6e96e 100644 --- a/webapp/backend/utils/validators.py +++ b/webapp/backend/utils/validators.py @@ -12,7 +12,7 @@ class ValidationError(Exception): class InputValidator: """Validator for user inputs.""" - SUPPORTED_FORMATS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'pdf', 'dot', 'dot_json'] + SUPPORTED_FORMATS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'pdf', 'dot', 'dot_json', 'drawio'] # Valid Pattern for url HELM_URL_PATTERN = re.compile( diff --git a/webapp/frontend/src/components/common/DiagramViewer.jsx b/webapp/frontend/src/components/common/DiagramViewer.jsx index 2aec112..94b543d 100644 --- a/webapp/frontend/src/components/common/DiagramViewer.jsx +++ b/webapp/frontend/src/components/common/DiagramViewer.jsx @@ -1,14 +1,72 @@ /** * DiagramViewer Component * Universal component for rendering diagrams in all supported formats - * Supports: DOT_JSON (interactive), PDF, DOT (code), SVG, PNG, JPG + * Supports: DOT_JSON (interactive), PDF, DOT (code), SVG, PNG, JPG, DRAWIO */ +import { useRef, useEffect } from 'react'; import { Code2, Copy } from 'lucide-react'; import PanZoomContainer from './PanZoomContainer.jsx'; import LoadingSpinner from './LoadingSpinner.jsx'; import { OUTPUT_FORMATS } from '../../utils/constants.js'; +/** + * Embedded draw.io viewer using embed.diagrams.net with postMessage protocol. + * When the iframe signals {event: "init"}, we send {action: "load", xml: content}. + */ +function DrawioViewer({ content }) { + const iframeRef = useRef(null); + const contentRef = useRef(content); + + // Keep ref in sync so the message handler always reads the latest content + useEffect(() => { + contentRef.current = content; + }, [content]); + + // Listen for draw.io init event and send the XML content + useEffect(() => { + const handleMessage = (event) => { + if (!event.origin.includes('diagrams.net')) return; + try { + const data = JSON.parse(event.data); + if (data.event === 'init') { + iframeRef.current?.contentWindow?.postMessage( + JSON.stringify({ action: 'load', xml: contentRef.current, fit: 1 }), + 'https://embed.diagrams.net' + ); + } + } catch { + // ignore JSON parse errors from unrelated messages + } + }; + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + + // When content changes on an already-loaded iframe, push the new diagram + useEffect(() => { + const iframe = iframeRef.current; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage( + JSON.stringify({ action: 'load', xml: content, fit: 1 }), + 'https://embed.diagrams.net' + ); + } + }, [content]); + + return ( +
+