mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 04:06:59 +00:00
feat(search): add global Ctrl+K command palette (#711)
* feat(search): add global command palette with cross-node stacks Press Ctrl+K from anywhere to open a search palette that jumps to pages, switches nodes, or opens stacks on any online node in the fleet. Trigger lives as an icon in the top bar. Replaces the former sidebar-scoped Ctrl+K handler. The cross-node stack search fan-out is extracted into a shared hook so the sidebar and palette stay in sync on the same debounce + abort shape. * fix(search): drop render-time ref write and effect-based query reset Replace `nodesRef.current = nodes` during render with direct use of `nodes` in the stack select callback, and fold the query-reset logic into a single `onOpenChange` handler so it no longer runs via an effect. Both changes resolve react-hooks rule violations that broke frontend lint in CI.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNodes } from '@/context/NodeContext';
|
||||
import { fetchForNode } from '@/lib/api';
|
||||
|
||||
export type StackStatus = 'running' | 'exited' | 'unknown';
|
||||
|
||||
export interface StackStatusInfo {
|
||||
status: StackStatus;
|
||||
}
|
||||
|
||||
export interface StackHit {
|
||||
nodeId: number;
|
||||
nodeName: string;
|
||||
file: string;
|
||||
status: StackStatus;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
query: string;
|
||||
enabled: boolean;
|
||||
excludeNodeId?: number;
|
||||
}
|
||||
|
||||
const DEBOUNCE_MS = 250;
|
||||
|
||||
export function useCrossNodeStackSearch({ query, enabled, excludeNodeId }: Options) {
|
||||
const { nodes } = useNodes();
|
||||
const [hits, setHits] = useState<StackHit[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Ref avoids re-running the effect on every NodeContext status tick
|
||||
const nodesRef = useRef(nodes);
|
||||
nodesRef.current = nodes;
|
||||
|
||||
useEffect(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!enabled || !q) {
|
||||
setHits([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const targets = nodesRef.current.filter(
|
||||
n => n.status !== 'offline' && n.id !== excludeNodeId,
|
||||
);
|
||||
if (targets.length === 0) {
|
||||
setHits([]);
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const perNode = await Promise.all(targets.map(async (node) => {
|
||||
try {
|
||||
const [listRes, statusRes] = await Promise.all([
|
||||
fetchForNode('/stacks', node.id, { signal: controller.signal }),
|
||||
fetchForNode('/stacks/statuses', node.id, { signal: controller.signal }),
|
||||
]);
|
||||
if (!listRes.ok) return [] as StackHit[];
|
||||
const rawList = await listRes.json();
|
||||
const files: string[] = Array.isArray(rawList) ? rawList : [];
|
||||
const statuses: Record<string, StackStatus> = {};
|
||||
if (statusRes.ok) {
|
||||
const raw = await statusRes.json();
|
||||
for (const [key, val] of Object.entries(raw)) {
|
||||
if (typeof val === 'string') {
|
||||
statuses[key] = val as StackStatus;
|
||||
} else if (val && typeof val === 'object' && 'status' in val) {
|
||||
statuses[key] = (val as StackStatusInfo).status;
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
.filter(f => f.toLowerCase().includes(q))
|
||||
.map<StackHit>(file => ({
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
file,
|
||||
status: statuses[file] ?? 'unknown',
|
||||
}));
|
||||
} catch {
|
||||
return [] as StackHit[];
|
||||
}
|
||||
}));
|
||||
if (controller.signal.aborted) return;
|
||||
setHits(perNode.flat());
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
controller.abort();
|
||||
};
|
||||
}, [enabled, query, excludeNodeId]);
|
||||
|
||||
return { hits, loading };
|
||||
}
|
||||
Reference in New Issue
Block a user