mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 12:17:34 +00:00
feat: match the Security masthead to the primary-page hero and make History search realtime (#1365)
- Masthead: add `size` and `subtitle` to PageMasthead and render the Security masthead at the hero title size with a one-line posture summary, so its height and weight match the Home and Fleet mastheads. The compact size with no subtitle stays the default, so Settings, Console, and Logs are unchanged. - History search now applies as you type (a short debounce, no Enter press), matching the Fleet overview search. It stays server-side so the query searches every completed scan, not just the loaded page.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -98,6 +98,18 @@ export function HistoryTab({ onInspect }: HistoryTabProps) {
|
||||
|
||||
useEffect(() => { void load(safePage, search); }, [load, safePage, search, nodeId]);
|
||||
|
||||
// Realtime search: apply the draft automatically a beat after typing stops (no
|
||||
// Enter), then refetch from page 0. Debounced rather than per-keystroke because
|
||||
// the list is server-paginated, so the query searches every completed scan, not
|
||||
// just the loaded page. Skip the initial mount so it does not reset pagination.
|
||||
const prevSearchDraftRef = useRef(searchDraft);
|
||||
useEffect(() => {
|
||||
if (prevSearchDraftRef.current === searchDraft) return;
|
||||
prevSearchDraftRef.current = searchDraft;
|
||||
const t = setTimeout(() => { setSearch(searchDraft); setPage(0); }, 250);
|
||||
return () => clearTimeout(t);
|
||||
}, [searchDraft]);
|
||||
|
||||
const toggleSelect = (scanId: number) => {
|
||||
setSelected((prev) => {
|
||||
if (prev.includes(scanId)) return prev.filter((x) => x !== scanId);
|
||||
@@ -157,7 +169,6 @@ export function HistoryTab({ onInspect }: HistoryTabProps) {
|
||||
placeholder="Search by image..."
|
||||
value={searchDraft}
|
||||
onChange={(e) => setSearchDraft(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { setPage(0); setSearch(searchDraft); } }}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -125,11 +125,11 @@ describe('HistoryTab', () => {
|
||||
expect(screen.getByRole('button', { name: /Compare \(2\/2\)/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('searches by image on Enter, adding imageRefLike to the request', async () => {
|
||||
it('searches by image as you type (no Enter), adding imageRefLike to the request', async () => {
|
||||
mockedFetch.mockResolvedValue(listResponse([scan({ image_ref: 'alpine:3.19' })]));
|
||||
render(<HistoryTab onInspect={vi.fn()} />);
|
||||
await waitFor(() => expect(screen.getByText('alpine:3.19')).toBeInTheDocument());
|
||||
await userEvent.type(screen.getByPlaceholderText('Search by image...'), 'redis{Enter}');
|
||||
await userEvent.type(screen.getByPlaceholderText('Search by image...'), 'redis');
|
||||
await waitFor(() => {
|
||||
const calls = mockedFetch.mock.calls.map((c) => c[0] as string);
|
||||
expect(calls.some((u) => u.includes('imageRefLike=redis'))).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user