Skip to content

Commit 34ec7ee

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
feat(webapp): search logs in adaptive time slices
Search logs incrementally in adaptive newest-first time slices. Preserve infinite scrolling for full pages and show progress with manual continuation when an underfilled page reaches its automatic search budget. Keep earlier results visible if a search times out or its remaining range expires, without claiming completion or skipping failed intervals. Mono-RevId: 2d9d618e308b856d9c5850f21c44729543c270e8
1 parent 1615ec7 commit 34ec7ee

14 files changed

Lines changed: 1455 additions & 394 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Log search uses whole-word matching for ASCII alphabetic terms, keeps substring matching for other terms, and incrementally searches older logs without blocking the page.

‎apps/webapp/app/components/logs/LogsTable.tsx‎

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ type LogsTableProps = {
3232
isLoading?: boolean;
3333
isLoadingMore?: boolean;
3434
hasMore?: boolean;
35+
isIncomplete?: boolean;
3536
onLoadMore?: () => void;
36-
onCheckForMore?: () => void;
37+
showKeepSearching?: boolean;
38+
onKeepSearching?: () => void;
39+
searchedTo?: string;
3740
variant?: TableVariant;
3841
selectedLogId?: string;
3942
onLogSelect?: (logId: string) => void;
@@ -62,8 +65,11 @@ export function LogsTable({
6265
isLoading = false,
6366
isLoadingMore = false,
6467
hasMore = false,
68+
isIncomplete = false,
6569
onLoadMore,
66-
onCheckForMore,
70+
showKeepSearching = false,
71+
onKeepSearching,
72+
searchedTo,
6773
selectedLogId,
6874
onLogSelect,
6975
}: LogsTableProps) {
@@ -88,30 +94,25 @@ export function LogsTable({
8894
return () => clearTimeout(timer);
8995
}, [isLoadingMore]);
9096

91-
// Intersection observer for infinite scroll
9297
useEffect(() => {
93-
if (!hasMore || isLoadingMore || !onLoadMore) return;
98+
if (isLoading || !hasMore || isLoadingMore || showKeepSearching || !onLoadMore) return;
9499

100+
const target = loadMoreRef.current;
101+
if (!target) return;
102+
103+
let active = true;
95104
const observer = new IntersectionObserver(
96105
(entries) => {
97-
if (entries[0].isIntersecting) {
98-
onLoadMore();
99-
}
106+
if (active && entries[0]?.isIntersecting) onLoadMore();
100107
},
101108
{ threshold: 0.1 }
102109
);
103-
104-
const currentRef = loadMoreRef.current;
105-
if (currentRef) {
106-
observer.observe(currentRef);
107-
}
108-
110+
observer.observe(target);
109111
return () => {
110-
if (currentRef) {
111-
observer.unobserve(currentRef);
112-
}
112+
active = false;
113+
observer.disconnect();
113114
};
114-
}, [hasMore, isLoadingMore, onLoadMore]);
115+
}, [hasMore, isLoading, isLoadingMore, onLoadMore, showKeepSearching]);
115116

116117
return (
117118
<div className="relative h-full overflow-auto border-t scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
@@ -133,7 +134,11 @@ export function LogsTable({
133134
</TableHeader>
134135
<TableBody>
135136
{logs.length === 0 ? (
136-
<BlankState isLoading={isLoading} onRefresh={() => window.location.reload()} />
137+
<BlankState
138+
isLoading={isLoading}
139+
isPartial={hasMore || isIncomplete}
140+
onRefresh={() => window.location.reload()}
141+
/>
137142
) : (
138143
logs.map((log) => {
139144
const isSelected = selectedLogId === log.id;
@@ -149,7 +154,7 @@ export function LogsTable({
149154

150155
return (
151156
<TableRow
152-
key={log.id}
157+
key={log.projectionFingerprint ?? log.id}
153158
className={cn(
154159
"cursor-pointer transition-colors",
155160
isSelected ? "bg-background-hover" : "hover:bg-background-dimmed"
@@ -200,27 +205,41 @@ export function LogsTable({
200205
)}
201206
</TableBody>
202207
</Table>
203-
{/* Infinite scroll trigger */}
204-
{hasMore && logs.length > 0 && (
208+
{(hasMore || logs.length > 0 || isIncomplete) && (
205209
<div ref={loadMoreRef} className="flex items-center justify-center py-12">
206-
<div className={cn("flex items-center gap-2", !showLoadMoreSpinner && "invisible")}>
207-
<Spinner /> <span className="text-text-dimmed">Loading more…</span>
208-
</div>
209-
</div>
210-
)}
211-
{/* Show all logs message with check for more button */}
212-
{!hasMore && logs.length > 0 && (
213-
<div className="flex items-center justify-center py-12">
214210
<div className="flex flex-col items-center gap-3">
215-
<span className="text-text-dimmed">Showing all {logs.length} logs</span>
211+
{searchedTo && <span className="text-text-dimmed">Searched back to {searchedTo}</span>}
212+
{hasMore ? (
213+
isLoadingMore ? (
214+
<div className={cn("flex items-center gap-2", !showLoadMoreSpinner && "invisible")}>
215+
<Spinner /> <span className="text-text-dimmed">Searching…</span>
216+
</div>
217+
) : showKeepSearching ? (
218+
<Button variant="tertiary/medium" onClick={onKeepSearching}>
219+
Keep searching
220+
</Button>
221+
) : null
222+
) : isIncomplete ? (
223+
<span className="text-text-dimmed">Search incomplete</span>
224+
) : (
225+
<span className="text-text-dimmed">Showing all {logs.length} logs</span>
226+
)}
216227
</div>
217228
</div>
218229
)}
219230
</div>
220231
);
221232
}
222233

223-
function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?: () => void }) {
234+
function BlankState({
235+
isLoading,
236+
isPartial,
237+
onRefresh,
238+
}: {
239+
isLoading?: boolean;
240+
isPartial?: boolean;
241+
onRefresh?: () => void;
242+
}) {
224243
if (isLoading) return <TableBlankRow colSpan={6} />;
225244

226245
const handleRefresh = onRefresh ?? (() => window.location.reload());
@@ -229,13 +248,17 @@ function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?:
229248
<TableBlankRow colSpan={6}>
230249
<div className="flex flex-col items-center justify-center gap-6">
231250
<Paragraph className="w-auto" variant="base/bright">
232-
No logs match your filters. Try refreshing or modifying your filters.
251+
{isPartial
252+
? "No matching logs found in the searched range yet."
253+
: "No logs match your filters. Try refreshing or modifying your filters."}
233254
</Paragraph>
234-
<div className="flex items-center gap-2">
235-
<Button LeadingIcon={ArrowPathIcon} variant="tertiary/medium" onClick={handleRefresh}>
236-
Refresh
237-
</Button>
238-
</div>
255+
{!isPartial && (
256+
<div className="flex items-center gap-2">
257+
<Button LeadingIcon={ArrowPathIcon} variant="tertiary/medium" onClick={handleRefresh}>
258+
Refresh
259+
</Button>
260+
</div>
261+
)}
239262
</div>
240263
</TableBlankRow>
241264
);

0 commit comments

Comments
 (0)