Merge pull request #73 from Sandor59100/main

feat(webapp): add draw.io support and embed integration
This commit is contained in:
Philippe Merle
2026-06-01 11:42:04 +02:00
committed by GitHub
13 changed files with 123 additions and 35 deletions
+12 -10
View File
@@ -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
+4
View File
@@ -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
+3 -2
View File
@@ -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)
+17 -12
View File
@@ -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
+3 -1
View File
@@ -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
)
+2 -1
View File
@@ -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]
+4 -1
View File
@@ -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
)
+1 -1
View File
@@ -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(
@@ -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 (
<div className="w-full h-[82vh] border rounded overflow-hidden bg-white">
<iframe
ref={iframeRef}
src="https://embed.diagrams.net/?embed=1&spin=1&proto=json&noSaveBtn=1&noExitBtn=1&libraries=1"
title="KubeDiagrams Draw.io Viewer"
className="w-full h-full"
allowFullScreen
/>
</div>
);
}
function DiagramViewer({
diagram,
outputFormat,
@@ -53,6 +111,11 @@ function DiagramViewer({
);
}
// DRAWIO - Draw.io embedded viewer
if (ext === OUTPUT_FORMATS.DRAWIO) {
return <DrawioViewer key={viewerKey} content={diagram} />;
}
// PDF - Embedded viewer
if (ext === OUTPUT_FORMATS.PDF) {
return (
@@ -13,8 +13,9 @@ function DownloadButton({ diagram, mimeType, outputFormat, filename, filenameFal
return null;
}
const finalFilename =
filename || filenameFallback || `diagram.${(outputFormat || 'png').toLowerCase()}`;
// Default to 'png' if outputFormat is not provided
const format = outputFormat || 'png';
const finalFilename = filename || filenameFallback || `diagram.${format.toLowerCase()}`;
return (
<div className="flex justify-center">
@@ -23,14 +24,14 @@ function DownloadButton({ diagram, mimeType, outputFormat, filename, filenameFal
handleDownload({
diagram,
mimeType,
outputFormat,
outputFormat: format,
filenameFallback: finalFilename,
})
}
className="mt-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition flex items-center gap-2"
>
<Download className="w-4 h-4" />
Download {outputFormat.toUpperCase()}
Download {format.toUpperCase()}
</button>
</div>
);
@@ -149,8 +149,9 @@ export function useDiagramGeneration({ apiFunction, validateInput, diagramType =
setStdout(data.stdout || '');
setStderr(data.stderr || '');
// Increment viewer key for DOT_JSON to force iframe reload
if (!hasFatal && (params.outputFormat || '').toLowerCase() === OUTPUT_FORMATS.DOT_JSON) {
// Increment viewer key for DOT_JSON and DRAWIO to force iframe reload
const fmt = (params.outputFormat || '').toLowerCase();
if (!hasFatal && (fmt === OUTPUT_FORMATS.DOT_JSON || fmt === OUTPUT_FORMATS.DRAWIO)) {
setViewerKey((k) => k + 1);
}
+4
View File
@@ -7,6 +7,7 @@ export const OUTPUT_FORMATS = Object.freeze({
PDF: 'pdf',
DOT: 'dot',
DOT_JSON: 'dot_json',
DRAWIO: 'drawio',
});
export const OUTPUT_FORMAT_LIST = Object.freeze([
@@ -17,6 +18,7 @@ export const OUTPUT_FORMAT_LIST = Object.freeze([
OUTPUT_FORMATS.PDF,
OUTPUT_FORMATS.DOT,
OUTPUT_FORMATS.DOT_JSON,
OUTPUT_FORMATS.DRAWIO,
]);
// API Endpoints
@@ -51,6 +53,7 @@ export const MIME_TYPES = Object.freeze({
[OUTPUT_FORMATS.PDF]: 'application/pdf',
[OUTPUT_FORMATS.DOT]: 'text/vnd.graphviz',
[OUTPUT_FORMATS.DOT_JSON]: 'application/json',
[OUTPUT_FORMATS.DRAWIO]: 'application/xml',
});
// Viewer message types for postMessage communication
@@ -72,4 +75,5 @@ export const TEXT_FORMATS = Object.freeze([
OUTPUT_FORMATS.SVG,
OUTPUT_FORMATS.DOT,
OUTPUT_FORMATS.DOT_JSON,
OUTPUT_FORMATS.DRAWIO,
]);
@@ -13,6 +13,7 @@ export const TOOLTIP_CONTENT = {
svg: 'SVG Vector - Perfect quality at any scale',
dot: 'DOT File - Graphviz text format for manual editing',
dotjson: 'Interactive Viewer - Explore with zoom/pan/click',
drawio: 'Draw.io - Open in draw.io editor or diagrams.net',
},
},