feat(webui): Add export-all and per-row copy buttons to search results table (resolves #1980).#2185
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis pull request introduces export functionality for search results. It adds an export handler registration mechanism to the search store, creates a new ActionsHeader component with an export button, provides JSONL formatting utilities for row-level operations, implements a browser-based file download utility, and extends the state management to support configurable export callbacks. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ActionsHeader
participant SearchState as SearchState<br/>(Store)
participant SearchResultsVirtualTable
participant Download as Download<br/>Utility
participant Browser
User->>ActionsHeader: Click export button
ActionsHeader->>SearchState: onSearchResultsExport()
SearchState->>SearchResultsVirtualTable: Invoke registered handler
SearchResultsVirtualTable->>SearchResultsVirtualTable: formatResultsAsJSONL()
SearchResultsVirtualTable->>Download: downloadTextFile(lines, filename)
Download->>Download: Create Blob & Object URL
Download->>Browser: Trigger anchor click for download
Browser->>User: Download JSONL file
Download->>Download: Schedule URL revocation (60s delay)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx`:
- Line 53: In SearchResultsVirtualTable update the conditional that currently
uses the JavaScript negation on the searchResults variable: replace the
`!searchResults` usage with the repo-preferred boolean style by checking
equality against false (i.e., use false == searchResults) and keep the existing
length check (0 === searchResults.length) so the overall condition remains
equivalent; modify the if condition that references searchResults accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 64a8ee11-dcb6-47e7-99ed-b812594e3dd4
📥 Commits
Reviewing files that changed from the base of the PR and between ad31b89 and dffcc2dd9e0953c40e81809945ca1c3a130b0500.
📒 Files selected for processing (1)
components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx
dffcc2d to
98aac00
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx`:
- Around line 56-89: The click handler and handleCopyAll currently allow
unhandled rejections and don't handle unavailable clipboard API; update
handleCopyAll to check navigator.clipboard exists, wrap the writeText call in
try/catch, and call messageApi.error(...) on failure (include a helpful
message), and remove the .catch that rethrows in the Button onClick (or simply
call handleCopyAll() without rethrowing). Refer to handleCopyAll and the Button
onClick/tooltip block to implement these changes so failures are surfaced to
users instead of propagating as unhandled promise rejections.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 514c6e9f-8400-47b0-b5a2-f7b191ba4c49
📥 Commits
Reviewing files that changed from the base of the PR and between dffcc2dd9e0953c40e81809945ca1c3a130b0500 and 98aac007dfe9e26a66848eff4aaed900834aeffe.
📒 Files selected for processing (2)
components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.module.csscomponents/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx
98aac00 to
3f6f169
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx`:
- Line 91: In SearchResultsVirtualTable replace the redundant CSS access
styles["copyButtonContainer"] || "" with the direct property access
styles.copyButtonContainer to simplify and preserve type-safety; locate the div
in the SearchResultsVirtualTable component (the element using className on the
copyButtonContainer) and update its className accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c8ddd0c8-f97c-43fa-98ac-71812c652955
📥 Commits
Reviewing files that changed from the base of the PR and between 98aac007dfe9e26a66848eff4aaed900834aeffe and 3f6f169c3dfcd53ab4a486e2f4238e203191f53b.
📒 Files selected for processing (2)
components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.module.csscomponents/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx
|
Thanks for the contribution - this seems useful addition One concern I have is that the current approach merges all results into a single string before copying to the clipboard. If there are a large number of results, this could put significant pressure on browser memory. Also, Chromium's V8 engine has a string size limit (~536.8M chars. see https://chromium.googlesource.com/v8/v8/+/ef40f0628efc7e9c15b6f28edd69aa76480f064d/src/objects/string.h#531), which we might run into in extreme cases. Would you be open to exploring a slightly different approach by splitting this into two features?
Happy to hear your thoughts |
Adds an 'Export All' button that streams results into a Blob and triggers a file download, avoiding large string concatenation in memory. The button only appears when results are present.
3f6f169 to
3f66363
Compare
thanks for the feedback. updated with export all button. Skipped per log "Copy" as think anyways if someone clicks on a log row, it selects the same and normal copy action would work. |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx (1)
96-96: 🧹 Nitpick | 🔵 TrivialSimplify CSS module access for clarity.
styles["exportButtonContainer"] || ""is redundant;styles.exportButtonContaineris cleaner and maintains type-safety.Suggested cleanup
- <div className={styles["exportButtonContainer"] || ""}> + <div className={styles.exportButtonContainer}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx` at line 96, Replace the redundant CSS-module access in the JSX where the div uses styles["exportButtonContainer"] || "" with the simpler, type-safe dot-access styles.exportButtonContainer; update the element in SearchResultsVirtualTable (index.tsx) to use styles.exportButtonContainer so the code is clearer and removes the unnecessary fallback string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx`:
- Around line 108-113: The tableHeight is passed directly to
VirtualTable.scroll.y and doesn't account for the export button container
(~40px) when hasResults is true; update the code that computes/passes
tableHeight (used with VirtualTable and symbol names like VirtualTable,
tableHeight, hasResults, and searchResultsTableColumns) to subtract 40 (or
Math.max(0, tableHeight - 40)) when hasResults is true, assign it to a new local
name such as adjustedTableHeight, and use adjustedTableHeight for the scroll.y
prop to prevent overflow.
---
Duplicate comments:
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx`:
- Line 96: Replace the redundant CSS-module access in the JSX where the div uses
styles["exportButtonContainer"] || "" with the simpler, type-safe dot-access
styles.exportButtonContainer; update the element in SearchResultsVirtualTable
(index.tsx) to use styles.exportButtonContainer so the code is clearer and
removes the unnecessary fallback string.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4120fd27-d3ab-4214-8d6b-0108026c1147
📥 Commits
Reviewing files that changed from the base of the PR and between 3f6f169c3dfcd53ab4a486e2f4238e203191f53b and 3f66363.
📒 Files selected for processing (2)
components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.module.csscomponents/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx
| <VirtualTable<SearchResult> | ||
| columns={searchResultsTableColumns} | ||
| dataSource={searchResults || []} | ||
| pagination={false} | ||
| rowKey={(record) => record._id} | ||
| scroll={{y: tableHeight, x: "max-content"}}/> |
There was a problem hiding this comment.
Adjust tableHeight to account for the export button container.
When hasResults is true, the export button container consumes approximately 40px (button height + margin). The tableHeight is passed directly to VirtualTable.scroll.y without adjustment, which may cause the table to overflow its parent container.
Proposed fix
+ // Height of export button container (button ~32px + margin-bottom 8px)
+ const EXPORT_BUTTON_CONTAINER_HEIGHT = 40;
+ const adjustedTableHeight = hasResults
+ ? tableHeight - EXPORT_BUTTON_CONTAINER_HEIGHT
+ : tableHeight;
+
return (
<div>
{contextHolder}
{hasResults && (
<div className={styles.exportButtonContainer}>
<Tooltip title={"Export all results as a text file"}>
<Button
icon={<DownloadOutlined/>}
size={"small"}
onClick={handleExportAll}
>
{"Export All"}
</Button>
</Tooltip>
</div>
)}
<VirtualTable<SearchResult>
columns={searchResultsTableColumns}
dataSource={searchResults || []}
pagination={false}
rowKey={(record) => record._id}
- scroll={{y: tableHeight, x: "max-content"}}/>
+ scroll={{y: adjustedTableHeight, x: "max-content"}}/>
</div>
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx`
around lines 108 - 113, The tableHeight is passed directly to
VirtualTable.scroll.y and doesn't account for the export button container
(~40px) when hasResults is true; update the code that computes/passes
tableHeight (used with VirtualTable and symbol names like VirtualTable,
tableHeight, hasResults, and searchResultsTableColumns) to subtract 40 (or
Math.max(0, tableHeight - 40)) when hasResults is true, assign it to a new local
name such as adjustedTableHeight, and use adjustedTableHeight for the scroll.y
prop to prevent overflow.
junhaoliao
left a comment
There was a problem hiding this comment.
Following up on my earlier review - thanks for addressing the clipboard load handling! After thinking about this more, I'd like to suggest a direction that I think would work even better for the overall UX. Happy to hear your thoughts.
Suggested approach
Instead of placing the export button above the table in a separate container, I think it would be cleaner to integrate the export functionality directly into the table as a new rightmost column. This avoids the tableHeight adjustment concern that CodeRabbit flagged and keeps the UI compact.
Here's what I'm thinking:
Here's how the overall layout looks:
Export-all button in the column header
A small icon-only button (DownloadOutlined, size="small", type="text") in the header of a new "Actions" column on the right side of the table. The button is disabled when the query hasn't completed or there are zero results.
Clicking it downloads a JSONL file and shows a success toast:
Per-row "Copy this event" button
Instead of "Copy all" (which has the memory concerns I raised earlier), a per-row copy button in the same Actions column cell. Clicking it copies that single log event as a JSONL line to the clipboard. This keeps each operation O(1) in memory.
Hovering over the copy button shows a tooltip:
Export format: JSONL
Rather than plain text, export as JSONL ({"timestamp": "...", "message": ...}\n...). The timestamp is formatted as an ISO 8601 string — we intentionally use ISO 8601 rather than the human-readable format used in the table display column because it's more immediately recognizable to both humans and automated parsers, and it's the de facto standard for structured log data interchange. The message field is included as a parsed JSON object if it's valid JSON, otherwise as a raw string. The filename would be clp-search-results-<timestamp>.jsonl.
Implementation details
1. New file: components/webui/client/src/utils/download.ts
Shared download utilities. Three exported functions:
import dayjs from "dayjs";
// Defer revocation by this amount because revoking the blob URL immediately after `click()`
// silently aborts downloads on some browsers (e.g., Firefox).
const REVOCATION_DELAY_MS = 60_000;
/**
* Returns a filesystem-safe timestamp string suitable for export filenames.
*
* @return ISO-8601-like string with colons and dots replaced by dashes.
*/
const getExportFilenameTimestamp = (): string => (
dayjs().format("YYYY-MM-DDTHH-mm-ss-SSS[Z]")
);
/**
* Formats a numeric timestamp of an event as an ISO 8601 string.
*
* @param timestamp
* @return ISO 8601 string.
*/
const getExportEventTimestamp = (timestamp: number): string => (
dayjs(timestamp).toISOString()
);
/**
* Triggers a plain-text file download in the browser by creating a temporary
* Blob URL and clicking a hidden anchor element.
*
* @param lines Iterable of strings to write into the file.
* @param filename Name for the downloaded file.
*/
const downloadTextFile = (lines: Iterable<string>, filename: string): void => {
const blob = new Blob([...lines], {type: "text/plain"});
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
setTimeout(() => {
URL.revokeObjectURL(url);
}, REVOCATION_DELAY_MS);
};
export {
downloadTextFile,
getExportEventTimestamp,
getExportFilenameTimestamp,
};getExportFilenameTimestamp()— Produces a filesystem-safe timestamp for filenames (colons and dots replaced with dashes).getExportEventTimestamp(timestamp)— Formats a numeric event timestamp as ISO 8601. We intentionally use ISO 8601 rather than the human-readable display format because it's more recognizable to both humans and automated parsers, and it's the de facto standard for structured log data interchange.downloadTextFile(lines, filename)— Creates a Blob from the lines, creates a temporary object URL, clicks a hidden<a>to trigger the download. Revokes the URL after 60 seconds (revoking immediately afterclick()silently aborts downloads on Firefox).
2. New file: components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/ActionsHeader.tsx
A React component that renders the export-all button inside the Actions column header. It has no props — instead, it reads from the zustand search store. This is separate from typings.tsx to keep the column definitions file focused on types and column config.
import {DownloadOutlined} from "@ant-design/icons";
import {
Button,
Tooltip,
} from "antd";
import useSearchStore from "../../../../SearchState/index";
import {SEARCH_UI_STATE} from "../../../../SearchState/typings";
/**
* Column header component with an export-all button.
*
* @return
*/
const ActionsHeader = () => {
const onSearchResultsExport = useSearchStore((state) => state.onSearchResultsExport);
const numResults = useSearchStore((state) => state.numSearchResultsTable);
const searchUiState = useSearchStore((state) => state.searchUiState);
return (
<Tooltip title={"Export all results as JSONL"}>
<Button
disabled={SEARCH_UI_STATE.DONE !== searchUiState || 0 === numResults}
icon={<DownloadOutlined/>}
size={"small"}
type={"text"}
onClick={onSearchResultsExport}/>
</Tooltip>
);
};
export default ActionsHeader;The button reads numSearchResultsTable and searchUiState from the store. It's disabled when the query hasn't completed (searchUiState !== SEARCH_UI_STATE.DONE) or there are zero results. On click it calls onSearchResultsExport(), which is the callback registered by the parent table component via the store side-channel (see step 3).
3. Modified: components/webui/client/src/pages/SearchPage/SearchState/index.tsx
Add two new members to the SearchState interface:
/**
* Triggers a file download of the current search results. The
* implementation is registered by the component that owns the cursor
* subscription via setOnSearchResultsExport.
*/
onSearchResultsExport: () => void;setOnSearchResultsExport: (fn: (() => void) | null) => void;Important: the implementation is stored in a local closure variable inside the create() callback, not in zustand state. This means setOnSearchResultsExport does not trigger re-renders across all consumers. Change the store creation from the current flat style to one that has a closure:
const useSearchStore = create<SearchState>((set) => {
let onSearchResultsExportImpl: (() => void) | null = null;
return {
...SEARCH_STATE_DEFAULT,
onSearchResultsExport: () => {
onSearchResultsExportImpl?.();
},
setOnSearchResultsExport: (fn) => {
onSearchResultsExportImpl = fn;
},
// ... rest of the existing actions unchanged ...
};
});Also make sure numSearchResultsTable is in SEARCH_STATE_DEFAULT (it's read by ActionsHeader).
4. Modified: components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/typings.tsx
Several additions to this file. The existing SearchResult interface and column definitions stay, with the following additions and adjustments:
Extract formatTimestamp as a named function (was previously an inline arrow in the timestamp column's render prop):
/**
* Formats a numeric timestamp as a UTC datetime string.
*
* @param timestamp
* @return The formatted datetime string.
*/
const formatTimestamp = (timestamp: number): string => (
dayjs.utc(timestamp).format(DATETIME_FORMAT_TEMPLATE)
);Add formatResultAsJsonl — serializes a search result as a single JSON line (no trailing newline). The timestamp uses getExportEventTimestamp() to produce an ISO 8601 string. For the message field: tries JSON.parse — if it succeeds, includes the parsed object; if it throws, includes the raw string.
/**
* Serializes a search result as a JSONL line. If the message field is valid
* JSON it is included as a parsed object; otherwise it is kept as a string.
*
* @param result
* @return A single JSON line (without trailing newline).
*/
const formatResultAsJsonl = (result: SearchResult): string => {
let messageValue: unknown;
try {
messageValue = JSON.parse(result.message);
} catch {
messageValue = result.message;
}
return JSON.stringify({
timestamp: getExportEventTimestamp(result.timestamp),
message: messageValue,
});
};Add renderActionsCell — a column render function for the Actions column. This is intentionally a plain function (not a React component) to satisfy the react/destructuring-assignment lint rule.
/**
* Cell renderer for the Actions column. Renders a per-row copy button.
*
* @param _
* @param record
* @return
*/
const renderActionsCell = (_: unknown, record: SearchResult) => {
const handleCopyRow = () => {
navigator.clipboard.writeText(formatResultAsJsonl(record))
.catch((e: unknown) => {
console.error("Failed to copy search result to clipboard", e);
});
};
return (
<Tooltip title={"Copy this event"}>
<Button
icon={<CopyOutlined/>}
size={"small"}
type={"text"}
onClick={handleCopyRow}/>
</Tooltip>
);
};Add an Actions column at the end of searchResultsTableColumns:
{
align: "right",
key: "actions",
render: renderActionsCell,
title: <ActionsHeader/>,
width: 6,
},Adjust column widths — Timestamp: 15→12, Message: 85→82 to make room for the 6-width Actions column.
New exports — add formatResultAsJsonl and formatTimestamp alongside the existing searchResultsTableColumns.
New imports — add CopyOutlined from @ant-design/icons, Button and Tooltip from antd, getExportEventTimestamp from utils/download, and ActionsHeader from ./ActionsHeader.
5. Modified: components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsx
New imports: add useCallback from react; message from antd; downloadTextFile and getExportFilenameTimestamp from utils/download; formatResultAsJsonl from ./typings; destructure setOnSearchResultsExport from the store.
Add message.useMessage() — provides messageApi and contextHolder for success/error toast notifications.
Add handleExport callback wrapped in useCallback (depends on messageApi and searchResults):
/**
* Exports all search results as a JSONL file download.
*
* NOTE: Results are exported in the original cursor order (i.e., timestamp descending),
* which may differ from the user's current table sort.
*/
const handleExport = useCallback(() => {
if (null === searchResults || 0 === searchResults.length) {
return;
}
try {
downloadTextFile(
searchResults.map((r) => `${formatResultAsJsonl(r)}\n`),
`clp-search-results-${getExportFilenameTimestamp()}.jsonl`
);
messageApi.success(`Exported ${searchResults.length} results`);
} catch (e) {
messageApi.error("Failed to export results");
console.error(e);
}
}, [
messageApi,
searchResults,
]);Register the handler in a useEffect with cleanup:
useEffect(() => {
setOnSearchResultsExport(handleExport);
return () => {
setOnSearchResultsExport(null);
};
}, [
handleExport,
setOnSearchResultsExport,
]);Wrap the return value in a fragment (<>) with {contextHolder} before the VirtualTable.
Remove x: "max-content" from scroll — AntD's virtual table requires scroll.x to be a number; the string value causes a console warning. Just use scroll={{y: tableHeight}}.
What to remove from your PR
index.module.css— the separate container for the export button is no longer needed- The
formatResultLinefunction — replaced byformatResultAsJsonl - The
hasResultsconditional and the button containerdivabove the table
Notes
- Why the store side-channel? The column header (
ActionsHeader) needs to trigger an export, but only the parentSearchResultsVirtualTablehas access to thesearchResultscursor data. Storing the callback in a closure variable inside the zustandcreate()callback (rather than as zustand state) lets us bridge the two components without triggering re-renders on every results update. - Blob URL revocation: revoking immediately after
anchor.click()silently aborts downloads on Firefox. Deferring by 60 seconds avoids that. scroll.x: "max-content"removed: AntD's virtual table requiresscroll.xto be a number.
After we are done with the changes, let's also fix the lint issues as reported by the CI before we proceed to review this again.
If you're short on time and OK with the above direction, I'm also happy to push these changes directly to your branch — just let me know.
…ExportImpl` declaration outside of the store creation
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/typings.tsx`:
- Around line 73-90: renderActionsCell's handleCopyRow should check
navigator.clipboard availability and surface user feedback on success or failure
instead of only logging to console; update handleCopyRow (in renderActionsCell)
to first if (!navigator?.clipboard) show antd message.error like "Clipboard not
available", otherwise call
navigator.clipboard.writeText(formatResultAsJsonl(record)) and in .then show
message.success("Copied"), in .catch show message.error("Failed to copy: " +
error message); use the existing formatResultAsJsonl helper and import/use antd
message (or message.useMessage if you need consistent positioning) to display
the notifications.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5b828e0c-fe9f-4a02-aee4-f3e9936fc1ff
📒 Files selected for processing (6)
components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/ActionsHeader.tsxcomponents/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/index.tsxcomponents/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/typings.tsxcomponents/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/utils.tscomponents/webui/client/src/pages/SearchPage/SearchState/index.tsxcomponents/webui/client/src/utils/download.ts
| const renderActionsCell = (_: unknown, record: SearchResult) => { | ||
| const handleCopyRow = () => { | ||
| navigator.clipboard.writeText(formatResultAsJsonl(record)) | ||
| .catch((e: unknown) => { | ||
| console.error("Failed to copy search result to clipboard", e); | ||
| }); | ||
| }; | ||
|
|
||
| return ( | ||
| <Tooltip title={"Copy this event"}> | ||
| <Button | ||
| icon={<CopyOutlined/>} | ||
| size={"small"} | ||
| type={"text"} | ||
| onClick={handleCopyRow}/> | ||
| </Tooltip> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Add clipboard availability check and user feedback for copy failures.
The per-row copy button doesn't verify navigator.clipboard availability and provides no user feedback on failure—users won't know if the copy failed. This contrasts with the bulk export handler in index.tsx which properly shows error messages.
Proposed fix
+import {message} from "antd";
+
const renderActionsCell = (_: unknown, record: SearchResult) => {
const handleCopyRow = () => {
+ if (null == navigator.clipboard || "function" !== typeof navigator.clipboard.writeText) {
+ message.error("Clipboard is unavailable");
+ return;
+ }
+
navigator.clipboard.writeText(formatResultAsJsonl(record))
- .catch((e: unknown) => {
- console.error("Failed to copy search result to clipboard", e);
- });
+ .then(() => {
+ message.success("Copied to clipboard");
+ })
+ .catch((e: unknown) => {
+ console.error("Failed to copy search result to clipboard", e);
+ message.error("Failed to copy to clipboard");
+ });
};Note: Using message directly from antd import works for quick feedback but doesn't provide the same contextual placement as message.useMessage(). If consistent positioning is important, consider lifting the message API to a shared context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/typings.tsx`
around lines 73 - 90, renderActionsCell's handleCopyRow should check
navigator.clipboard availability and surface user feedback on success or failure
instead of only logging to console; update handleCopyRow (in renderActionsCell)
to first if (!navigator?.clipboard) show antd message.error like "Clipboard not
available", otherwise call
navigator.clipboard.writeText(formatResultAsJsonl(record)) and in .then show
message.success("Copied"), in .catch show message.error("Failed to copy: " +
error message); use the existing formatResultAsJsonl helper and import/use antd
message (or message.useMessage if you need consistent positioning) to display
the notifications.
| * @param result | ||
| * @return A single JSON line (without trailing newline). | ||
| */ | ||
| const formatResultAsJsonl = (result: SearchResult): string => { |
There was a problem hiding this comment.
Why are these functions put in typings.tsx? Shouldn't them be in utils.tsx?
| }, [ | ||
| handleExport, | ||
| setOnSearchResultsExport, | ||
| ]); |
There was a problem hiding this comment.
We usually just put data shared by two components in SearchState. Is it better to just put searchResults in SearchState, and make onSearchResultsExport statically do handleExport? The performance penalty shouldn't be too big since zustand only checks the array.
Co-authored-by: hoophalab <200652805+hoophalab@users.noreply.github.com>
…timestamps and JSONL formatting
…s table (resolves y-scope#1980). (y-scope#2185) Co-authored-by: Naman Goyal <namangg@amazon.com> Co-authored-by: Junhao Liao <junhao.liao@yscope.com> Co-authored-by: hoophalab <200652805+hoophalab@users.noreply.github.com>





Description
Adds an Actions column to the search results virtual table with two capabilities:
Export all — An icon-only download button in the column header exports the full result set as a JSONL file (
clp-search-results-<timestamp>.jsonl). Disabled until the query completes and results are available.Copy event — A per-row copy icon button copies the individual log event (formatted as a single JSONL line) to the clipboard. This avoids the memory concerns of a "copy all" approach by keeping each operation O(1).
Hovering shows a tooltip:
The JSONL format uses ISO 8601 timestamps — intentionally chosen over the human-readable display format because ISO 8601 is more immediately recognizable to both humans and automated parsers, and is the de facto standard for structured log data interchange.
Overall layout:
Key design decisions
tableHeight).ActionsHeader) needs to trigger an export, but only the parentSearchResultsVirtualTablehas access to thesearchResultscursor data. The export callback is stored in a closure variable inside the zustandcreate()callback (not zustand state), so registering it doesn't trigger re-renders.anchor.click()silently aborts downloads on Firefox. Revocation is deferred by 60 seconds.Checklist
Validation performed
npm run lint:check --workspaceswith--max-warnings 0).tsc --noEmit).*query, verified export-all downloads a JSONL file with correct content, verified per-row copy copies a single JSONL line to clipboard, verified button enable/disable states.Summary by CodeRabbit
Release Notes
New Features