diff --git a/webapp/README.md b/webapp/README.md index f0d16f8..1a1b672 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 @@ -430,18 +432,18 @@ python3 app.py # Start Flask server in debug mode ### Technologies Used **Backend**: -- Flask 3.1.2 - Web framework -- Gunicorn 23.0.0 - WSGI server -- KubeDiagrams 0.6.0 - Diagram generation -- PyYAML 6.0.2 - YAML parsing +- Flask 3.1.3 - Web framework +- Gunicorn 26.0.0 - WSGI server +- KubeDiagrams (main) - Diagram generation +- PyYAML 6.0.3 - YAML parsing +- Werkzeug 3.1.6 - WSGI utilities **Frontend**: - React 19.1.0 - UI framework - Vite 6.3.5 - Build tool - TailwindCSS 4.1.6 - Styling -- Lucide React - Icons -- Motion - Animations - +- Lucide React 0.552.0 - Icons +- Motion 12.11.4 - Animations --- ## Contributing 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..93b9ef2 100644 --- a/webapp/backend/requirements.txt +++ b/webapp/backend/requirements.txt @@ -1,20 +1,25 @@ blinker==1.9.0 -cfgv==3.4.0 +cfgv==3.5.0 click==8.2.1 -diagrams==0.24.4 -filelock==3.20.3 +diagrams==0.25.1 +distlib==0.4.0 +filelock==3.25.2 Flask==3.1.3 flask-cors==6.0.1 graphviz==0.20.3 -gunicorn==23.0.0 -identify==2.6.14 +gunicorn==26.0.0 +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 +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.6 diff --git a/webapp/backend/services/helmService.py b/webapp/backend/services/helmService.py index e6b145e..26c65a7 100644 --- a/webapp/backend/services/helmService.py +++ b/webapp/backend/services/helmService.py @@ -8,6 +8,7 @@ from .models import DiagramResult from .file_manager import FileManager from .utils import parse_extra_args, has_fatal_error, encode_content + def generate_from_helm( chart_url: str, output_format: str = "png", @@ -27,7 +28,7 @@ def generate_from_helm( # Extraction du nom de base parsed = urlparse(chart_url) base_name = os.path.basename(parsed.path).replace(".tgz", "").replace(".tar.gz", "") - + # Pour les URLs OCI if chart_url.startswith('oci://'): base_name = chart_url.rstrip('/').split('/')[-1] @@ -133,3 +134,4 @@ def generate_from_helm( error=f"Internal error: {e}", command=" ".join(cmd) if 'cmd' in locals() else None ) + diff --git a/webapp/backend/services/helmfileService.py b/webapp/backend/services/helmfileService.py index ffe2a4a..f3790e8 100644 --- a/webapp/backend/services/helmfileService.py +++ b/webapp/backend/services/helmfileService.py @@ -7,6 +7,7 @@ from .models import DiagramResult from .file_manager import FileManager from .utils import parse_extra_args, has_fatal_error, encode_content + def generate_from_helmfile( helmfile_content: str, output_format: str = "png", @@ -27,7 +28,7 @@ def generate_from_helmfile( """ with FileManager.create_temp_file(helmfile_content, suffix=".yaml", mode='wb') as temp_helmfile_path: output_path = temp_helmfile_path + f".{output_format}" - + try: # Command helmfile template template_cmd = ["helmfile", "template", "-f", temp_helmfile_path] diff --git a/webapp/backend/services/manifestService.py b/webapp/backend/services/manifestService.py index 17400c4..2b8f4d5 100644 --- a/webapp/backend/services/manifestService.py +++ b/webapp/backend/services/manifestService.py @@ -6,6 +6,7 @@ from .models import DiagramResult from .file_manager import FileManager from .utils import parse_extra_args, has_fatal_error, encode_content + def generate_from_manifest( manifest_content: str, output_format: str = "png", @@ -26,8 +27,9 @@ 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) + requested_output, png_output = FileManager.get_output_paths(tmp_manifest, output_format) - + try: # Command cmd = ["kube-diagrams", tmp_manifest, "-o", requested_output] @@ -95,3 +97,4 @@ def generate_from_manifest( error=f"Internal error: {e}", command=" ".join(cmd) if 'cmd' in locals() else None ) + 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 ( +