Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 308 additions & 1 deletion components/log-viewer-webui/client/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions components/log-viewer-webui/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@
"@sinclair/typebox": "^0.34.25",
"antd": "^5.24.5",
"axios": "^1.7.9",
"highlight.js": "^11.11.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.4.1",
"react-syntax-highlighter": "^15.6.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
"eslint-config-yscope": "latest",
"typescript": "~5.6.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
.searchControlsContainer {
margin-top: 20px;
padding: 0 20px;
display: flex;
gap: 10px;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {LinkOutlined} from "@ant-design/icons";
import {
Tooltip,
Typography,
} from "antd";

import styles from "./index.module.css";


const {Link} = Typography;

// eslint-disable-next-line no-warning-comments
// TODO: Fix link to connect to package log viewer when log viewer setup finished. Also pass
// proper args to package log viewer.
const LOG_VIEWER_URL = "https://y-scope.github.io/yscope-log-viewer/";

interface LogViewerLinkProps {
filePath: string;
}

/**
* Render a link to the log viewer with open file.
*
* @param props
* @param props.filePath
* @return
*/
const LogViewerLink = ({filePath}: LogViewerLinkProps) => (
<Tooltip title={"Open file"}>
<Link
href={LOG_VIEWER_URL}
target={"_blank"}
type={"secondary"}
>
<LinkOutlined className={styles["linkIcon"] || ""}/>
{filePath}
</Link>
</Tooltip>
);

export default LogViewerLink;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.linkIcon {
margin-right: 4px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import SyntaxHighlighter from "react-syntax-highlighter";

import {Typography} from "antd";

import LogViewerLink from "./LogViewerLink";
import {highlighterCustomStyles} from "./utils";

import "highlight.js/styles/intellij-light.css";


const {Text} = Typography;

interface MessageProps {
message: string;
filePath: string;
}

/**
* Renders a message with syntax highlighting and a file path link.
*
* @param props
* @param props.message
* @param props.filePath
* @return
*/
const Message = ({message, filePath}: MessageProps) => {
return (
<>
{/* Parent `Text` component allows syntax highlighter to inherit AntD fonts. */}
<Text>
<SyntaxHighlighter
customStyle={highlighterCustomStyles}
language={"armasm"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider using more appropriate language for log syntax highlighting.

The current implementation uses "armasm" as the language for syntax highlighting, which seems unusual for log messages. Consider using a more appropriate language like "log", "text" or "plaintext".

-                    language={"armasm"}
+                    language={"log"}

🌐 Web query:

What's the best syntax highlighting language to use for log files in react-syntax-highlighter?

💡 Result:

React Syntax Highlighter offers a wide range of language support for syntax highlighting in React applications, but there isn't a specific language explicitly designed for log files. However, several options can be effective for highlighting log files depending on your specific log format.

Best Options for Log File Syntax Highlighting

For log files in react-syntax-highlighter, these languages work particularly well:

Plain Text
The simplest approach is to use "text" as your language option, which will render your logs in monospaced font without specific highlighting[1][7]. This works well for simple logs where you don't need complex syntax highlighting.

<SyntaxHighlighter language="text" style={solarizedlight}>
  {logFileContent}
</SyntaxHighlighter>

Shell/Bash
For command-line or terminal-style logs, the "shell" or "bash" language options provide useful highlighting that can make timestamps, errors, and command outputs more distinguishable[10]. These languages highlight typical patterns found in terminal outputs.

HTTP
If your logs contain HTTP requests and responses, the "http" language option can be particularly useful as it will highlight status codes, headers, and request methods[10].

JavaScript Console
For logs that originate from browser consoles or JavaScript applications, using "javascript" as the language can provide good highlighting for objects, errors, and various data types[5][6].

Customization Options

Regardless of which language you choose, react-syntax-highlighter offers several customization features that can enhance log file readability:

Line Numbers
Adding line numbers helps with referencing specific parts of lengthy logs[5]:

<SyntaxHighlighter 
  language="text" 
  style={solarizedlight}
  showLineNumbers>
  {logFileContent}
</SyntaxHighlighter>

Highlighting Specific Lines
You can highlight important lines (like errors) in your logs using the wrapLines and lineProps options[5]:

const lineProps = (lineNumber) => {
  let style = { display: 'block' };
  if (lineNumber === 2) { // Highlight line containing error
    style.backgroundColor = '#ffdd57';
  }
  return { style };
};

<SyntaxHighlighter
  language="text"
  style={solarizedlight}
  wrapLines
  lineProps={lineProps}>
  {logFileContent}
</SyntaxHighlighter>

Custom Styling
You can further customize the appearance using the customStyle prop to match your application's theme[1][5].

If your log files have a specific format or structure, you might need to experiment with different language options to find the one that best highlights the important parts of your logs. In some cases, you might even consider creating a custom language definition if your logs have a very specific structure that isn't well-served by the existing options.

Citations:


Switch to Plain Text Highlighting for Logs

To improve readability of log output, use the plain text highlighter instead of “armasm”. For example:

• File:
components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/index.tsx
Line: 33

-                    language={"armasm"}
+                    language="text"

If your logs include shell commands, HTTP messages or JavaScript console output, consider using "bash", "http" or "javascript" respectively.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
language={"armasm"}
language="text"

useInlineStyles={false}
>
{message}
</SyntaxHighlighter>
</Text>
<LogViewerLink filePath={filePath}/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aesthetics: it might look cleaner to display the file link in a separate column. Users could also filter or sort based on the file name.

If horizontal space is a concern, we could consider adding buttons to show/hide columns later.

(I don't have full context of the PR, so feel push back)

@davemarco davemarco May 1, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, this is what the old webui did. I do agree the columns are better, but I think this will be later once we setup dynamic columns for all fields in the query results(clp-s only). for now i think better/simpler to match old ui.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, I believe we can add a right pop-up panel (a.k.a. a "drawer") for displaying metadata of a specific log event when selected.

For now, since we intend to make the log viewer links visible (as a feature highlight), it makes sense to leave the links directly in the table. Adding an extra column for file name display would work, but as you mention we could be leaving a lot of gap in the log viewer link cells when the message is too long - it could be a waste of space.

Therefore, for now I believe it is fine to leave the links at the bottom of the message cells.

</>
);
};

export default Message;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Style overrides for the syntax highlighter. "react-syntax-highlighter" lib will not accept
* styles directly from CSS modules.
*/
export const highlighterCustomStyles: React.CSSProperties = {
background: "none",
border: "none",
fontFamily: "inherit",
margin: "0",
padding: "0",
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Table} from "antd";

import {
SearchResult,
searchResultsTableColumns,
} from "./typings";


// eslint-disable-next-line no-warning-comments
// TODO: Replace with values from database once api implemented.
const DUMMY_RESULTS: SearchResult[] = [
{
id: 1,
timestamp: "2023-01-01 12:00:00",
message: "INFO: User login successful for user 'john.doe'.",
filePath: "/var/logs/auth.log",
},
{
id: 2,
timestamp: "2023-01-01 12:01:00",
message: "ERROR: Failed to connect to database 'logs_db'.",
filePath: "/var/logs/db.log",
},
{
id: 3,
timestamp: "2023-01-01 12:02:00",
message: "WARN: Disk space running low on volume '/var/logs'.",
filePath: "/var/logs/system.log",
},
{
id: 4,
timestamp: "2023-01-01 12:03:00",
message: "DEBUG: Processing request ID 12345.",
filePath: "/var/logs/app.log",
},
];
Comment on lines +9 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add pagination controls for future implementation.

The dummy data is well structured and provides a good representation of expected search results. While the TODO comment correctly indicates these will be replaced with database values, consider planning for server-side pagination in the component design.


🌐 Web query:

What are the best practices for implementing server-side pagination with Ant Design Table components?

💡 Result:

Best Practices for Implementing Server-Side Pagination with Ant Design Table

Implementing server-side pagination in Ant Design's Table component ensures efficiency and scalability, especially when working with large datasets. Here are the best practices supported by recent examples and the official Ant Design approach:

1. Maintain Pagination State in React

  • Use React's useState to keep track of pagination details such as current, pageSize, and total.
  • Store the loaded data and loading state in separate state variables for clarity and performance[5][8].

2. Fetch Data Based on Table Pagination Events

  • Use the Table's onChange event to trigger API calls when the user changes the page, page size, or interacts with sorting/filtering features.
  • Pass current page and page size to the backend as query parameters.
  • Update the data and total count when new data is fetched from the server[4][5][8].

3. Configure the Table's Pagination Prop Properly

  • Pass the pagination state to the Table's pagination prop:
    <Table
      columns={columns}
      dataSource={data}
      pagination={{
        current: pagination.current,
        pageSize: pagination.pageSize,
        total: pagination.total,
        showSizeChanger: true
      }}
      loading={loading}
      onChange={handleTableChange}
    />
  • Enable showSizeChanger for user control over page size options[5][8].

4. Efficient Data Fetching and Loading Feedback

  • Show a spinner or loading indicator while data is being fetched by toggling the loading prop of the Table[2][5][8].
  • Optionally, debounce or throttle API calls if pagination changes rapidly.

5. Always Update the Total Count

  • Ensure the total record count (total) is updated in your pagination state after each data fetch. This keeps the page count correct and the pagination UI in sync[5][8].

6. Handle Sorting and Filtering

  • When using server-side sorting or filtering, capture the additional parameters in onChange and pass them to your API.
  • Backend should handle sorting/filtering and return the paginated, sorted, and/or filtered data[4][10].

7. Robust Error Handling

  • Manage and display errors from API failures gracefully, making sure loading states are reset and users are informed if something goes wrong[5][7].

Example Pattern

const [data, setData] = useState([]);
const [pagination, setPagination] = useState({
  current: 1,
  pageSize: 10,
  total: 0
});
const [loading, setLoading] = useState(false);

const fetchData = async (params) => {
  setLoading(true);
  const response = await api.get('/items', {
    params: {
      page: params.pagination.current,
      size: params.pagination.pageSize
    }
  });
  setData(response.data.items);
  setPagination({
    ...params.pagination,
    total: response.data.totalCount
  });
  setLoading(false);
};

useEffect(() => {
  fetchData({ pagination });
}, []);

const handleTableChange = (newPagination, filters, sorter) => {
  fetchData({
    pagination: newPagination,
    filters,
    sorter,
  });
};

<Table
  columns={columns}
  dataSource={data}
  pagination={pagination}
  loading={loading}
  onChange={handleTableChange}
/>

[5][8]

Summary Table: Key Practices

Practice Why It Matters
State management for pagination Keeps UI and data in sync
onChange triggers data fetch Fetches only needed data per page
Show loading during fetch Improves user experience
API returns paginated, total data Ensures pagination UI is correct
Error & empty state handling Prevents UI glitches, informs users
Support for dynamic page sizes Enhances flexibility for users
Pass sorting/filtering to backend Ensures accurate results

By following these best practices, you can efficiently implement server-side pagination in Ant Design Table components, resulting in responsive UIs and scalable performance for large data sets[5][8][4].

Citations:


Plan for server-side pagination in SearchResultsTable

Once the real API is in place, extend the component to support server-side pagination by:

  • Maintaining pagination state (current, pageSize, total) alongside data and loading via useState.
  • Configuring the Ant Design <Table>’s pagination prop with your pagination state and showSizeChanger.
  • Implementing an onChange handler that:
    • Receives pagination, filters, sorter
    • Calls your API with page, size, plus any sorting/filtering params
    • Updates data, total, and resets loading
  • Toggling the Table’s loading prop during fetches for clear feedback.
  • Ensuring the API returns both paginated items and a totalCount.
  • Handling API errors gracefully (reset loading, display a message).

File to update:
• components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/index.tsx (around the DUMMY_RESULTS/TODO block)


/**
* Renders search results in a table.
*
* @return
*/
const SearchResultsTable = () => {
return (
<Table<SearchResult>
columns={searchResultsTableColumns}
dataSource={DUMMY_RESULTS}
pagination={false}
rowKey={(record) => record.id.toString()}
virtual={true}/>
);
};
Comment on lines +38 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance component flexibility with props.

The component is currently hardcoded to use dummy data. Consider enhancing it with props to make it more flexible and reusable:

-const SearchResultsTable = () => {
+interface SearchResultsTableProps {
+    dataSource?: SearchResult[];
+    loading?: boolean;
+    onSortChange?: (field: string, order: 'ascend' | 'descend' | undefined) => void;
+}
+
+const SearchResultsTable = ({
+    dataSource = DUMMY_RESULTS,
+    loading = false,
+    onSortChange,
+}: SearchResultsTableProps) => {
     return (
         <Table<SearchResult>
             columns={searchResultsTableColumns}
-            dataSource={DUMMY_RESULTS}
+            dataSource={dataSource}
+            loading={loading}
+            onChange={(pagination, filters, sorter) => {
+                if (onSortChange && !Array.isArray(sorter)) {
+                    onSortChange(sorter.field as string, sorter.order);
+                }
+            }}
             pagination={false}
             rowKey={(record) => record.id.toString()}
             virtual={true}/>
     );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Renders search results in a table.
*
* @return
*/
const SearchResultsTable = () => {
return (
<Table<SearchResult>
columns={searchResultsTableColumns}
dataSource={DUMMY_RESULTS}
pagination={false}
rowKey={(record) => record.id.toString()}
virtual={true}/>
);
};
/**
* Renders search results in a table.
*
* @return
*/
interface SearchResultsTableProps {
dataSource?: SearchResult[];
loading?: boolean;
onSortChange?: (field: string, order: 'ascend' | 'descend' | undefined) => void;
}
const SearchResultsTable = ({
dataSource = DUMMY_RESULTS,
loading = false,
onSortChange,
}: SearchResultsTableProps) => {
return (
<Table<SearchResult>
columns={searchResultsTableColumns}
dataSource={dataSource}
loading={loading}
onChange={(pagination, filters, sorter) => {
if (onSortChange && !Array.isArray(sorter)) {
onSortChange(sorter.field as string, sorter.order);
}
}}
pagination={false}
rowKey={(record) => record.id.toString()}
virtual={true}
/>
);
};


export default SearchResultsTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {TableProps} from "antd";

import Message from "./Message";


/**
* Structure of search results data displayed in the table.
*/
interface SearchResult {
id: number;
timestamp: string;
message: string;
filePath: string;
}

/**
* Columns configuration for the seach results table.
*/
const searchResultsTableColumns: NonNullable<TableProps<SearchResult>["columns"]> = [
{
dataIndex: "timestamp",
key: "timestamp",
sorter: true,

@hoophalab hoophalab May 1, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something, sorting doesn't seem to work. According to the docs, it looks like sorter is expected to be a function.

@davemarco davemarco May 1, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sorter will be server side, like it will need to launch a new query to the backend, so this is just a dummy sorter until database api is implemented

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sorter will be server side

It makes sense to set the sorter as true. I believe we will need to register a onChange handler on the Table to detect if any sorter changes (i.e., user requests a different sorting order on a column), right?

(I haven't run the code locally yet. Do the sorting indicators (asc / desc) change as we click the table headers?)

We can register the handler and print a log if the sorter changes, though this will be just boilerplate code until we submit the API integration PR. @davemarco I'll let you decide whether you want to add the handler in this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense

(I haven't run the code locally yet. Do the sorting indicators (asc / desc) change as we click the table headers?)

The indicator changes.

title: "Timestamp",
width: 15,
},
{
dataIndex: "message",
key: "message",
render: (_, record) => (
<Message
filePath={record.filePath}
message={record.message}/>
),
title: "Message",
width: 85,
},
];

export type {SearchResult};
export {searchResultsTableColumns};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.searchPageContainer {
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px 16px 16px;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import styles from "./index.module.css";
import SearchControls from "./SearchControls";
import SearchResultsTable from "./SearchResults/SearchResultsTable";


/**
Expand All @@ -8,7 +10,10 @@ import SearchControls from "./SearchControls";
*/
const SearchPage = () => {
return (
<SearchControls/>
<div className={styles["searchPageContainer"]}>
<SearchControls/>
<SearchResultsTable/>
</div>
);
};

Expand Down