feat(runs): add log search with backend LIKE filter and pagination
- add Search pill component with lucide icon and focus highlight - wire debounced search input to GET /run/:id?q= backend filter - backend filters run logs with LIKE %q% via TypeORM - add sectionHeadingRow layout for heading + search alignment - add i18n keys: runs.step_title, logs_search_placeholder
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
Card,
|
||||
DescriptionList,
|
||||
Notification,
|
||||
Pagination,
|
||||
Search,
|
||||
Table,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
@@ -67,6 +69,11 @@ export function RunDetailPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const [pulseKey, setPulseKey] = useState(0);
|
||||
const [logPage, setLogPage] = useState(1);
|
||||
const [logSearch, setLogSearch] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
const logSearchRef = useRef('');
|
||||
const LOG_PAGE_SIZE = 25;
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const FINAL: ScenarioRunStatus[] = ['pass', 'fail'];
|
||||
@@ -79,6 +86,20 @@ export function RunDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
setDebouncedSearch(logSearch);
|
||||
logSearchRef.current = logSearch;
|
||||
setLogPage(1);
|
||||
}, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [logSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !runId || polling) return;
|
||||
runs.get(id, runId, debouncedSearch).then(setRun).catch(() => undefined);
|
||||
}, [debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !runId) return;
|
||||
let cancelled = false;
|
||||
@@ -92,7 +113,7 @@ export function RunDetailPage() {
|
||||
setPolling(true);
|
||||
pollRef.current = setInterval(async () => {
|
||||
try {
|
||||
const updated = await runs.get(id, runId);
|
||||
const updated = await runs.get(id, runId, logSearchRef.current);
|
||||
if (cancelled) return;
|
||||
setRun(updated);
|
||||
setPulseKey((k) => k + 1);
|
||||
@@ -215,14 +236,29 @@ export function RunDetailPage() {
|
||||
|
||||
{run.logs.length > 0 && (
|
||||
<div className={styles.stepsSection}>
|
||||
<h2 className={styles.sectionHeading}>{t('runs.logs_heading')}</h2>
|
||||
<div className={styles.sectionHeadingRow}>
|
||||
<h2 className={styles.sectionHeading}>{t('runs.logs_heading')}</h2>
|
||||
<Search
|
||||
value={logSearch}
|
||||
onChange={setLogSearch}
|
||||
placeholder={t('runs.logs_search_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
columns={logColumns}
|
||||
data={run.logs}
|
||||
data={run.logs.slice((logPage - 1) * LOG_PAGE_SIZE, logPage * LOG_PAGE_SIZE)}
|
||||
rowKey={(l) => l.id}
|
||||
loading={false}
|
||||
emptyMessage=""
|
||||
/>
|
||||
{run.logs.length > LOG_PAGE_SIZE && (
|
||||
<Pagination
|
||||
page={logPage}
|
||||
pageSize={LOG_PAGE_SIZE}
|
||||
total={run.logs.length}
|
||||
onPageChange={setLogPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user