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 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 { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||||
import { useColumnVisibility, type ColumnDef } from '@/hooks/useColumnVisibility';
|
import { useColumnVisibility, type ColumnDef } from '@/hooks/useColumnVisibility';
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -166,6 +166,36 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
|
||||||
|
|
||||||
const sortIndicator = (key: SortKey) => sortKey() === key ? (sortDirection() === 'asc' ? ' ▲' : ' ▼') : '';
|
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
|
// Get all unique namespaces for the filter dropdown
|
||||||
const allNamespaces = createMemo(() => {
|
const allNamespaces = createMemo(() => {
|
||||||
const namespaces = new Set<string>();
|
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="flex gap-2 flex-1 items-center">
|
||||||
<div class="relative flex-1">
|
<div class="relative flex-1">
|
||||||
<input
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search clusters, nodes, pods..."
|
placeholder="Search clusters, nodes, pods..."
|
||||||
value={search()}
|
value={search()}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createMemo, type Accessor } from 'solid-js';
|
import { createMemo, type Accessor } from 'solid-js';
|
||||||
|
import { unwrap } from 'solid-js/store';
|
||||||
import { getGlobalWebSocketStore } from '@/stores/websocket-global';
|
import { getGlobalWebSocketStore } from '@/stores/websocket-global';
|
||||||
import type {
|
import type {
|
||||||
Resource,
|
Resource,
|
||||||
|
|
@ -240,12 +241,30 @@ export function useResourcesAsLegacy() {
|
||||||
// Check if we have unified resources (populated after first broadcast)
|
// Check if we have unified resources (populated after first broadcast)
|
||||||
const hasUnifiedResources = createMemo(() => resources().length > 0);
|
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
|
// Convert resources to legacy VM format
|
||||||
// Falls back to legacy state.vms array when unified resources aren't yet populated
|
// Falls back to legacy state.vms array when unified resources aren't yet populated
|
||||||
const asVMs = createMemo(() => {
|
const asVMs = createMemo(() => {
|
||||||
|
const legacy = wsStore.state.vms ?? [];
|
||||||
// If we don't have unified resources yet, use legacy arrays directly
|
// If we don't have unified resources yet, use legacy arrays directly
|
||||||
if (!hasUnifiedResources()) {
|
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 => {
|
return byType('vm').map(r => {
|
||||||
|
|
@ -299,9 +318,14 @@ export function useResourcesAsLegacy() {
|
||||||
// Convert resources to legacy Container format
|
// Convert resources to legacy Container format
|
||||||
// Falls back to legacy state.containers array when unified resources aren't yet populated
|
// Falls back to legacy state.containers array when unified resources aren't yet populated
|
||||||
const asContainers = createMemo(() => {
|
const asContainers = createMemo(() => {
|
||||||
|
const legacy = wsStore.state.containers ?? [];
|
||||||
// If we don't have unified resources yet, use legacy arrays directly
|
// If we don't have unified resources yet, use legacy arrays directly
|
||||||
if (!hasUnifiedResources()) {
|
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+).
|
// 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
|
// Convert resources to legacy Host format
|
||||||
// Falls back to legacy state.hosts array when unified resources aren't yet populated
|
// Falls back to legacy state.hosts array when unified resources aren't yet populated
|
||||||
const asHosts = createMemo(() => {
|
const asHosts = createMemo(() => {
|
||||||
|
const legacy = wsStore.state.hosts ?? [];
|
||||||
// If we don't have unified resources yet, use legacy arrays directly
|
// If we don't have unified resources yet, use legacy arrays directly
|
||||||
if (!hasUnifiedResources()) {
|
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) => {
|
return byType('host').map((r) => {
|
||||||
// Extract platform-specific data - unwrap SolidJS Proxy objects into plain JS objects
|
// 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
|
// Interfaces from platformData
|
||||||
const interfaces = platformData?.interfaces as Array<{
|
const interfaces = platformData?.interfaces as Array<{
|
||||||
|
|
@ -448,15 +477,20 @@ export function useResourcesAsLegacy() {
|
||||||
// Convert resources to legacy Node format
|
// Convert resources to legacy Node format
|
||||||
// Falls back to legacy state.nodes array when unified resources aren't yet populated
|
// Falls back to legacy state.nodes array when unified resources aren't yet populated
|
||||||
const asNodes = createMemo(() => {
|
const asNodes = createMemo(() => {
|
||||||
|
const legacy = wsStore.state.nodes ?? [];
|
||||||
// If we don't have unified resources yet, use legacy arrays directly
|
// If we don't have unified resources yet, use legacy arrays directly
|
||||||
if (!hasUnifiedResources()) {
|
if (!hasUnifiedResources()) {
|
||||||
// Return legacy nodes array as-is (it's already in the right format)
|
// 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 => {
|
return byType('node').map(r => {
|
||||||
// Unwrap SolidJS Proxy objects into plain JS objects
|
// 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
|
// Build temperature object from unified resource
|
||||||
// The unified resource has temperature as a simple number,
|
// 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)
|
// Convert resources to legacy DockerHost format (including nested containers)
|
||||||
// Falls back to legacy state.dockerHosts array when unified resources aren't yet populated
|
// Falls back to legacy state.dockerHosts array when unified resources aren't yet populated
|
||||||
const asDockerHosts = createMemo(() => {
|
const asDockerHosts = createMemo(() => {
|
||||||
|
const legacy = wsStore.state.dockerHosts ?? [];
|
||||||
// If we don't have unified resources yet, use legacy arrays directly
|
// If we don't have unified resources yet, use legacy arrays directly
|
||||||
if (!hasUnifiedResources()) {
|
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');
|
const dockerHostResources = byType('docker-host');
|
||||||
|
|
@ -522,13 +561,13 @@ export function useResourcesAsLegacy() {
|
||||||
|
|
||||||
return dockerHostResources.map(h => {
|
return dockerHostResources.map(h => {
|
||||||
// Unwrap SolidJS Proxy objects into plain JS objects
|
// 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
|
// Find containers belonging to this host
|
||||||
const hostContainers = dockerContainerResources
|
const hostContainers = dockerContainerResources
|
||||||
.filter(c => c.parentId === h.id)
|
.filter(c => c.parentId === h.id)
|
||||||
.map(c => {
|
.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)
|
// Extract original container ID from compound resource ID (hostID/containerID)
|
||||||
// This is needed for sparkline metrics to match the sampler which uses original IDs
|
// 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;
|
const originalContainerId = c.id.includes('/') ? c.id.split('/').pop()! : c.id;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue