diff --git a/docs/development/MOCK_MODE.md b/docs/development/MOCK_MODE.md index 5f09f6a..4d4e2ab 100644 --- a/docs/development/MOCK_MODE.md +++ b/docs/development/MOCK_MODE.md @@ -25,6 +25,8 @@ Edit `mock.env` (or `mock.env.local` for overrides): | `PULSE_MOCK_LXCS_PER_NODE` | `8` | Containers per node. | | `PULSE_MOCK_RANDOM_METRICS` | `true` | Jitter metrics. | | `PULSE_MOCK_STOPPED_PERCENT` | `20` | % of offline guests. | +| `PULSE_MOCK_TRENDS_SEED_DURATION` | `1h` | Pre-seed backend chart history (improves demo “Trends” immediately). | +| `PULSE_MOCK_TRENDS_SAMPLE_INTERVAL` | `30s` | Backend chart sampling interval while in mock mode. | ## ℹ️ How it Works * **Data**: Swaps `PULSE_DATA_DIR` to `/opt/pulse/tmp/mock-data`. diff --git a/frontend-modern/src/components/Kubernetes/KubernetesClusters.tsx b/frontend-modern/src/components/Kubernetes/KubernetesClusters.tsx index bf99321..a5c45e5 100644 --- a/frontend-modern/src/components/Kubernetes/KubernetesClusters.tsx +++ b/frontend-modern/src/components/Kubernetes/KubernetesClusters.tsx @@ -1,6 +1,7 @@ import type { Component } from 'solid-js'; import { For, Show, createMemo, createSignal } from 'solid-js'; import { usePersistentSignal } from '@/hooks/usePersistentSignal'; +import { useColumnVisibility, type ColumnDef } from '@/hooks/useColumnVisibility'; import type { KubernetesCluster, KubernetesDeployment, @@ -11,6 +12,7 @@ import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; import { ScrollableTable } from '@/components/shared/ScrollableTable'; import { StatusDot } from '@/components/shared/StatusDot'; +import { ColumnPicker } from '@/components/shared/ColumnPicker'; import { formatRelativeTime, formatBytes } from '@/utils/format'; import { DEGRADED_HEALTH_STATUSES, OFFLINE_HEALTH_STATUSES, type StatusIndicator } from '@/utils/status'; @@ -125,6 +127,18 @@ const formatAge = (timestamp?: number | string | null): string => { return formatRelativeTime(ts); }; +// Column definitions for the pods table +const POD_COLUMNS: ColumnDef[] = [ + { id: 'name', label: 'Pod', priority: 'essential', toggleable: false }, + { id: 'namespace', label: 'Namespace', priority: 'essential', toggleable: true }, + { id: 'cluster', label: 'Cluster', priority: 'primary', toggleable: true }, + { id: 'status', label: 'Status', priority: 'essential', toggleable: false }, + { id: 'ready', label: 'Ready', priority: 'primary', toggleable: true }, + { id: 'restarts', label: 'Restarts', priority: 'primary', toggleable: true }, + { id: 'image', label: 'Image', priority: 'secondary', toggleable: true }, + { id: 'age', label: 'Age', priority: 'primary', toggleable: true }, +]; + export const KubernetesClusters: Component = (props) => { const [search, setSearch] = createSignal(''); const [viewMode, setViewMode] = createSignal('clusters'); @@ -132,6 +146,9 @@ export const KubernetesClusters: Component = (props) => const [showHidden, setShowHidden] = createSignal(false); const [namespaceFilter, setNamespaceFilter] = createSignal('all'); + // Column visibility for pods table + const podColumns = useColumnVisibility('k8s-pod-columns', POD_COLUMNS); + // Sorting state with persistence type SortKey = 'name' | 'status' | 'namespace' | 'cluster' | 'age' | 'restarts' | 'ready' | 'replicas'; type SortDir = 'asc' | 'desc'; @@ -207,8 +224,10 @@ export const KubernetesClusters: Component = (props) => const term = search().trim().toLowerCase(); const clusters = props.clusters ?? []; const status = statusFilter(); + const key = sortKey(); + const dir = sortDirection(); - return clusters + const filtered = clusters .filter((cluster) => showHidden() || !cluster.hidden) .filter((cluster) => { if (status === 'all') return true; @@ -230,15 +249,27 @@ export const KubernetesClusters: Component = (props) => .join(' ') .toLowerCase(); return haystack.includes(term); - }) - .sort((a, b) => getClusterDisplayName(a).localeCompare(getClusterDisplayName(b))); + }); + + // Sort clusters + return filtered.sort((a, b) => { + let cmp = 0; + switch (key) { + case 'name': cmp = getClusterDisplayName(a).localeCompare(getClusterDisplayName(b)); break; + case 'status': cmp = (normalize(a.status) === 'online' ? 0 : 1) - (normalize(b.status) === 'online' ? 0 : 1); break; + default: cmp = getClusterDisplayName(a).localeCompare(getClusterDisplayName(b)); + } + return dir === 'desc' ? -cmp : cmp; + }); }); const filteredNodes = createMemo(() => { const term = search().trim().toLowerCase(); const status = statusFilter(); + const key = sortKey(); + const dir = sortDirection(); - return allNodes() + const filtered = allNodes() .filter(({ node }) => { if (status === 'all') return true; const isHealthy = node.ready && !node.unschedulable; @@ -259,6 +290,18 @@ export const KubernetesClusters: Component = (props) => .toLowerCase(); return haystack.includes(term); }); + + // Sort nodes + return filtered.sort((a, b) => { + let cmp = 0; + switch (key) { + case 'name': cmp = (a.node.name ?? '').localeCompare(b.node.name ?? ''); break; + case 'cluster': cmp = getClusterDisplayName(a.cluster).localeCompare(getClusterDisplayName(b.cluster)); break; + case 'status': cmp = (a.node.ready ? 0 : 1) - (b.node.ready ? 0 : 1); break; + default: cmp = (a.node.name ?? '').localeCompare(b.node.name ?? ''); + } + return dir === 'desc' ? -cmp : cmp; + }); }); const filteredPods = createMemo(() => { @@ -535,6 +578,17 @@ export const KubernetesClusters: Component = (props) => Show hidden + {/* Column Picker - only show for pods view */} + +