diff --git a/backend/main.py b/backend/main.py index 0b07b83..27539db 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import subprocess +import time +import json app = FastAPI() @@ -12,6 +14,9 @@ app.add_middleware( allow_headers=["*"], ) +cache = {} +CACHE_TTL = 300 # 5 minutes + def run_kubectl(args, context=None): cmd = ["kubectl"] if context: @@ -20,6 +25,21 @@ def run_kubectl(args, context=None): result = subprocess.run(cmd, capture_output=True, text=True) return result +def get_cache_key(resource, namespace, context): + return f"{resource}:{namespace}:{context or ''}" + +def get_cached_items(resource, namespace, context): + key = get_cache_key(resource, namespace, context) + if key in cache: + ts, items = cache[key] + if time.time() - ts < CACHE_TTL: + return items + return None + +def set_cached_items(resource, namespace, context, items): + key = get_cache_key(resource, namespace, context) + cache[key] = (time.time(), items) + @app.get("/") def root(): return {"message": "Kubectl Web GUI backend"} @@ -45,37 +65,68 @@ def get_namespaces(context: str = None): namespaces = [line for line in result.stdout.splitlines() if line] return {"namespaces": namespaces} +def paginated_list(resource, namespace, context, search, page, page_size, bypass, kubectl_args, item_mapper): + key = get_cache_key(resource, namespace, context) + items = None + if not bypass: + items = get_cached_items(resource, namespace, context) + if items is None: + result = run_kubectl(kubectl_args, context=context) + try: + obj = json.loads(result.stdout) + raw_items = obj.get("items", []) + except Exception: + raw_items = [] + items = [item_mapper(i) for i in raw_items] + set_cached_items(resource, namespace, context, items) + if search: + s = search.lower() + items = [i for i in items if s in (i.get("name") or "").lower()] + total = len(items) + start = (page - 1) * page_size + paginated = items[start:start + page_size] + return { + "items": paginated, + "total": total, + "page": page, + "page_size": page_size + } + @app.get("/pods") -def get_pods(namespace: str = "default", context: str = None): - result = run_kubectl( - ["get", "pods", "-n", namespace, "-o", "json"], - context=context - ) - return {"raw": result.stdout, "namespace": namespace, "context": context} +def get_pods(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False): + def mapper(i): + return { + "name": i.get("metadata", {}).get("name", "unknown"), + "namespace": i.get("metadata", {}).get("namespace", namespace) + } + return paginated_list("pods", namespace, context, search, page, page_size, bypass, ["get", "pods", "-n", namespace, "-o", "json"], mapper) @app.get("/nodes") -def get_nodes(context: str = None): - result = run_kubectl( - ["get", "nodes", "-o", "json"], - context=context - ) - return {"raw": result.stdout, "context": context} +def get_nodes(context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False): + def mapper(i): + return { + "name": i.get("metadata", {}).get("name", "unknown"), + "namespace": "-" + } + return paginated_list("nodes", "default", context, search, page, page_size, bypass, ["get", "nodes", "-o", "json"], mapper) @app.get("/pvcs") -def get_pvcs(namespace: str = "default", context: str = None): - result = run_kubectl( - ["get", "pvc", "-n", namespace, "-o", "json"], - context=context - ) - return {"raw": result.stdout, "namespace": namespace, "context": context} +def get_pvcs(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False): + def mapper(i): + return { + "name": i.get("metadata", {}).get("name", "unknown"), + "namespace": i.get("metadata", {}).get("namespace", namespace) + } + return paginated_list("pvcs", namespace, context, search, page, page_size, bypass, ["get", "pvc", "-n", namespace, "-o", "json"], mapper) @app.get("/configmaps") -def get_configmaps(namespace: str = "default", context: str = None): - result = run_kubectl( - ["get", "configmaps", "-n", namespace, "-o", "json"], - context=context - ) - return {"raw": result.stdout, "namespace": namespace, "context": context} +def get_configmaps(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False): + def mapper(i): + return { + "name": i.get("metadata", {}).get("name", "unknown"), + "namespace": i.get("metadata", {}).get("namespace", namespace) + } + return paginated_list("configmaps", namespace, context, search, page, page_size, bypass, ["get", "configmaps", "-n", namespace, "-o", "json"], mapper) @app.get("/describe") def describe(resource: str, name: str, namespace: str = "default", context: str = None): diff --git a/frontend/app.js b/frontend/app.js index 81e3c59..0bd24c7 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -6,11 +6,11 @@ document.addEventListener('DOMContentLoaded', () => { const resourceTitle = document.getElementById('resource-title'); const searchInput = document.getElementById('search-input'); const searchBtn = document.getElementById('search-btn'); + const refreshBtn = document.getElementById('refresh-btn'); const setDefaultBtn = document.getElementById('set-default'); const actionName = document.getElementById('action-name'); const actionOutput = document.getElementById('action-output'); - let currentData = []; let currentPage = 1; const pageSize = 10; @@ -79,49 +79,33 @@ document.addEventListener('DOMContentLoaded', () => { } } - async function loadResources() { + async function loadResources(bypass = false, page = 1) { const resType = resourceSelect.value; const ns = namespaceSelect.value || 'default'; const ctx = contextSelect.value || null; + const search = searchInput.value.trim(); resourceTitle.textContent = resType.charAt(0).toUpperCase() + resType.slice(1); resourceList.innerHTML = '