feat(kubernetes): Add keyboard focus for search input
- Typing anywhere on the page auto-focuses the search input - Escape key clears search and blurs input - Matches Dashboard behavior for consistent UX - Fixed missing createEffect import
This commit is contained in:
parent
91f8b81547
commit
9725849d77
2 changed files with 80 additions and 10 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { For, Show, createMemo, createSignal } from 'solid-js';
|
||||
import { For, Show, createMemo, createSignal, createEffect } from 'solid-js';
|
||||
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||
import { useColumnVisibility, type ColumnDef } from '@/hooks/useColumnVisibility';
|
||||
import type {
|
||||
|
|
@ -166,6 +166,36 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
|
|||
|
||||
const sortIndicator = (key: SortKey) => sortKey() === key ? (sortDirection() === 'asc' ? ' ▲' : ' ▼') : '';
|
||||
|
||||
// Search input ref for keyboard focus
|
||||
let searchInputRef: HTMLInputElement | undefined;
|
||||
|
||||
// Global keyboard handler - focus search on typing
|
||||
createEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const isInputField =
|
||||
target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.tagName === 'SELECT' ||
|
||||
target.contentEditable === 'true';
|
||||
|
||||
// Escape clears search
|
||||
if (e.key === 'Escape' && searchInputRef) {
|
||||
setSearch('');
|
||||
searchInputRef.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus search on printable character (when not in input field)
|
||||
if (!isInputField && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
searchInputRef?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
// Get all unique namespaces for the filter dropdown
|
||||
const allNamespaces = createMemo(() => {
|
||||
const namespaces = new Set<string>();
|
||||
|
|
@ -426,6 +456,7 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
|
|||
<div class="flex gap-2 flex-1 items-center">
|
||||
<div class="relative flex-1">
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search clusters, nodes, pods..."
|
||||
value={search()}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
*/
|
||||
|
||||
import { createMemo, type Accessor } from 'solid-js';
|
||||
import { unwrap } from 'solid-js/store';
|
||||
import { getGlobalWebSocketStore } from '@/stores/websocket-global';
|
||||
import type {
|
||||
Resource,
|
||||
|
|
@ -240,12 +241,30 @@ export function useResourcesAsLegacy() {
|
|||
// Check if we have unified resources (populated after first broadcast)
|
||||
const hasUnifiedResources = createMemo(() => resources().length > 0);
|
||||
|
||||
// Prefer legacy arrays when they exist. The backend currently broadcasts both the
|
||||
// legacy arrays and unified resources, and the legacy arrays are already reconciled
|
||||
// by id in the WebSocket store (stable identities, fine-grained updates).
|
||||
// Only synthesize legacy types from unified resources when it looks like the backend
|
||||
// isn't providing that legacy field (e.g., resources include the type but legacy array is empty).
|
||||
const hasVmResources = createMemo(() => resources().some((r) => r.type === 'vm'));
|
||||
const hasNodeResources = createMemo(() => resources().some((r) => r.type === 'node'));
|
||||
const hasContainerResources = createMemo(() =>
|
||||
resources().some((r) => r.type === 'container' || r.type === 'oci-container'),
|
||||
);
|
||||
const hasHostResources = createMemo(() => resources().some((r) => r.type === 'host'));
|
||||
const hasDockerHostResources = createMemo(() => resources().some((r) => r.type === 'docker-host'));
|
||||
|
||||
// Convert resources to legacy VM format
|
||||
// Falls back to legacy state.vms array when unified resources aren't yet populated
|
||||
const asVMs = createMemo(() => {
|
||||
const legacy = wsStore.state.vms ?? [];
|
||||
// If we don't have unified resources yet, use legacy arrays directly
|
||||
if (!hasUnifiedResources()) {
|
||||
return wsStore.state.vms ?? [];
|
||||
return legacy;
|
||||
}
|
||||
// If legacy data exists (or there are no VM resources), keep using it.
|
||||
if (legacy.length > 0 || !hasVmResources()) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
return byType('vm').map(r => {
|
||||
|
|
@ -299,9 +318,14 @@ export function useResourcesAsLegacy() {
|
|||
// Convert resources to legacy Container format
|
||||
// Falls back to legacy state.containers array when unified resources aren't yet populated
|
||||
const asContainers = createMemo(() => {
|
||||
const legacy = wsStore.state.containers ?? [];
|
||||
// If we don't have unified resources yet, use legacy arrays directly
|
||||
if (!hasUnifiedResources()) {
|
||||
return wsStore.state.containers ?? [];
|
||||
return legacy;
|
||||
}
|
||||
// If legacy data exists (or there are no container resources), keep using it.
|
||||
if (legacy.length > 0 || !hasContainerResources()) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
// Include both traditional LXC containers and OCI containers (Proxmox VE 9.1+).
|
||||
|
|
@ -364,14 +388,19 @@ export function useResourcesAsLegacy() {
|
|||
// Convert resources to legacy Host format
|
||||
// Falls back to legacy state.hosts array when unified resources aren't yet populated
|
||||
const asHosts = createMemo(() => {
|
||||
const legacy = wsStore.state.hosts ?? [];
|
||||
// If we don't have unified resources yet, use legacy arrays directly
|
||||
if (!hasUnifiedResources()) {
|
||||
return wsStore.state.hosts ?? [];
|
||||
return legacy;
|
||||
}
|
||||
// If legacy data exists (or there are no host resources), keep using it.
|
||||
if (legacy.length > 0 || !hasHostResources()) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
return byType('host').map((r) => {
|
||||
// Extract platform-specific data - unwrap SolidJS Proxy objects into plain JS objects
|
||||
const platformData = r.platformData ? JSON.parse(JSON.stringify(r.platformData)) as Record<string, unknown> : undefined;
|
||||
const platformData = r.platformData ? (unwrap(r.platformData) as Record<string, unknown>) : undefined;
|
||||
|
||||
// Interfaces from platformData
|
||||
const interfaces = platformData?.interfaces as Array<{
|
||||
|
|
@ -448,15 +477,20 @@ export function useResourcesAsLegacy() {
|
|||
// Convert resources to legacy Node format
|
||||
// Falls back to legacy state.nodes array when unified resources aren't yet populated
|
||||
const asNodes = createMemo(() => {
|
||||
const legacy = wsStore.state.nodes ?? [];
|
||||
// If we don't have unified resources yet, use legacy arrays directly
|
||||
if (!hasUnifiedResources()) {
|
||||
// Return legacy nodes array as-is (it's already in the right format)
|
||||
return wsStore.state.nodes ?? [];
|
||||
return legacy;
|
||||
}
|
||||
// If legacy data exists (or there are no node resources), keep using it.
|
||||
if (legacy.length > 0 || !hasNodeResources()) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
return byType('node').map(r => {
|
||||
// Unwrap SolidJS Proxy objects into plain JS objects
|
||||
const platformData = r.platformData ? JSON.parse(JSON.stringify(r.platformData)) as Record<string, unknown> : undefined;
|
||||
const platformData = r.platformData ? (unwrap(r.platformData) as Record<string, unknown>) : undefined;
|
||||
|
||||
// Build temperature object from unified resource
|
||||
// The unified resource has temperature as a simple number,
|
||||
|
|
@ -512,9 +546,14 @@ export function useResourcesAsLegacy() {
|
|||
// Convert resources to legacy DockerHost format (including nested containers)
|
||||
// Falls back to legacy state.dockerHosts array when unified resources aren't yet populated
|
||||
const asDockerHosts = createMemo(() => {
|
||||
const legacy = wsStore.state.dockerHosts ?? [];
|
||||
// If we don't have unified resources yet, use legacy arrays directly
|
||||
if (!hasUnifiedResources()) {
|
||||
return wsStore.state.dockerHosts ?? [];
|
||||
return legacy;
|
||||
}
|
||||
// If legacy data exists (or there are no docker-host resources), keep using it.
|
||||
if (legacy.length > 0 || !hasDockerHostResources()) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
const dockerHostResources = byType('docker-host');
|
||||
|
|
@ -522,13 +561,13 @@ export function useResourcesAsLegacy() {
|
|||
|
||||
return dockerHostResources.map(h => {
|
||||
// Unwrap SolidJS Proxy objects into plain JS objects
|
||||
const platformData = h.platformData ? JSON.parse(JSON.stringify(h.platformData)) as Record<string, unknown> : undefined;
|
||||
const platformData = h.platformData ? (unwrap(h.platformData) as Record<string, unknown>) : undefined;
|
||||
|
||||
// Find containers belonging to this host
|
||||
const hostContainers = dockerContainerResources
|
||||
.filter(c => c.parentId === h.id)
|
||||
.map(c => {
|
||||
const cPlatform = c.platformData ? JSON.parse(JSON.stringify(c.platformData)) as Record<string, unknown> : undefined;
|
||||
const cPlatform = c.platformData ? (unwrap(c.platformData) as Record<string, unknown>) : undefined;
|
||||
// Extract original container ID from compound resource ID (hostID/containerID)
|
||||
// This is needed for sparkline metrics to match the sampler which uses original IDs
|
||||
const originalContainerId = c.id.includes('/') ? c.id.split('/').pop()! : c.id;
|
||||
|
|
|
|||
Loading…
Reference in a new issue