mirror of
https://github.com/philippemerle/KubeDiagrams.git
synced 2026-08-16 14:16:14 +00:00
Merge pull request #73 from Sandor59100/main
feat(webapp): add draw.io support and embed integration
This commit is contained in:
+12
-10
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user