feat: add backend caching and pagination for resources
Co-authored-by: aider (openrouter/thinkingmachines/inkling-small:free) <aider@aider.chat>
This commit is contained in:
1 parent
f8422404d7
commit
77febb993e
3 files changed
+104
-74
No files matched your search
+75
-24
@@ -1,6 +1,8 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@@ -12,6 +14,9 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cache = {}
|
||||||
|
CACHE_TTL = 300 # 5 minutes
|
||||||
|
|
||||||
def run_kubectl(args, context=None):
|
def run_kubectl(args, context=None):
|
||||||
cmd = ["kubectl"]
|
cmd = ["kubectl"]
|
||||||
if context:
|
if context:
|
||||||
@@ -20,6 +25,21 @@ def run_kubectl(args, context=None):
|
|||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
return result
|
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("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {"message": "Kubectl Web GUI backend"}
|
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]
|
namespaces = [line for line in result.stdout.splitlines() if line]
|
||||||
return {"namespaces": namespaces}
|
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")
|
@app.get("/pods")
|
||||||
def get_pods(namespace: str = "default", context: str = None):
|
def get_pods(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
|
||||||
result = run_kubectl(
|
def mapper(i):
|
||||||
["get", "pods", "-n", namespace, "-o", "json"],
|
return {
|
||||||
context=context
|
"name": i.get("metadata", {}).get("name", "unknown"),
|
||||||
)
|
"namespace": i.get("metadata", {}).get("namespace", namespace)
|
||||||
return {"raw": result.stdout, "namespace": namespace, "context": context}
|
}
|
||||||
|
return paginated_list("pods", namespace, context, search, page, page_size, bypass, ["get", "pods", "-n", namespace, "-o", "json"], mapper)
|
||||||
|
|
||||||
@app.get("/nodes")
|
@app.get("/nodes")
|
||||||
def get_nodes(context: str = None):
|
def get_nodes(context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
|
||||||
result = run_kubectl(
|
def mapper(i):
|
||||||
["get", "nodes", "-o", "json"],
|
return {
|
||||||
context=context
|
"name": i.get("metadata", {}).get("name", "unknown"),
|
||||||
)
|
"namespace": "-"
|
||||||
return {"raw": result.stdout, "context": context}
|
}
|
||||||
|
return paginated_list("nodes", "default", context, search, page, page_size, bypass, ["get", "nodes", "-o", "json"], mapper)
|
||||||
|
|
||||||
@app.get("/pvcs")
|
@app.get("/pvcs")
|
||||||
def get_pvcs(namespace: str = "default", context: str = None):
|
def get_pvcs(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
|
||||||
result = run_kubectl(
|
def mapper(i):
|
||||||
["get", "pvc", "-n", namespace, "-o", "json"],
|
return {
|
||||||
context=context
|
"name": i.get("metadata", {}).get("name", "unknown"),
|
||||||
)
|
"namespace": i.get("metadata", {}).get("namespace", namespace)
|
||||||
return {"raw": result.stdout, "namespace": namespace, "context": context}
|
}
|
||||||
|
return paginated_list("pvcs", namespace, context, search, page, page_size, bypass, ["get", "pvc", "-n", namespace, "-o", "json"], mapper)
|
||||||
|
|
||||||
@app.get("/configmaps")
|
@app.get("/configmaps")
|
||||||
def get_configmaps(namespace: str = "default", context: str = None):
|
def get_configmaps(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
|
||||||
result = run_kubectl(
|
def mapper(i):
|
||||||
["get", "configmaps", "-n", namespace, "-o", "json"],
|
return {
|
||||||
context=context
|
"name": i.get("metadata", {}).get("name", "unknown"),
|
||||||
)
|
"namespace": i.get("metadata", {}).get("namespace", namespace)
|
||||||
return {"raw": result.stdout, "namespace": namespace, "context": context}
|
}
|
||||||
|
return paginated_list("configmaps", namespace, context, search, page, page_size, bypass, ["get", "configmaps", "-n", namespace, "-o", "json"], mapper)
|
||||||
|
|
||||||
@app.get("/describe")
|
@app.get("/describe")
|
||||||
def describe(resource: str, name: str, namespace: str = "default", context: str = None):
|
def describe(resource: str, name: str, namespace: str = "default", context: str = None):
|
||||||
|
|||||||
+27
-49
@@ -6,11 +6,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const resourceTitle = document.getElementById('resource-title');
|
const resourceTitle = document.getElementById('resource-title');
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const searchBtn = document.getElementById('search-btn');
|
const searchBtn = document.getElementById('search-btn');
|
||||||
|
const refreshBtn = document.getElementById('refresh-btn');
|
||||||
const setDefaultBtn = document.getElementById('set-default');
|
const setDefaultBtn = document.getElementById('set-default');
|
||||||
const actionName = document.getElementById('action-name');
|
const actionName = document.getElementById('action-name');
|
||||||
const actionOutput = document.getElementById('action-output');
|
const actionOutput = document.getElementById('action-output');
|
||||||
|
|
||||||
let currentData = [];
|
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
const pageSize = 10;
|
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 resType = resourceSelect.value;
|
||||||
const ns = namespaceSelect.value || 'default';
|
const ns = namespaceSelect.value || 'default';
|
||||||
const ctx = contextSelect.value || null;
|
const ctx = contextSelect.value || null;
|
||||||
|
const search = searchInput.value.trim();
|
||||||
resourceTitle.textContent = resType.charAt(0).toUpperCase() + resType.slice(1);
|
resourceTitle.textContent = resType.charAt(0).toUpperCase() + resType.slice(1);
|
||||||
resourceList.innerHTML = '<li>Loading...</li>';
|
resourceList.innerHTML = '<li>Loading...</li>';
|
||||||
currentPage = 1;
|
currentPage = page;
|
||||||
try {
|
try {
|
||||||
let url;
|
let url;
|
||||||
if (resType === 'pods') {
|
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') {
|
} 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') {
|
} 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') {
|
} 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 {
|
} 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 res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
currentData = [];
|
renderList(data);
|
||||||
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();
|
|
||||||
updateActionVisibility();
|
updateActionVisibility();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load resources', e);
|
console.error('Failed to load resources', e);
|
||||||
@@ -129,21 +113,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList(data) {
|
||||||
const filter = searchInput.value.toLowerCase();
|
const items = data.items || [];
|
||||||
const filtered = currentData.filter(item => item.name.toLowerCase().includes(filter));
|
const total = data.total || 0;
|
||||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
const page = data.page || 1;
|
||||||
if (currentPage > totalPages) currentPage = totalPages;
|
const pageSize = data.page_size || 10;
|
||||||
if (currentPage < 1) currentPage = 1;
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||||
|
|
||||||
const start = (currentPage - 1) * pageSize;
|
|
||||||
const pageItems = filtered.slice(start, start + pageSize);
|
|
||||||
|
|
||||||
resourceList.innerHTML = '';
|
resourceList.innerHTML = '';
|
||||||
if (pageItems.length === 0) {
|
if (items.length === 0) {
|
||||||
resourceList.innerHTML = '<li>No results</li>';
|
resourceList.innerHTML = '<li>No results</li>';
|
||||||
} else {
|
} else {
|
||||||
pageItems.forEach(item => {
|
items.forEach(item => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.style.cursor = 'pointer';
|
li.style.cursor = 'pointer';
|
||||||
li.innerHTML = `<strong>${item.name}</strong> <span style="color:#666">(${item.namespace})</span>`;
|
li.innerHTML = `<strong>${item.name}</strong> <span style="color:#666">(${item.namespace})</span>`;
|
||||||
@@ -162,29 +143,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pageInfo = document.getElementById('page-info');
|
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 prevBtn = document.getElementById('prev-page');
|
||||||
const nextBtn = document.getElementById('next-page');
|
const nextBtn = document.getElementById('next-page');
|
||||||
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
if (prevBtn) prevBtn.disabled = page <= 1;
|
||||||
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
|
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
contextSelect.addEventListener('change', () => { saveSession(); loadNamespaces(); });
|
contextSelect.addEventListener('change', () => { saveSession(); loadNamespaces(); });
|
||||||
namespaceSelect.addEventListener('change', () => { saveSession(); loadResources(); });
|
namespaceSelect.addEventListener('change', () => { saveSession(); loadResources(); });
|
||||||
resourceSelect.addEventListener('change', () => { saveSession(); loadResources(); });
|
resourceSelect.addEventListener('change', () => { saveSession(); loadResources(); });
|
||||||
searchBtn.addEventListener('click', () => { currentPage = 1; renderList(); });
|
searchBtn.addEventListener('click', () => loadResources(false, 1));
|
||||||
searchInput.addEventListener('input', () => { currentPage = 1; renderList(); });
|
refreshBtn.addEventListener('click', () => loadResources(true, 1));
|
||||||
setDefaultBtn.addEventListener('click', () => { saveSession(); alert('Defaults saved for session'); });
|
setDefaultBtn.addEventListener('click', () => { saveSession(); alert('Defaults saved for session'); });
|
||||||
|
|
||||||
document.getElementById('prev-page').addEventListener('click', () => {
|
document.getElementById('prev-page').addEventListener('click', () => {
|
||||||
if (currentPage > 1) { currentPage--; renderList(); }
|
if (currentPage > 1) loadResources(false, currentPage - 1);
|
||||||
});
|
});
|
||||||
document.getElementById('next-page').addEventListener('click', () => {
|
document.getElementById('next-page').addEventListener('click', () => {
|
||||||
const filter = searchInput.value.toLowerCase();
|
loadResources(false, currentPage + 1);
|
||||||
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(); }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-describe').addEventListener('click', async () => {
|
document.getElementById('btn-describe').addEventListener('click', async () => {
|
||||||
|
|||||||
+2
-1
@@ -31,7 +31,8 @@
|
|||||||
|
|
||||||
<section id="search">
|
<section id="search">
|
||||||
<input type="text" id="search-input" placeholder="Search by name..." />
|
<input type="text" id="search-input" placeholder="Search by name..." />
|
||||||
<button id="search-btn">Search / Refresh</button>
|
<button id="search-btn">Search</button>
|
||||||
|
<button id="refresh-btn">Refresh</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="resources">
|
<section id="resources">
|
||||||
|
|||||||
Reference in new issue
Block a user