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 = '
  • Loading...
  • '; - currentPage = 1; + currentPage = page; try { let url; if (resType === 'pods') { - url = `http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}${ctx ? '&context=' + encodeURIComponent(ctx) : ''}`; + url = `http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}&page=${page}&page_size=${pageSize}&bypass=${bypass}`; } else if (resType === 'nodes') { - url = `http://localhost:8000/nodes${ctx ? '?context=' + encodeURIComponent(ctx) : ''}`; + url = `http://localhost:8000/nodes?page=${page}&page_size=${pageSize}&bypass=${bypass}`; } else if (resType === 'pvcs') { - url = `http://localhost:8000/pvcs?namespace=${encodeURIComponent(ns)}${ctx ? '&context=' + encodeURIComponent(ctx) : ''}`; + url = `http://localhost:8000/pvcs?namespace=${encodeURIComponent(ns)}&page=${page}&page_size=${pageSize}&bypass=${bypass}`; } else if (resType === 'configmaps') { - url = `http://localhost:8000/configmaps?namespace=${encodeURIComponent(ns)}${ctx ? '&context=' + encodeURIComponent(ctx) : ''}`; + url = `http://localhost:8000/configmaps?namespace=${encodeURIComponent(ns)}&page=${page}&page_size=${pageSize}&bypass=${bypass}`; } else { - url = `http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}`; + url = `http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}&page=${page}&page_size=${pageSize}&bypass=${bypass}`; } + if (search) url += `&search=${encodeURIComponent(search)}`; + if (ctx) url += `&context=${encodeURIComponent(ctx)}`; const res = await fetch(url); const data = await res.json(); - currentData = []; - try { - const obj = JSON.parse(data.raw || '{}'); - if (resType === 'pods' || resType === 'pvcs' || resType === 'configmaps') { - const items = obj.items || []; - currentData = items.map(i => ({ - name: i.metadata?.name || 'unknown', - namespace: i.metadata?.namespace || ns - })); - } else if (resType === 'nodes') { - const items = obj.items || []; - currentData = items.map(i => ({ - name: i.metadata?.name || 'unknown', - namespace: '-' - })); - } - } catch (e) { - currentData = []; - } - renderList(); + renderList(data); updateActionVisibility(); } catch (e) { console.error('Failed to load resources', e); @@ -129,21 +113,18 @@ document.addEventListener('DOMContentLoaded', () => { } } - function renderList() { - const filter = searchInput.value.toLowerCase(); - const filtered = currentData.filter(item => item.name.toLowerCase().includes(filter)); - const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); - if (currentPage > totalPages) currentPage = totalPages; - if (currentPage < 1) currentPage = 1; - - const start = (currentPage - 1) * pageSize; - const pageItems = filtered.slice(start, start + pageSize); + function renderList(data) { + const items = data.items || []; + const total = data.total || 0; + const page = data.page || 1; + const pageSize = data.page_size || 10; + const totalPages = Math.max(1, Math.ceil(total / pageSize)); resourceList.innerHTML = ''; - if (pageItems.length === 0) { + if (items.length === 0) { resourceList.innerHTML = '
  • No results
  • '; } else { - pageItems.forEach(item => { + items.forEach(item => { const li = document.createElement('li'); li.style.cursor = 'pointer'; li.innerHTML = `${item.name} (${item.namespace})`; @@ -162,29 +143,26 @@ document.addEventListener('DOMContentLoaded', () => { } const pageInfo = document.getElementById('page-info'); - if (pageInfo) pageInfo.textContent = `Page ${currentPage} of ${totalPages} (${filtered.length} total)`; + if (pageInfo) pageInfo.textContent = `Page ${page} of ${totalPages} (${total} total)`; const prevBtn = document.getElementById('prev-page'); const nextBtn = document.getElementById('next-page'); - if (prevBtn) prevBtn.disabled = currentPage <= 1; - if (nextBtn) nextBtn.disabled = currentPage >= totalPages; + if (prevBtn) prevBtn.disabled = page <= 1; + if (nextBtn) nextBtn.disabled = page >= totalPages; } contextSelect.addEventListener('change', () => { saveSession(); loadNamespaces(); }); namespaceSelect.addEventListener('change', () => { saveSession(); loadResources(); }); resourceSelect.addEventListener('change', () => { saveSession(); loadResources(); }); - searchBtn.addEventListener('click', () => { currentPage = 1; renderList(); }); - searchInput.addEventListener('input', () => { currentPage = 1; renderList(); }); + searchBtn.addEventListener('click', () => loadResources(false, 1)); + refreshBtn.addEventListener('click', () => loadResources(true, 1)); setDefaultBtn.addEventListener('click', () => { saveSession(); alert('Defaults saved for session'); }); document.getElementById('prev-page').addEventListener('click', () => { - if (currentPage > 1) { currentPage--; renderList(); } + if (currentPage > 1) loadResources(false, currentPage - 1); }); document.getElementById('next-page').addEventListener('click', () => { - const filter = searchInput.value.toLowerCase(); - const filtered = currentData.filter(item => item.name.toLowerCase().includes(filter)); - const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); - if (currentPage < totalPages) { currentPage++; renderList(); } + loadResources(false, currentPage + 1); }); document.getElementById('btn-describe').addEventListener('click', async () => { diff --git a/frontend/index.html b/frontend/index.html index 402c415..85bf55a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -31,7 +31,8 @@