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:
2026-04-10 15:09:05 +03:00
parent fd655c3253
commit 2046e64428
9 changed files with 119 additions and 9 deletions
+39 -3
View File
@@ -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>
)}
</>