From 7d2e2ab8425174124ee4b07795ba426c287eeedc Mon Sep 17 00:00:00 2001 From: loveuer Date: Wed, 12 Nov 2025 23:26:52 +0800 Subject: [PATCH] feat: improve k8s resource filtering UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change namespace filter from text input to dropdown select - Add name filtering input for all resource types - Fix UI overlap issue with namespace dropdown label - Add automatic namespace list loading - Implement server-side name filtering for all resources - Support combined namespace + name filtering 🤖 Generated with [Qoder][https://qoder.com] --- frontend/src/pages/K8sResourceList.tsx | 52 ++++++++++++-- internal/module/k8s/handler.resource.go | 92 +++++++++++++++++++++++-- 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/K8sResourceList.tsx b/frontend/src/pages/K8sResourceList.tsx index 8f556db..9218202 100644 --- a/frontend/src/pages/K8sResourceList.tsx +++ b/frontend/src/pages/K8sResourceList.tsx @@ -26,6 +26,8 @@ import { DialogContent, DialogActions, Snackbar, + MenuItem, + Select, } from '@mui/material' import SettingsIcon from '@mui/icons-material/Settings' import CloseIcon from '@mui/icons-material/Close' @@ -56,6 +58,8 @@ export default function K8sResourceList() { const [kubeconfig, setKubeconfig] = useState('') const [kubeconfigError, setKubeconfigError] = useState(false) const [namespace, setNamespace] = useState('') + const [namespaces, setNamespaces] = useState([]) + const [nameFilter, setNameFilter] = useState('') const [createDialogOpen, setCreateDialogOpen] = useState(false) const [yamlContent, setYamlContent] = useState('') const [applyLoading, setApplyLoading] = useState(false) @@ -80,8 +84,11 @@ export default function K8sResourceList() { useEffect(() => { if (kubeconfig) { fetchResources() + if (selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') { + fetchNamespaces() + } } - }, [selectedKind, namespace]) + }, [selectedKind, namespace, nameFilter]) const fetchKubeconfig = async () => { try { @@ -222,6 +229,9 @@ export default function K8sResourceList() { if (namespace && selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') { url.searchParams.set('namespace', namespace) } + if (nameFilter) { + url.searchParams.set('name', nameFilter) + } const res = await fetch(url.toString()) if (!res.ok) throw new Error(`HTTP ${res.status}`) @@ -234,6 +244,18 @@ export default function K8sResourceList() { } } + const fetchNamespaces = async () => { + try { + const res = await fetch('/api/v1/k8s/namespace/list') + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const result = await res.json() + const namespaceList = result.data?.items?.map((ns: any) => ns.metadata.name) || [] + setNamespaces(namespaceList) + } catch (e: any) { + console.error('Failed to fetch namespaces:', e) + } + } + const getResourceColumns = () => { switch (selectedKind.key) { case 'namespace': @@ -412,13 +434,35 @@ export default function K8sResourceList() { {selectedKind.key !== 'namespace' && selectedKind.key !== 'pv' && ( - + setNamespace(e.target.value)} size="small" - sx={{ width: 300 }} + sx={{ width: 200 }} + SelectProps={{ + displayEmpty: true, + }} + InputLabelProps={{ shrink: true }} + > + + 所有命名空间 + + {namespaces.map((ns) => ( + + {ns} + + ))} + + setNameFilter(e.target.value)} + size="small" + sx={{ width: 200 }} /> )} diff --git a/internal/module/k8s/handler.resource.go b/internal/module/k8s/handler.resource.go index ecb245c..3dfecbf 100644 --- a/internal/module/k8s/handler.resource.go +++ b/internal/module/k8s/handler.resource.go @@ -6,12 +6,14 @@ import ( "encoding/json" "fmt" "io" + "strings" "gitea.loveuer.com/loveuer/cluster/internal/model" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" "gorm.io/gorm" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -93,6 +95,7 @@ func K8sNamespaceList(ctx context.Context, db *gorm.DB, store store.Store) fiber func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { namespace := c.Query("namespace", "") + name := c.Query("name", "") clientset, err := getK8sClient(db) if err != nil { @@ -104,8 +107,20 @@ func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fibe return resp.R500(c, "", nil, err) } + // Filter by name if provided + var filtered []appsv1.Deployment + if name != "" { + for _, deployment := range deployments.Items { + if strings.Contains(deployment.Name, name) { + filtered = append(filtered, deployment) + } + } + } else { + filtered = deployments.Items + } + return resp.R200(c, map[string]any{ - "items": deployments.Items, + "items": filtered, }) } } @@ -113,6 +128,7 @@ func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fibe func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { namespace := c.Query("namespace", "") + name := c.Query("name", "") clientset, err := getK8sClient(db) if err != nil { @@ -124,8 +140,20 @@ func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fib return resp.R500(c, "", nil, err) } + // Filter by name if provided + var filtered []appsv1.StatefulSet + if name != "" { + for _, statefulset := range statefulsets.Items { + if strings.Contains(statefulset.Name, name) { + filtered = append(filtered, statefulset) + } + } + } else { + filtered = statefulsets.Items + } + return resp.R200(c, map[string]any{ - "items": statefulsets.Items, + "items": filtered, }) } } @@ -133,6 +161,7 @@ func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fib func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { namespace := c.Query("namespace", "") + name := c.Query("name", "") clientset, err := getK8sClient(db) if err != nil { @@ -144,8 +173,20 @@ func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber return resp.R500(c, "", nil, err) } + // Filter by name if provided + var filtered []corev1.ConfigMap + if name != "" { + for _, configmap := range configmaps.Items { + if strings.Contains(configmap.Name, name) { + filtered = append(filtered, configmap) + } + } + } else { + filtered = configmaps.Items + } + return resp.R200(c, map[string]any{ - "items": configmaps.Items, + "items": filtered, }) } } @@ -153,6 +194,7 @@ func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber func K8sPodList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { namespace := c.Query("namespace", "") + name := c.Query("name", "") clientset, err := getK8sClient(db) if err != nil { @@ -164,8 +206,20 @@ func K8sPodList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handl return resp.R500(c, "", nil, err) } + // Filter by name if provided + var filtered []corev1.Pod + if name != "" { + for _, pod := range pods.Items { + if strings.Contains(pod.Name, name) { + filtered = append(filtered, pod) + } + } + } else { + filtered = pods.Items + } + return resp.R200(c, map[string]any{ - "items": pods.Items, + "items": filtered, }) } } @@ -191,6 +245,7 @@ func K8sPVList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handle func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { namespace := c.Query("namespace", "") + name := c.Query("name", "") clientset, err := getK8sClient(db) if err != nil { @@ -202,8 +257,20 @@ func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handl return resp.R500(c, "", nil, err) } + // Filter by name if provided + var filtered []corev1.PersistentVolumeClaim + if name != "" { + for _, pvc := range pvcs.Items { + if strings.Contains(pvc.Name, name) { + filtered = append(filtered, pvc) + } + } + } else { + filtered = pvcs.Items + } + return resp.R200(c, map[string]any{ - "items": pvcs.Items, + "items": filtered, }) } } @@ -211,6 +278,7 @@ func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handl func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { namespace := c.Query("namespace", "") + name := c.Query("name", "") clientset, err := getK8sClient(db) if err != nil { @@ -222,8 +290,20 @@ func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.H return resp.R500(c, "", nil, err) } + // Filter by name if provided + var filtered []corev1.Service + if name != "" { + for _, service := range services.Items { + if strings.Contains(service.Name, name) { + filtered = append(filtered, service) + } + } + } else { + filtered = services.Items + } + return resp.R200(c, map[string]any{ - "items": services.Items, + "items": filtered, }) } }