feat: improve k8s resource filtering UI

- 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]
This commit is contained in:
loveuer
2025-11-12 23:26:52 +08:00
parent 54ed79cea3
commit 7d2e2ab842
2 changed files with 134 additions and 10 deletions

View File

@@ -26,6 +26,8 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
Snackbar, Snackbar,
MenuItem,
Select,
} from '@mui/material' } from '@mui/material'
import SettingsIcon from '@mui/icons-material/Settings' import SettingsIcon from '@mui/icons-material/Settings'
import CloseIcon from '@mui/icons-material/Close' import CloseIcon from '@mui/icons-material/Close'
@@ -56,6 +58,8 @@ export default function K8sResourceList() {
const [kubeconfig, setKubeconfig] = useState('') const [kubeconfig, setKubeconfig] = useState('')
const [kubeconfigError, setKubeconfigError] = useState(false) const [kubeconfigError, setKubeconfigError] = useState(false)
const [namespace, setNamespace] = useState('') const [namespace, setNamespace] = useState('')
const [namespaces, setNamespaces] = useState<string[]>([])
const [nameFilter, setNameFilter] = useState('')
const [createDialogOpen, setCreateDialogOpen] = useState(false) const [createDialogOpen, setCreateDialogOpen] = useState(false)
const [yamlContent, setYamlContent] = useState('') const [yamlContent, setYamlContent] = useState('')
const [applyLoading, setApplyLoading] = useState(false) const [applyLoading, setApplyLoading] = useState(false)
@@ -80,8 +84,11 @@ export default function K8sResourceList() {
useEffect(() => { useEffect(() => {
if (kubeconfig) { if (kubeconfig) {
fetchResources() fetchResources()
if (selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') {
fetchNamespaces()
}
} }
}, [selectedKind, namespace]) }, [selectedKind, namespace, nameFilter])
const fetchKubeconfig = async () => { const fetchKubeconfig = async () => {
try { try {
@@ -222,6 +229,9 @@ export default function K8sResourceList() {
if (namespace && selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') { if (namespace && selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') {
url.searchParams.set('namespace', namespace) url.searchParams.set('namespace', namespace)
} }
if (nameFilter) {
url.searchParams.set('name', nameFilter)
}
const res = await fetch(url.toString()) const res = await fetch(url.toString())
if (!res.ok) throw new Error(`HTTP ${res.status}`) 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 = () => { const getResourceColumns = () => {
switch (selectedKind.key) { switch (selectedKind.key) {
case 'namespace': case 'namespace':
@@ -412,13 +434,35 @@ export default function K8sResourceList() {
</Box> </Box>
{selectedKind.key !== 'namespace' && selectedKind.key !== 'pv' && ( {selectedKind.key !== 'namespace' && selectedKind.key !== 'pv' && (
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2, display: 'flex', gap: 2 }}>
<TextField <TextField
placeholder="Namespace (空则查询所有)" select
label="Namespace"
value={namespace} value={namespace}
onChange={(e) => setNamespace(e.target.value)} onChange={(e) => setNamespace(e.target.value)}
size="small" size="small"
sx={{ width: 300 }} sx={{ width: 200 }}
SelectProps={{
displayEmpty: true,
}}
InputLabelProps={{ shrink: true }}
>
<MenuItem value="">
<em></em>
</MenuItem>
{namespaces.map((ns) => (
<MenuItem key={ns} value={ns}>
{ns}
</MenuItem>
))}
</TextField>
<TextField
label="名称过滤"
placeholder="按名称过滤"
value={nameFilter}
onChange={(e) => setNameFilter(e.target.value)}
size="small"
sx={{ width: 200 }}
/> />
</Box> </Box>
)} )}

View File

@@ -6,12 +6,14 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"strings"
"gitea.loveuer.com/loveuer/cluster/internal/model" "gitea.loveuer.com/loveuer/cluster/internal/model"
"gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/resp"
"gitea.loveuer.com/loveuer/cluster/pkg/store" "gitea.loveuer.com/loveuer/cluster/pkg/store"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"gorm.io/gorm" "gorm.io/gorm"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema" "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 { func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
namespace := c.Query("namespace", "") namespace := c.Query("namespace", "")
name := c.Query("name", "")
clientset, err := getK8sClient(db) clientset, err := getK8sClient(db)
if err != nil { 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) 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{ 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 { func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
namespace := c.Query("namespace", "") namespace := c.Query("namespace", "")
name := c.Query("name", "")
clientset, err := getK8sClient(db) clientset, err := getK8sClient(db)
if err != nil { 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) 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{ 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 { func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
namespace := c.Query("namespace", "") namespace := c.Query("namespace", "")
name := c.Query("name", "")
clientset, err := getK8sClient(db) clientset, err := getK8sClient(db)
if err != nil { 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) 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{ 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 { func K8sPodList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
namespace := c.Query("namespace", "") namespace := c.Query("namespace", "")
name := c.Query("name", "")
clientset, err := getK8sClient(db) clientset, err := getK8sClient(db)
if err != nil { 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) 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{ 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 { func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
namespace := c.Query("namespace", "") namespace := c.Query("namespace", "")
name := c.Query("name", "")
clientset, err := getK8sClient(db) clientset, err := getK8sClient(db)
if err != nil { 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) 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{ 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 { func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
namespace := c.Query("namespace", "") namespace := c.Query("namespace", "")
name := c.Query("name", "")
clientset, err := getK8sClient(db) clientset, err := getK8sClient(db)
if err != nil { 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) 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{ return resp.R200(c, map[string]any{
"items": services.Items, "items": filtered,
}) })
} }
} }