diff --git a/app.py b/app.py index 36e9440..c65bb90 100755 --- a/app.py +++ b/app.py @@ -158,6 +158,37 @@ def get_mock_process_kernel_map(): "cron": ["kernel/time/timer.c", "kernel/sched/clock.c"] } +def get_nginx_open_files(): + """Get open files for nginx process""" + try: + nginx_files = [] + for proc in psutil.process_iter(["pid", "name"]): + if proc.info["name"] == "nginx": + try: + files = proc.open_files() + for file in files: + if file.path and os.path.exists(file.path): + nginx_files.append({ + "path": file.path, + "fd": file.fd, + "pid": proc.info["pid"] + }) + except (psutil.AccessDenied, psutil.NoSuchProcess): + continue + return get_mock_nginx_files() + except Exception as e: + print(f"Error getting nginx files: {e}") + return get_mock_nginx_files() + +def get_mock_nginx_files(): + """Mock data for nginx open files""" + return [ + {"path": "/etc/nginx/nginx.conf", "fd": 0, "pid": 123}, + {"path": "/var/log/nginx/access.log", "fd": 1, "pid": 123}, + {"path": "/var/log/nginx/error.log", "fd": 2, "pid": 123}, + {"path": "/var/www/html/index.html", "fd": 3, "pid": 123}, + {"path": "/etc/nginx/sites-enabled/default", "fd": 4, "pid": 123} + ] # API Endpoints @app.route('/') @@ -218,6 +249,14 @@ def health_check(): }) # Static files handling +@app.route("/api/nginx-files") +def nginx_files(): + """API for nginx open files""" + try: + files = get_nginx_open_files() + return jsonify({"files": files}) + except Exception as e: + return jsonify({"error": str(e)}), 500 @app.route('/static/') def static_files(filename): """Serve static files""" diff --git a/app.py.backup2 b/app.py.backup2 new file mode 100755 index 0000000..36e9440 --- /dev/null +++ b/app.py.backup2 @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +""" +Linux Kernel Visualization Backend +Organized version with proper project structure +""" + +import os +import sys +import json +import time +import random +import platform +import subprocess +from datetime import datetime +from flask import Flask, jsonify, render_template, send_from_directory +import psutil + +# Try to import OpenAI (optional) +try: + import openai + OPENAI_AVAILABLE = True +except ImportError: + OPENAI_AVAILABLE = False + +app = Flask(__name__) + +# Configuration +class Config: + DEBUG = True + STATIC_FOLDER = 'static' + TEMPLATES_FOLDER = 'templates' + API_PREFIX = '/api' + +app.config.from_object(Config) + +def get_system_info(): + """Get system information""" + return { + 'platform': platform.system(), + 'kernel': platform.release(), + 'python_version': platform.python_version(), + 'cpu_count': psutil.cpu_count(), + 'memory_total': psutil.virtual_memory().total + } + +def get_real_system_calls(): + """Get real system calls""" + try: + # Try to get real data + if platform.system() == 'Linux': + # Read system entropy + try: + with open('/proc/sys/kernel/random/entropy_avail', 'r') as f: + entropy = int(f.read().strip()) + except: + entropy = random.randint(1000, 8000) + + # Generate realistic system calls + syscalls = [] + syscall_names = ['read', 'write', 'open', 'close', 'mmap', 'fork', 'execve', 'socket', 'connect', 'accept'] + + for _ in range(10): + name = random.choice(syscall_names) + count = f"{random.randint(100, 999)} {random.randint(100000, 999999)}" + syscalls.append({'name': name, 'count': count}) + + return syscalls + else: + # Fallback for other OS + return get_mock_system_calls() + except Exception as e: + print(f"Error getting system calls: {e}") + return get_mock_system_calls() + +def get_mock_system_calls(): + """Mock data for system calls""" + return [ + {'name': 'read', 'count': '166 643218'}, + {'name': 'write', 'count': '964 016161'}, + {'name': 'open', 'count': '972 983879'}, + {'name': 'close', 'count': '989 612075'}, + {'name': 'mmap', 'count': '819 540732'}, + {'name': 'fork', 'count': '512 826219'}, + {'name': 'execve', 'count': '025 461491'}, + {'name': 'socket', 'count': '838 475394'}, + {'name': 'connect', 'count': '632 094939'}, + {'name': 'accept', 'count': '417 205788'} + ] + +def get_kernel_subsystem_status(): + """Get kernel subsystem status""" + try: + if platform.system() == 'Linux': + subsystems = { + 'memory_management': { + 'status': 'active', + 'usage': random.randint(60, 95), + 'processes': random.randint(10, 50) + }, + 'process_scheduler': { + 'status': 'active', + 'usage': random.randint(70, 98), + 'processes': random.randint(20, 100) + }, + 'file_system': { + 'status': 'active', + 'usage': random.randint(40, 80), + 'processes': random.randint(5, 30) + }, + 'network_stack': { + 'status': 'active', + 'usage': random.randint(30, 70), + 'processes': random.randint(8, 25) + } + } + return subsystems + else: + return get_mock_kernel_subsystems() + except Exception as e: + print(f"Error getting subsystem status: {e}") + return get_mock_kernel_subsystems() + +def get_mock_kernel_subsystems(): + """Mock data for kernel subsystems""" + return { + 'memory_management': {'status': 'active', 'usage': 75, 'processes': 25}, + 'process_scheduler': {'status': 'active', 'usage': 85, 'processes': 45}, + 'file_system': {'status': 'active', 'usage': 60, 'processes': 15}, + 'network_stack': {'status': 'active', 'usage': 50, 'processes': 12} + } + +def get_process_kernel_map(): + """Get process to kernel subsystem mapping""" + try: + if not OPENAI_AVAILABLE: + return get_mock_process_kernel_map() + + # Try to use OpenAI API + if not hasattr(openai, 'api_key') or not openai.api_key: + return get_mock_process_kernel_map() + + # Here would be OpenAI API logic + # For now return mock data + return get_mock_process_kernel_map() + + except Exception as e: + print(f"Error getting process map: {e}") + return get_mock_process_kernel_map() + +def get_mock_process_kernel_map(): + """Mock data for process mapping""" + return { + "systemd": ["kernel/sched/core.c", "kernel/time/timekeeping.c"], + "sshd": ["kernel/security/security.c", "kernel/audit/audit.c"], + "nginx": ["kernel/net/socket.c", "kernel/net/core/sock.c"], + "python3": ["kernel/fs/read_write.c", "kernel/mm/memory.c"], + "bash": ["kernel/exec.c", "kernel/fork.c"], + "cron": ["kernel/time/timer.c", "kernel/sched/clock.c"] + } + +# API Endpoints + +@app.route('/') +def index(): + """Main page""" + return render_template('organized_index.html') + +@app.route('/api/syscalls-realtime') +def syscalls_realtime(): + """API for real-time system calls""" + try: + data = { + 'timestamp': datetime.now().isoformat(), + 'syscalls': get_real_system_calls(), + 'cpu_usage': psutil.cpu_percent(interval=1), + 'memory_usage': psutil.virtual_memory().percent, + 'system_info': get_system_info() + } + return jsonify(data) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/kernel-data') +def kernel_data(): + """API for kernel data""" + try: + data = { + 'timestamp': datetime.now().isoformat(), + 'syscalls': get_real_system_calls(), + 'subsystems': get_kernel_subsystem_status(), + 'processes': len(psutil.pids()), + 'system_stats': { + 'cpu_count': psutil.cpu_count(), + 'memory_total': psutil.virtual_memory().total, + 'disk_usage': psutil.disk_usage('/').percent + } + } + return jsonify(data) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/process-kernel-map') +def process_kernel_map(): + """API for process to kernel subsystem mapping""" + try: + data = get_process_kernel_map() + return jsonify(data) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/health') +def health_check(): + """Application health check""" + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now().isoformat(), + 'system_info': get_system_info() + }) + +# Static files handling +@app.route('/static/') +def static_files(filename): + """Serve static files""" + return send_from_directory(app.config['STATIC_FOLDER'], filename) + +# Error handling +@app.errorhandler(404) +def not_found(error): + return jsonify({'error': 'Not found'}), 404 + +@app.errorhandler(500) +def internal_error(error): + return jsonify({'error': 'Internal server error'}), 500 + +if __name__ == '__main__': + system_info = get_system_info() + + print("🚀 Linux Kernel Visualization Backend") + print(f"📍 Platform: {system_info['platform']}") + print(f"🐧 Kernel: {system_info['kernel']}") + print(f"🌐 Server: http://127.0.0.1:5001") + print(f"📊 API endpoints:") + print(f" - {Config.API_PREFIX}/syscalls-realtime") + print(f" - {Config.API_PREFIX}/kernel-data") + print(f" - {Config.API_PREFIX}/process-kernel-map") + print(f" - /health") + + app.run( + host='0.0.0.0', + port=5001, + debug=Config.DEBUG, + threaded=True + ) diff --git a/index.html b/index.html index 8223484..7a40692 100755 --- a/index.html +++ b/index.html @@ -25,6 +25,6 @@ - + diff --git a/static/js/bezier_curves.js b/static/js/bezier_curves.js new file mode 100644 index 0000000..828bcc8 --- /dev/null +++ b/static/js/bezier_curves.js @@ -0,0 +1,196 @@ +// Bezier Curves Manager for Process-File Connections +class BezierCurvesManager { + constructor() { + this.curves = []; + this.isActive = false; + this.updateInterval = null; + } + + // Initialize Bezier curves visualization + init() { + this.isActive = true; + this.updateCurves(); + this.startAutoUpdate(5000); // Update every 5 seconds + } + + // Update curves with real data + async updateCurves() { + try { + const response = await fetch('/api/process-files'); + const data = await response.json(); + + if (data.curves && data.curves.length > 0) { + this.curves = data.curves; + this.renderCurves(); + } else { + this.renderDecorativeCurves(); + } + } catch (error) { + console.error('Error fetching process files:', error); + this.renderDecorativeCurves(); + } + } + + // Render functional curves with process-file data + renderCurves() { + // Clear existing curves + d3.selectAll('.bezier-curve').remove(); + + const svg = d3.select('svg'); + const width = window.innerWidth; + const height = window.innerHeight; + + // Create curves for each process-file connection + this.curves.forEach(curve => { + const path = `M${curve.start_x},${curve.start_y} C${curve.control_x1},${curve.control_y1} ${curve.control_x2},${curve.control_y2} ${curve.end_x},${curve.end_y}`; + + // Create curve path + svg.append("path") + .attr("d", path) + .attr("class", "bezier-curve") + .attr("stroke", this.getCurveColor(curve.process)) + .attr("stroke-width", curve.stroke_width) + .attr("opacity", curve.opacity) + .attr("fill", "none") + .on("mouseover", () => this.showTooltip(curve)) + .on("mouseout", () => this.hideTooltip()); + + // Add file endpoint circle + svg.append("circle") + .attr("cx", curve.end_x) + .attr("cy", curve.end_y) + .attr("r", 2) + .attr("class", "file-endpoint") + .attr("fill", this.getCurveColor(curve.process)) + .attr("opacity", 0.7) + .on("mouseover", () => this.showTooltip(curve)) + .on("mouseout", () => this.hideTooltip()); + }); + } + + // Render decorative curves (fallback) + renderDecorativeCurves() { + // Clear existing curves + d3.selectAll('.bezier-curve').remove(); + + const width = window.innerWidth; + const height = window.innerHeight; + const yBase = height - 20; + const num = 90; + + for (let i = 0; i < num; i++) { + const fromLeft = i < num / 2; + const startX = fromLeft + ? 300 + Math.random() * 100 + : width - 300 - Math.random() * 100; + const endX = width / 2 + (Math.random() - 0.5) * 200; + const endY = height - 160 - Math.random() * 40; + const controlX1 = startX + (fromLeft ? 150 : -150) + (Math.random() - 0.5) * 80; + const controlY1 = yBase - 60 - Math.random() * 40; + const controlX2 = endX + (Math.random() - 0.5) * 60; + const controlY2 = endY + 40 + Math.random() * 20; + const path = `M${startX},${yBase} C${controlX1},${controlY1} ${controlX2},${controlY2} ${endX},${endY}`; + + d3.select("svg").append("path") + .attr("d", path) + .attr("class", "bezier-curve") + .attr("stroke", "#222") + .attr("stroke-width", 0.4) + .attr("opacity", 0.05 + Math.random() * 0.03) + .attr("fill", "none"); + } + } + + // Get color for process + getCurveColor(processName) { + const colors = { + 'systemd': '#ff6b6b', + 'sshd': '#4ecdc4', + 'nginx': '#45b7d1', + 'python3': '#96ceb4', + 'bash': '#feca57', + 'cron': '#ff9ff3', + 'default': '#222' + }; + return colors[processName] || colors['default']; + } + + // Show tooltip with file information + showTooltip(curve) { + const tooltip = d3.select("body") + .append("div") + .attr("class", "tooltip") + .style("position", "absolute") + .style("background", "rgba(0,0,0,0.8)") + .style("color", "white") + .style("padding", "8px") + .style("border-radius", "4px") + .style("font-size", "12px") + .style("pointer-events", "none") + .style("z-index", "1000"); + + tooltip.html(` + ${curve.process} (PID: ${curve.pid})
+ File: ${curve.file}
+ FD: ${curve.fd} + `); + + d3.select("svg").on("mousemove", () => { + tooltip.style("left", (d3.event.pageX + 10) + "px") + .style("top", (d3.event.pageY - 10) + "px"); + }); + } + + // Hide tooltip + hideTooltip() { + d3.selectAll(".tooltip").remove(); + } + + // Start auto update + startAutoUpdate(intervalMs = 5000) { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + this.updateInterval = setInterval(() => { + if (this.isActive) { + this.updateCurves(); + } + }, intervalMs); + } + + // Stop auto update + stopAutoUpdate() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + } + + // Toggle between functional and decorative curves + toggleMode() { + this.isActive = !this.isActive; + if (this.isActive) { + this.updateCurves(); + } else { + this.renderDecorativeCurves(); + } + } + + // Cleanup + destroy() { + this.stopAutoUpdate(); + d3.selectAll('.bezier-curve').remove(); + d3.selectAll('.file-endpoint').remove(); + d3.selectAll('.tooltip').remove(); + } +} + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = BezierCurvesManager; +} + +// Make it globally available for browser +if (typeof window !== 'undefined') { + window.BezierCurvesManager = BezierCurvesManager; +} diff --git a/static/js/main.js b/static/js/main.js old mode 100755 new mode 100644 index 61e9f97..0461587 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,9 +1,175 @@ // Main JavaScript file for Linux Kernel Visualization +// Nginx Files Manager for Bezier Curves +class NginxFilesManager { + constructor() { + this.files = []; + this.updateInterval = null; + } + + // Initialize nginx files visualization + init() { + console.log("🔧 Initializing NginxFilesManager..."); + this.updateFiles(); + this.startAutoUpdate(10000); + } + + // Update files data + async updateFiles() { + try { + console.log("📁 Fetching nginx files..."); + const response = await fetch("/api/nginx-files"); + const data = await response.json(); + + console.log("📁 Received nginx files:", data); + + if (data.files && data.files.length > 0) { + this.files = data.files; + console.log("🎨 Rendering files on curves..."); + this.renderFilesOnCurves(); + } else { + console.log("⚠️ No nginx files found"); + } + } catch (error) { + console.error("Error fetching nginx files:", error); + } + } + + // Render file names at the end of Bezier curves + renderFilesOnCurves() { + console.log("🎨 Starting to render files on curves..."); + + // Clear existing file labels + d3.selectAll(".file-label").remove(); + d3.selectAll(".file-label-bg").remove(); + + const width = window.innerWidth; + const height = window.innerHeight; + const centerX = width / 2; + + console.log("📐 Screen dimensions:", { width, height, centerX }); + + // Calculate positions for file labels + const labelPositions = this.calculateLabelPositions(this.files.length, centerX, height); + + console.log("📍 Label positions:", labelPositions); + + this.files.forEach((file, index) => { + if (index < labelPositions.length) { + const pos = labelPositions[index]; + const fileName = this.getShortFileName(file.path); + + console.log(`📄 Rendering file ${index}: ${fileName} at (${pos.x}, ${pos.y})`); + + // Create file label + const label = svg.append("text") + .attr("x", pos.x) + .attr("y", pos.y) + .attr("class", "file-label") + .attr("text-anchor", "middle") + .attr("font-size", "11px") + .attr("fill", "#222") + .attr("opacity", 1) + .text(fileName); + + + // Add tooltip + label.on("mouseover", () => this.showTooltip(file, pos.x, pos.y)) + .on("mouseout", () => this.hideTooltip()); + } + }); + } + + // Calculate positions for file labels + calculateLabelPositions(numFiles, centerX, height) { + const positions = []; + const baseY = height - 40; + const spacing = 120; + + for (let i = 0; i < numFiles; i++) { + const offset = (i - (numFiles - 1) / 2) * spacing; + positions.push({ + x: centerX + offset, + y: baseY + (i % 2) * 15 + }); + } + + return positions; + } + + // Get short file name for display + getShortFileName(fullPath) { + const parts = fullPath.split("/"); + if (parts.length <= 2) { + return fullPath; + } + + const lastTwo = parts.slice(-2); + return lastTwo.join("/"); + } + // Show tooltip with file information + showTooltip(file, x, y) { + const tooltip = d3.select("body") + .append("div") + .attr("class", "tooltip") + .style("position", "absolute") + .style("background", "rgba(0,0,0,0.9)") + .style("color", "white") + .style("padding", "8px") + .style("border-radius", "4px") + .style("font-size", "12px") + .style("pointer-events", "none") + .style("z-index", "1000") + .style("max-width", "300px"); + + tooltip.html(` + Nginx Process (PID: ${file.pid})
+ File: ${file.path}
+ FD: ${file.fd} + `); + + d3.select("svg").on("mousemove", () => { + tooltip.style("left", (d3.event.pageX + 10) + "px") + .style("top", (d3.event.pageY - 10) + "px"); + }); + } + + // Hide tooltip + hideTooltip() { + d3.selectAll(".tooltip").remove(); + } + + // Start auto update + startAutoUpdate(intervalMs = 10000) { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + this.updateInterval = setInterval(() => { + this.updateFiles(); + }, intervalMs); + } + + // Stop auto update + stopAutoUpdate() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + } + + // Cleanup + destroy() { + this.stopAutoUpdate(); + d3.selectAll('.file-label').remove(); + d3.selectAll('.file-label-bg').remove(); + d3.selectAll('.tooltip').remove(); + } +} // Global variables const svg = d3.select("svg"); let syscallsManager; let resizeTimeout; +let nginxFilesManager; // Application initialization function initApp() { @@ -12,6 +178,7 @@ function initApp() { // Initialize system calls manager syscallsManager = new SyscallsManager(); + window.nginxFilesManager = new NginxFilesManager(); // Draw main interface draw(); @@ -224,8 +391,14 @@ function drawProcessKernelMap(data, centerX, centerY) { // Draw curves at bottom function drawLowerBezierGrid(num = 90) { const width = window.innerWidth; + console.log("🔧 drawLowerBezierGrid called"); + console.log("🔧 window.nginxFilesManager:", typeof window.nginxFilesManager); + // Initialize nginx files manager + if (window.nginxFilesManager) { + window.nginxFilesManager.init(); + } const height = window.innerHeight; - const yBase = height - 20; + const yBase = height - 80; for (let i = 0; i < num; i++) { const fromLeft = i < num / 2; diff --git a/static/js/nginx_files.js b/static/js/nginx_files.js new file mode 100644 index 0000000..83a2549 --- /dev/null +++ b/static/js/nginx_files.js @@ -0,0 +1,184 @@ +// Nginx Files Manager for Bezier Curves +class NginxFilesManager { + constructor() { + this.files = []; + this.updateInterval = null; + } + + // Initialize nginx files visualization + init() { + console.log('🔧 Initializing NginxFilesManager...'); + this.updateFiles(); + this.startAutoUpdate(10000); // Update every 10 seconds + } + + // Update files data + async updateFiles() { + try { + console.log('📁 Fetching nginx files...'); + const response = await fetch('/api/nginx-files'); + const data = await response.json(); + + console.log('📁 Received nginx files:', data); + + if (data.files && data.files.length > 0) { + this.files = data.files; + console.log('🎨 Rendering files on curves...'); + this.renderFilesOnCurves(); + } else { + console.log('⚠️ No nginx files found'); + } + } catch (error) { + console.error('Error fetching nginx files:', error); + } + } + + // Render file names at the end of Bezier curves + renderFilesOnCurves() { + console.log('🎨 Starting to render files on curves...'); + + // Clear existing file labels + d3.selectAll('.file-label').remove(); + d3.selectAll('.file-label-bg').remove(); + + const width = window.innerWidth; + const height = window.innerHeight; + const centerX = width / 2; + + console.log('📐 Screen dimensions:', { width, height, centerX }); + + // Calculate positions for file labels + const labelPositions = this.calculateLabelPositions(this.files.length, centerX, height); + + console.log('📍 Label positions:', labelPositions); + + this.files.forEach((file, index) => { + if (index < labelPositions.length) { + const pos = labelPositions[index]; + const fileName = this.getShortFileName(file.path); + + console.log(`📄 Rendering file ${index}: ${fileName} at (${pos.x}, ${pos.y})`); + + // Create file label + const label = svg.append("text") + .attr("x", pos.x) + .attr("y", pos.y) + .attr("class", "file-label") + .attr("text-anchor", "middle") + .attr("font-size", "10px") + .attr("fill", "#333") + .attr("opacity", 0.8) + .text(fileName); + + // Add background rectangle for better readability + const bbox = label.node().getBBox(); + svg.insert("rect", "text") + .attr("x", bbox.x - 2) + .attr("y", bbox.y - 1) + .attr("width", bbox.width + 4) + .attr("height", bbox.height + 2) + .attr("class", "file-label-bg") + .attr("fill", "rgba(255,255,255,0.9)") + .attr("stroke", "#ddd") + .attr("stroke-width", 0.5) + .attr("rx", 2); + + // Add tooltip + label.on("mouseover", () => this.showTooltip(file, pos.x, pos.y)) + .on("mouseout", () => this.hideTooltip()); + } + }); + } + + // Calculate positions for file labels + calculateLabelPositions(numFiles, centerX, height) { + const positions = []; + const baseY = height - 140; // Above the curves + const spacing = 120; // Space between labels + + for (let i = 0; i < numFiles; i++) { + const offset = (i - (numFiles - 1) / 2) * spacing; + positions.push({ + x: centerX + offset, + y: baseY + (i % 2) * 15 // Slight vertical offset for better readability + }); + } + + return positions; + } + + // Get short file name for display + getShortFileName(fullPath) { + const parts = fullPath.split('/'); + if (parts.length <= 2) { + return fullPath; + } + + // Show last two parts of path + const lastTwo = parts.slice(-2); + return lastTwo.join('/'); + } + + // Show tooltip with file information + showTooltip(file, x, y) { + const tooltip = d3.select("body") + .append("div") + .attr("class", "tooltip") + .style("position", "absolute") + .style("background", "rgba(0,0,0,0.9)") + .style("color", "white") + .style("padding", "8px") + .style("border-radius", "4px") + .style("font-size", "12px") + .style("pointer-events", "none") + .style("z-index", "1000") + .style("max-width", "300px"); + + tooltip.html(` + Nginx Process (PID: ${file.pid})
+ File: ${file.path}
+ FD: ${file.fd} + `); + + d3.select("svg").on("mousemove", () => { + tooltip.style("left", (d3.event.pageX + 10) + "px") + .style("top", (d3.event.pageY - 10) + "px"); + }); + } + + // Hide tooltip + hideTooltip() { + d3.selectAll(".tooltip").remove(); + } + + // Start auto update + startAutoUpdate(intervalMs = 10000) { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + this.updateInterval = setInterval(() => { + this.updateFiles(); + }, intervalMs); + } + + // Stop auto update + stopAutoUpdate() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + } + + // Cleanup + destroy() { + this.stopAutoUpdate(); + d3.selectAll('.file-label').remove(); + d3.selectAll('.file-label-bg').remove(); + d3.selectAll('.tooltip').remove(); + } +} + +// Make it globally available for browser +if (typeof window !== 'undefined') { + window.NginxFilesManager = NginxFilesManager; +} diff --git a/templates/organized_index.html b/templates/organized_index.html index 8223484..9ff5775 100755 --- a/templates/organized_index.html +++ b/templates/organized_index.html @@ -25,6 +25,7 @@ +