Skip to content

feat: Implemented log search functionality with highlighted results #647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

harshit078
Copy link

@harshit078 harshit078 commented Apr 21, 2025

Linked Issue(s)

#556

Acceptance Criteria fulfillment

  • Added Search in Logs
  • Improved Search UI and functionality

Proposed changes

  • Added Search functionality in logs
  • Added highlighted logs with ease to scroll through the list of selected logs
Screen.Recording.2025-04-21.at.4.27.38.PM.mov
Screenshot 2025-04-21 at 4 27 24 PM

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added a search bar for system logs with dynamic text highlighting of matching terms in log entries.
    • Introduced navigation controls to cycle through search results, showing current and total matches.
    • Log entries now highlight all occurrences of the search query for better readability.
  • Improvements

    • Improved scrolling behavior with smooth scroll-to-bottom functionality.
    • Enhanced highlight visuals for active search matches within logs.

Copy link

coderabbitai bot commented Apr 21, 2025

Walkthrough

The changes introduce a comprehensive search and navigation feature to the system logs interface. A new LogSearch component provides a search input with navigation controls, allowing users to find and cycle through matching log entries. Both PlainLog and FormattedLog components are updated to highlight text matching the search query. The main SystemLogs component is enhanced with state and logic to manage search queries, highlight matches, and handle smooth scrolling to matched entries. Scroll detection is refactored from using an IntersectionObserver to a scroll-to-bottom function, simplifying the logic for displaying the scroll down button.

Changes

File(s) Change Summary
web-server/src/components/Service/SystemLog/FormattedLog.tsx
web-server/src/components/Service/SystemLog/PlainLog.tsx
Added a HighlightedText helper component to highlight search matches; updated main components to accept an optional searchQuery prop and render highlighted text accordingly.
web-server/src/components/Service/SystemLog/LogSearch.tsx Introduced a new LogSearch component with search input, navigation controls, and callbacks for searching and navigating between results.
web-server/src/content/Service/SystemLogs.tsx Enhanced the SystemLogs component with search and navigation logic, state management for search and highlights, integration of LogSearch, replaced IntersectionObserver with a scroll-to-bottom function, and updated scroll button handler.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LogSearch
    participant SystemLogs
    participant LogEntry (PlainLog/FormattedLog)
    
    User->>LogSearch: Types search query / uses navigation controls
    LogSearch->>SystemLogs: onSearch(query) / onNavigate(direction)
    SystemLogs->>SystemLogs: Update search state, find matches, update highlights
    SystemLogs->>LogEntry: Pass searchQuery prop
    LogEntry->>LogEntry: Highlight matching text
    SystemLogs->>SystemLogs: Scroll to highlighted match (if navigating)
Loading

Possibly related PRs

  • feat(system_logs): Fixed auto scroll #630: Refactors and replaces the IntersectionObserver-based scroll detection and scroll down button logic in SystemLogs.tsx, directly modifying the same scrolling behavior and UI elements as this PR.

Poem

In the warren of logs where the data flows,
A bunny now searches, and every match glows.
With highlights of yellow, and scrolls oh-so-smooth,
Hopping through entries, each click on the move.
Search, skip, and find—what a clever new trick!
Logs now respond, and the answers come quick.
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
web-server/src/components/Service/SystemLog/FormattedLog.tsx (1)

13-30: 🛠️ Refactor suggestion

Factor‑out HighlightedText to avoid code duplication

See earlier comment in PlainLog.tsx.

🧹 Nitpick comments (4)
web-server/src/components/Service/SystemLog/PlainLog.tsx (1)

3-7: Unused prop: index

index is accepted in PlainLogProps but never used in the component. Either remove it from the prop list or use it (e.g. as key when mapping). Keeping unused props increases cognitive load.

web-server/src/components/Service/SystemLog/LogSearch.tsx (2)

7-16: gap has no effect without display:flex (or grid)

SearchContainer sets gap: 5 but the default display is block, so the property is ignored. Add display: flex (or grid) to get the intended spacing:

-const SearchContainer = styled('div')(() => ({
+const SearchContainer = styled('div')(() => ({
   position: 'sticky',
   top: 0,
   zIndex: 1,
   gap: 5,
+  display: 'flex',
   paddingBottom: 8,
   alignItems: 'center',
   backdropFilter: 'blur(10px)',
   borderRadius: 5,
 }));

64-71: Prevent the default “Enter” behaviour in the text field

Pressing in a single‑line TextField can submit the surrounding form (if any) or trigger an unintended click on the first <button> in the DOM. Add event.preventDefault() after handling navigation.

-      if (event.key === 'Enter' && event.shiftKey) {
+      if (event.key === 'Enter' && event.shiftKey) {
+        event.preventDefault();
         onNavigate('prev');
       } else if (event.key === 'Enter') {
+        event.preventDefault();
         onNavigate('next');
       }
web-server/src/content/Service/SystemLogs.tsx (1)

75-78: Use a data‑attribute instead of inline style to mark matches

Selecting by span[style*="background-color: yellow"] can yield false positives and is brittle if the style changes. When rendering highlights, add data-log-match and query via [data-log-match] to decouple logic from styling.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a545c0 and 3a3abb0.

📒 Files selected for processing (4)
  • web-server/src/components/Service/SystemLog/FormattedLog.tsx (2 hunks)
  • web-server/src/components/Service/SystemLog/LogSearch.tsx (1 hunks)
  • web-server/src/components/Service/SystemLog/PlainLog.tsx (1 hunks)
  • web-server/src/content/Service/SystemLogs.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
web-server/src/components/Service/SystemLog/FormattedLog.tsx (1)
web-server/src/types/resources.ts (1)
  • ParsedLog (1056-1062)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
web-server/src/content/Service/SystemLogs.tsx (1)

75-87: ⚠️ Potential issue

Critical issue: useEffect inside a callback function

React hooks can only be called at the top level of your component or other hooks, not inside regular JavaScript functions, conditions, or loops. The useEffect hook inside your handleSearch callback will cause runtime errors.

Move this effect to the component's top level:

- const handleSearch = useCallback((query: string) => {
-   setSearchQuery(query);
-   setCurrentMatch(0);
-   setTotalMatches(0);
-   setHighlightedElements([]);
-   updateHighlight(null);
-
-   if (!query) return;
-
-   useEffect(() => {
-     const elements = Array.from(
-       containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
-     ) as HTMLElement[];
-     setHighlightedElements(elements);
-     setTotalMatches(elements.length);
-     setCurrentMatch(elements.length ? 1 : 0);
-
-     if (elements.length) {
-       updateHighlight(elements[0]);
-       elements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
-     }
-   }, [searchQuery, logs, updateHighlight]);
-
- }, []);
+ const handleSearch = useCallback((query: string) => {
+   setSearchQuery(query);
+   setCurrentMatch(0);
+   setTotalMatches(0);
+   setHighlightedElements([]);
+   updateHighlight(null);
+ }, [updateHighlight]);
+
+ useEffect(() => {
+   if (!searchQuery) return;
+   
+   const elements = Array.from(
+     containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
+   ) as HTMLElement[];
+   setHighlightedElements(elements);
+   setTotalMatches(elements.length);
+   setCurrentMatch(elements.length ? 1 : 0);
+
+   if (elements.length) {
+     updateHighlight(elements[0]);
+     elements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
+   }
+ }, [searchQuery, logs, updateHighlight]);
🧹 Nitpick comments (1)
web-server/src/content/Service/SystemLogs.tsx (1)

51-64: Consider using CSS classes for highlights instead of inline styles

Using inline styles for highlights works, but it can be harder to maintain. Consider using CSS classes with defined styles in your stylesheet.

- if (currentHighlightRef.current) {
-   currentHighlightRef.current.style.backgroundColor = 'yellow';
-   currentHighlightRef.current.style.color = 'black';
- }
-
- if (element) {
-   element.style.backgroundColor = 'rgb(255, 161, 10)';
-   element.style.color = 'white';
-   currentHighlightRef.current = element;
- } else {
-   currentHighlightRef.current = null;
- }
+ if (currentHighlightRef.current) {
+   currentHighlightRef.current.classList.remove('active-highlight');
+   currentHighlightRef.current.classList.add('search-highlight');
+ }
+
+ if (element) {
+   element.classList.remove('search-highlight');
+   element.classList.add('active-highlight');
+   currentHighlightRef.current = element;
+ } else {
+   currentHighlightRef.current = null;
+ }

And add these CSS classes to your stylesheet:

.search-highlight {
  background-color: yellow;
  color: black;
}

.active-highlight {
  background-color: rgb(255, 161, 10);
  color: white;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3abb0 and 940f740.

📒 Files selected for processing (1)
  • web-server/src/content/Service/SystemLogs.tsx (4 hunks)
🔇 Additional comments (7)
web-server/src/content/Service/SystemLogs.tsx (7)

128-137: Properly implemented container ref on scrollable element

Great job fixing the container ref issue from the previous review! The containerRef is now correctly attached to the scrollable FlexBox, which will allow the scroll logic to work properly.


91-107: The navigation logic works well

The navigation function properly handles cycling through matches in both directions. It updates the current match index, scrolls to the selected element, and updates the highlight styling.


33-36: Simplified scroll detection logic

Good simplification of the scroll detection logic! Using a direct scroll event listener is more straightforward than an IntersectionObserver for this use case.


133-134: Properly integrated search query with log components

The search query is correctly passed down to both PlainLog and FormattedLog components, which will enable highlighting of matched text in the logs.

Also applies to: 135-136


116-121: Well-integrated search UI component

The LogSearch component is properly integrated with all the necessary props for search functionality and navigation.


42-49: Smooth scrolling implementation

The scrollToBottom function properly implements smooth scrolling behavior to enhance the user experience.


77-77: 🛠️ Refactor suggestion

Potential null reference issue

The optional chaining on containerRef.current?.querySelectorAll protects against null, but if the component hasn't rendered yet, there won't be any elements to query.

Add a check to ensure the search is only performed when the container is available:

- const elements = Array.from(
-   containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
- ) as HTMLElement[];
+ const elements = containerRef.current
+   ? Array.from(containerRef.current.querySelectorAll('span[style*="background-color: yellow"]')) as HTMLElement[]
+   : [];

Likely an incorrect or invalid review comment.

@samad-yar-khan
Copy link
Contributor

samad-yar-khan commented Apr 23, 2025

from the looks of it is just highlighting the logs, not actually searching through them or filtering the search results. Seems satisfactory from the issue. But can we make this more usable ?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
web-server/src/content/Service/SystemLogs.tsx (1)

154-167: 🛠️ Refactor suggestion

Remove direct reference to DOM elements in JSX calculation.

Accessing DOM properties directly in render calculations can cause issues during SSR and is generally not a React best practice. Move this calculation to a useEffect or memoized value.

+ const [buttonMarginLeft, setButtonMarginLeft] = useState(0);
+
+ useEffect(() => {
+   if (containerRef.current) {
+     setButtonMarginLeft(containerRef.current.clientWidth / 2 - 67);
+   }
+ }, []);

  // In the JSX
  <Button
    onClick={scrollToBottom}
    component={MotionBox}
    initial={{ opacity: 0, scale: 0.7 }}
    animate={{ opacity: 1, scale: 1 }}
    transition={{
      ease: 'easeOut'
    }}
    bottom={20}
    sx={{
      position: 'fixed',
-     marginLeft: `calc(${containerRef.current
-         ? containerRef.current.clientWidth / 2 - 67
-         : 0
-       }px)`
+     marginLeft: `${buttonMarginLeft}px`
    }}
  >
♻️ Duplicate comments (1)
web-server/src/content/Service/SystemLogs.tsx (1)

69-84: DOM query for highlighted elements is brittle and should be improved.

The current approach of querying elements by their inline style attribute is fragile. React might reformat or reorder style properties, causing the selector to fail. This was noted in a previous review comment as well.

As suggested in the previous review, use a dedicated class or data attribute for highlighted elements and update your query selector:

- const elements = Array.from(
-   containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
- ) as HTMLElement[];
+ const elements = Array.from(
+   containerRef.current?.querySelectorAll('span.log-highlight') ?? []
+ ) as HTMLElement[];

Make sure to add the corresponding class to the highlighted spans in both PlainLog and FormattedLog components.

🧹 Nitpick comments (2)
web-server/src/content/Service/SystemLogs.tsx (2)

54-67: Consider debouncing the search input to improve performance.

For an improved user experience, consider adding debounce to prevent excessive processing during rapid typing. This is especially important since you're performing DOM operations in response to search input.

+import { debounce } from 'lodash';
+
 const SystemLogs = memo(({ serviceName }: { serviceName?: ServiceNames }) => {
   // ... existing code
 
-  const handleSearch = useCallback((query: string) => {
+  const debouncedSearch = useCallback(
+    debounce((query: string) => {
+      // Reset states only when query is empty or below a threshold
+      if (!query || query.length < 3) {
+        setCurrentMatch(0);
+        setTotalMatches(0);
+        setHighlightedElements([]);
+        updateHighlight(null);
+        return;
+      }
+      // Let the effect handle DOM updates
+    }, 300),
+    [updateHighlight]
+  );
+
+  const handleSearch = useCallback((query: string) => {
     setSearchQuery(query);
-  
-    // Reset states only when query is empty or below a threshold
-    if (!query || query.length < 3) {
-      setCurrentMatch(0);
-      setTotalMatches(0);
-      setHighlightedElements([]);
-      updateHighlight(null);
-      return;
-    }
-  
-    // Let the effect handle DOM updates
+    debouncedSearch(query);
   }, [updateHighlight]);

87-103: Add error handling for navigation edge cases.

The navigation function works well, but lacks error handling for edge cases. Consider adding checks to ensure the element still exists in the DOM before attempting to scroll to it.

 const handleNavigate = useCallback((direction: 'prev' | 'next') => {
   if (highlightedElements.length === 0) return;

   let newIndex = currentMatch;
   if (direction === 'next') {
     newIndex = currentMatch < totalMatches ? currentMatch + 1 : 1;
   } else {
     newIndex = currentMatch > 1 ? currentMatch - 1 : totalMatches;
   }

   setCurrentMatch(newIndex);
   const element = highlightedElements[newIndex - 1];
-  if (element) {
+  if (element && document.body.contains(element)) {
     element.scrollIntoView({ behavior: 'smooth', block: 'center' });
     updateHighlight(element);
+  } else {
+    // Element no longer in DOM, re-query highlighted elements
+    const freshElements = Array.from(
+      containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
+    ) as HTMLElement[];
+    setHighlightedElements(freshElements);
+    if (freshElements.length) {
+      setTotalMatches(freshElements.length);
+      setCurrentMatch(1);
+      updateHighlight(freshElements[0]);
+      freshElements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
+    }
   }
 }, [currentMatch, totalMatches, highlightedElements, updateHighlight]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 901cd26 and e2e5018.

📒 Files selected for processing (2)
  • web-server/src/components/Service/SystemLog/LogSearch.tsx (1 hunks)
  • web-server/src/content/Service/SystemLogs.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web-server/src/components/Service/SystemLog/LogSearch.tsx
🔇 Additional comments (8)
web-server/src/content/Service/SystemLogs.tsx (8)

3-3: Component optimization with React.memo and new imports look good.

The component is properly wrapped with memo for performance optimization, and all necessary hooks and components are imported. This will prevent unnecessary re-renders when the serviceName prop doesn't change.

Also applies to: 8-8, 19-19, 175-175


22-28: State management for search functionality is well-structured.

The state variables are appropriately separated for different aspects of the search functionality. Good use of the ref for tracking the currently highlighted element across renders.


30-37: Clean implementation of scrollToBottom function.

The scroll functionality is simplified with direct scrollTo calls, which is more straightforward than the previous approach.


39-52: The highlight management callback looks good.

The updateHighlight callback effectively toggles between the default yellow background and the distinct orange highlight for the active match.


105-115: Initial scroll implementation looks good.

The initial scroll implementation now correctly uses a ref to track the initial load and requestAnimationFrame to ensure the DOM is ready before scrolling.


117-125: Memoized renderLogs function is a good optimization.

Extracting the logs rendering logic into a memoized callback is a good optimization that prevents unnecessary re-renders.


134-139: Good integration of the LogSearch component.

The LogSearch component is properly integrated with all the necessary props. This cleanly separates the search UI from the main component.


146-148: Container ref placement is now correct.

The containerRef is now properly attached to the scrollable container, fixing the previous issue where it was attached to an empty element.

@harshit078
Copy link
Author

Screenshot 2025-04-27 at 1 12 38 PM

Improved pointer and key input interactions from 150 ms to 80ms

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
web-server/src/content/Service/SystemLogs.tsx (2)

88-90: Replace fragile style selector with semantic class / data-attribute

The DOM lookup relies on matching the literal string background-color: yellow, which is brittle (style order, minification, different color formats, etc.).
A robust approach is to mark the highlighted <span> with a class (log-highlight) or data- attribute and query that instead.

This point was raised in a previous review but is still unresolved.


22-23: showScrollDownButton is never toggled – add scroll listener

showScrollDownButton is initialised but never updated when the user scrolls away from the bottom, so the floating button will never appear.

A simple scroll handler on the containerRef will fix this:

+  /* show / hide “scroll-to-bottom” button */
+  useEffect(() => {
+    const el = containerRef.current;
+    if (!el) return;
+
+    const handleScroll = () => {
+      const { scrollTop, scrollHeight, clientHeight } = el;
+      const atBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
+      showScrollDownButton.set(!atBottom);
+    };
+
+    el.addEventListener('scroll', handleScroll);
+    handleScroll(); // initialise
+    return () => el.removeEventListener('scroll', handleScroll);
+  }, [showScrollDownButton]);

Also applies to: 179-185

🧹 Nitpick comments (1)
web-server/src/content/Service/SystemLogs.tsx (1)

191-195: Hard-coded marginLeft breaks on resize – use CSS centering

The floating button’s horizontal position is calculated once during render and won’t adjust if the log pane resizes.
Using left: 50% with transform: translateX(-50%) is simpler and fully responsive:

-            bottom={20}
-            sx={{
-              position: 'fixed',
-              marginLeft: `calc(${containerRef.current
-                  ? containerRef.current.clientWidth / 2 - 67
-                  : 0
-                }px)`
-            }}
+            sx={{
+              position: 'fixed',
+              bottom: 20,
+              left: '50%',
+              transform: 'translateX(-50%)'
+            }}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2e5018 and 65c860e.

📒 Files selected for processing (1)
  • web-server/src/content/Service/SystemLogs.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
web-server/src/content/Service/SystemLogs.tsx (5)
web-server/src/hooks/useSystemLogs.tsx (1)
  • useSystemLogs (5-22)
web-server/src/utils/logFormatter.ts (1)
  • parseLogLine (12-95)
web-server/src/components/Service/SystemLog/PlainLog.tsx (1)
  • PlainLog (34-40)
web-server/src/components/Service/SystemLog/FormattedLog.tsx (1)
  • FormattedLog (37-82)
web-server/src/components/Service/SystemLog/SystemLogsErrorFllback.tsx (1)
  • SystemLogsErrorFallback (12-60)

Comment on lines +84 to +102
useLayoutEffect(() => {
if (!debouncedSearchQuery || debouncedSearchQuery.length < 3) return;

const elements = Array.from(
containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
) as HTMLElement[];

setHighlightedElements(elements);
highlightedElementsRef.current = elements;
const newTotalMatches = elements.length;
setTotalMatches(newTotalMatches);
totalMatchesRef.current = newTotalMatches;

if (currentMatchRef.current === 0) {
setCurrentMatch(newTotalMatches ? 1 : 0);
currentMatchRef.current = newTotalMatches ? 1 : 0;
}
}, [debouncedSearchQuery]);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Re-run highlight scan when new logs arrive

useLayoutEffect only depends on debouncedSearchQuery.
If new log lines stream in while the user keeps the same query, they will never be highlighted.

-  }, [debouncedSearchQuery]);
+  }, [debouncedSearchQuery, logs]);

Adding logs ensures new matches are detected as soon as fresh data renders.

📝 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
useLayoutEffect(() => {
if (!debouncedSearchQuery || debouncedSearchQuery.length < 3) return;
const elements = Array.from(
containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
) as HTMLElement[];
setHighlightedElements(elements);
highlightedElementsRef.current = elements;
const newTotalMatches = elements.length;
setTotalMatches(newTotalMatches);
totalMatchesRef.current = newTotalMatches;
if (currentMatchRef.current === 0) {
setCurrentMatch(newTotalMatches ? 1 : 0);
currentMatchRef.current = newTotalMatches ? 1 : 0;
}
}, [debouncedSearchQuery]);
useLayoutEffect(() => {
if (!debouncedSearchQuery || debouncedSearchQuery.length < 3) return;
const elements = Array.from(
containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
) as HTMLElement[];
setHighlightedElements(elements);
highlightedElementsRef.current = elements;
const newTotalMatches = elements.length;
setTotalMatches(newTotalMatches);
totalMatchesRef.current = newTotalMatches;
if (currentMatchRef.current === 0) {
setCurrentMatch(newTotalMatches ? 1 : 0);
currentMatchRef.current = newTotalMatches ? 1 : 0;
}
}, [debouncedSearchQuery, logs]);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
web-server/src/content/Service/SystemLogs.tsx (3)

87-89: Use a stable marker for highlighted spans instead of relying on inline style

The selector 'span[style*="background-color: yellow"]' is fragile as React may normalize styles or reorder properties.

- const elements = Array.from(
-   containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
- ) as HTMLElement[];
+ const elements = Array.from(
+   containerRef.current?.querySelectorAll('.log-highlight') ?? []
+ ) as HTMLElement[];

This means you'll need to add the className="log-highlight" to the highlighted spans in PlainLog and FormattedLog components.


84-102: Re-run highlight scan when new logs arrive

The useLayoutEffect only depends on debouncedSearchQuery. If new logs load while a search query is active, matches in those logs won't be highlighted or included in navigation.

- }, [debouncedSearchQuery]);
+ }, [debouncedSearchQuery, logs]);

22-22: Missing implementation for showScrollDownButton toggle

You've removed the IntersectionObserver but there's no mechanism to detect when the user scrolls up to show the scroll-down button.

 const SystemLogs = memo(({ serviceName }: { serviceName?: ServiceNames }) => {
   // ... existing code
+  useEffect(() => {
+    const handleScroll = () => {
+      if (!containerRef.current) return;
+      
+      const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
+      const isScrolledToBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
+      showScrollDownButton.set(!isScrolledToBottom);
+    };
+    
+    const scrollContainer = containerRef.current;
+    if (scrollContainer) {
+      scrollContainer.addEventListener('scroll', handleScroll);
+    }
+    
+    return () => {
+      if (scrollContainer) {
+        scrollContainer.removeEventListener('scroll', handleScroll);
+      }
+    };
+  }, [showScrollDownButton]);
🧹 Nitpick comments (2)
web-server/src/content/Service/SystemLogs.tsx (2)

53-66: Direct DOM manipulation may conflict with React's rendering

You're directly manipulating DOM element styles instead of using React's state-based rendering. This can lead to issues if React re-renders the elements.

Consider modifying the HighlightedText component to accept an isActive prop, so React can handle the highlighting:

// In PlainLog and FormattedLog components:
<HighlightedText
  text={text}
  searchQuery={searchQuery}
+ isActiveMatch={matchIndex === currentActiveMatchIndex}
/>

// In HighlightedText component:
const getStyle = (isMatch, isActive) => {
  if (isMatch) {
    return isActive 
      ? { backgroundColor: 'rgb(255, 161, 10)', color: 'white' }
      : { backgroundColor: 'yellow', color: 'black' };
  }
  return {};
};

This would eliminate the need for direct DOM manipulation.


72-81: Consider extracting magic numbers as constants

The minimum search query length (3 characters) is hardcoded in multiple places.

+const MIN_SEARCH_LENGTH = 3;

const handleSearch = useCallback((query: string) => {
  setSearchQuery(query);
  isSearchingRef.current = !!query;

-  if (!query || query.length < 3) {
+  if (!query || query.length < MIN_SEARCH_LENGTH) {
    // ...

Also update line 85 to use the constant.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65c860e and 113e483.

📒 Files selected for processing (1)
  • web-server/src/content/Service/SystemLogs.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
web-server/src/content/Service/SystemLogs.tsx (6)
web-server/src/hooks/useSystemLogs.tsx (1)
  • useSystemLogs (5-22)
web-server/src/utils/logFormatter.ts (1)
  • parseLogLine (12-95)
web-server/src/components/Service/SystemLog/PlainLog.tsx (1)
  • PlainLog (34-40)
web-server/src/components/Service/SystemLog/FormattedLog.tsx (1)
  • FormattedLog (37-82)
web-server/src/components/Service/SystemLog/SystemLogsErrorFllback.tsx (1)
  • SystemLogsErrorFallback (12-60)
web-server/src/components/Service/SystemLog/LogSearch.tsx (1)
  • LogSearch (178-178)
🔇 Additional comments (5)
web-server/src/content/Service/SystemLogs.tsx (5)

176-178: Proper implementation of container with ref

This is a good improvement that addresses a previous critical issue - you've correctly attached the containerRef to the scrollable container.


164-169: Good integration of the search component

The LogSearch component is well-integrated with proper props being passed for search functionality and navigation.


142-155: Good extraction of renderLogs as a memoized callback

The extraction of the log rendering logic into a memoized callback with the correct dependencies is a good performance optimization.


27-34: 🛠️ Refactor suggestion

Redundant state and ref duplications

You have duplicate state and refs for highlightedElements, currentMatch, and totalMatches. This creates potential synchronization issues.

Remove the redundant state variables and use either the state or the refs consistently:

- const [highlightedElements, setHighlightedElements] = useState<HTMLElement[]>([]);
- const [currentMatch, setCurrentMatch] = useState(0);
- const [totalMatches, setTotalMatches] = useState(0);
  const currentHighlightRef = useRef<HTMLElement | null>(null);
  const isInitialLoad = useRef(true);
  const isSearchingRef = useRef(false);
  const currentMatchRef = useRef(0);
  const totalMatchesRef = useRef(0);
  const highlightedElementsRef = useRef<HTMLElement[]>([]);
  const isNavigatingRef = useRef(false);

Then update your component to only use the refs for DOM manipulation and state for rendering:

// When updating for UI rendering
- setHighlightedElements(elements);
  highlightedElementsRef.current = elements;
- setTotalMatches(newTotalMatches);
  totalMatchesRef.current = newTotalMatches;
- setCurrentMatch(newTotalMatches ? 1 : 0);
  currentMatchRef.current = newTotalMatches ? 1 : 0;

// In LogSearch component props
  <LogSearch
    onSearch={handleSearch}
    onNavigate={handleNavigate}
-   currentMatch={currentMatch}
-   totalMatches={totalMatches}
+   currentMatch={currentMatchRef.current}
+   totalMatches={totalMatchesRef.current}
  />

Likely an incorrect or invalid review comment.


68-82:

✅ Verification successful

Consider enhancing search with filtering capability

According to the PR comments, the current implementation only highlights matches but doesn't filter the logs to show only matching entries.

Consider enhancing the feature by adding an option to filter logs to show only entries containing the search query:

+ const [shouldFilterLogs, setShouldFilterLogs] = useState(false);

// In handleSearch:
  const handleSearch = useCallback((query: string) => {
    // ...existing code...
  }, [updateHighlight]);

+ const filteredLogs = useMemo(() => {
+   if (!debouncedSearchQuery || !shouldFilterLogs) return logs;
+   return logs.filter(log => log.toLowerCase().includes(debouncedSearchQuery.toLowerCase()));
+ }, [logs, debouncedSearchQuery, shouldFilterLogs]);

// In renderLogs:
- return logs.map((log, index) => {
+ return (shouldFilterLogs ? filteredLogs : logs).map((log, index) => {

// Update LogSearch component:
<LogSearch
  onSearch={handleSearch}
  onNavigate={handleNavigate}
  currentMatch={currentMatch}
  totalMatches={totalMatches}
+ onToggleFilter={setShouldFilterLogs}
+ isFilterEnabled={shouldFilterLogs}
/>

Then update your LogSearch component to include a filter toggle button.


🏁 Script executed:

#!/bin/bash
# Check if this enhancement aligns with the PR requirements

echo "PR objectives mention:"
cat << EOF
The pull request titled "feat: Implemented log search functionality with highlighted results"
aims to enhance the log viewing experience by adding search capabilities within logs.

The comment on the PR by user samad-yar-khan notes that the current implementation
appears to highlight the logs but does not actually perform searching through or filtering
of the search results. The commenter suggests considering enhancements to make the feature
more usable.
EOF

echo -e "\nChecking the current LogSearch component implementation:"
fd -t f LogSearch.tsx | xargs cat

Length of output: 5083


🏁 Script executed:

#!/bin/bash
# Locate SystemLogs.tsx and show relevant sections around log rendering
file=$(fd -t f SystemLogs.tsx)
echo "----- File: $file -----"
sed -n '1,200p' "$file"
echo "----- Rendering logs in SystemLogs.tsx -----"
grep -R "renderLogs" -n "$file"
grep -R "logs" -n "$file" | sed -n '1,50p'

Length of output: 1131


🏁 Script executed:

#!/bin/bash
set -e

# Find SystemLogs.tsx files
files=$(fd -t f SystemLogs.tsx)

for f in $files; do
  echo "===== File: $f ====="
  
  # Show first 200 lines for context
  sed -n '1,200p' "$f"
  
  echo -e "\n--- Occurrences of 'logs', 'render', and 'renderLogs' in $f ---"
  grep -E -n "(logs|renderLogs|render)" "$f" || true
  
  echo -e "\n\n"
done

Length of output: 9077


No changes needed: filtering logs is a future enhancement
The current PR implements in-line match highlighting per its scope. Introducing entry‐level filtering would be a valuable follow-up but isn’t required to fulfill the “highlight search results” objective here.

• The existing handleSearch + renderLogs flow correctly highlights matches without altering the log list.
• If you’d like to expose a “show only matching entries” toggle, consider capturing it in a separate issue or PR to avoid expanding the current feature scope.

Copy link
Contributor

@jayantbh jayantbh left a comment

Choose a reason for hiding this comment

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

Okay, first things first, my review might be overwhelming.
Sorry about that, should have done this in parts perhaps.

But given that I rarely get time to review PRs in our OSS repo, I decided doing it all in one go might save you more time.

That being said, great first attempt, because it functions alright. :)

However, the code quality leaves a lot to be desired.
Take this as a challenge, I'm hoping you'll come away having learnt a lot of serious stuff from this.

Some learning material I've linked in one of the comments:
https://kentcdodds.com/blog/make-impossible-states-impossible
https://www.youtube.com/watch?v=IcgmSRJHu_8 (link is about Elm lang, but focus on the concept - it's beautifully presented)

}

const HighlightedText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => {
if (!searchQuery) return <>{text}</>;
Copy link
Contributor

Choose a reason for hiding this comment

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

A component can directly return text. Text nodes are valid react nodes.

const HighlightedText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => {
if (!searchQuery) return <>{text}</>;

const escapeRegExp = (string: string) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a nitpick. You don't need to address this.

Constants, functions, or any logic that doesn't strictly depend on the component itself can be declared outside of it.

This allows the reference to be maintained and eliminates and chances of rerendering of anything that depends on this stuff.

Right now it's not a required change because no child components depend on it at the moment. But doing so anyway is a good practice because if someone needs to use this function in the future:

  1. They don't have to use it from a prop.
  2. They won't have rerender issues due to references getting changed.

Comment on lines +30 to +32
) : (
part
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't really need to wrap in a parenthesis

Comment on lines +24 to +25
<>
{parts.map((part, i) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

You'll need to check this, but I think you can directly return the parts.map and stuff without wrapping in a fragment. Confirm once.

searchQuery?: string;
}

const HighlightedText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Calling the component HighlightedText is misleading because this is used to render logs regardless of whether it's highlighted or not.

FormattedLog was perhaps a better name.


const containerElement = containerRef.current;
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the pattern you're using is:

  • User input
  • debounce 300ms
  • Update search query
  • wait 300ms
  • handle search highlighting

This is something I've seen beginners do, and, when it's laid out for you I'm sure you can see what's wrong here.

Debouncing highlighting on user input is definitely a good idea, but your flow should just be:

  • user inputs
  • debounce 300ms
  • trigger highlighting handler

You also shouldn't maintain two states for search queries, one input by the user, one used by the highlighting handler.

P.S.: This is the kind of stuff that causes folks to fail our frontend interviews, so with this learning, you now have an advantage over them. :) Hopefully you'll come out a lot stronger out of this program. :)

Comment on lines +54 to 65
if (currentHighlightRef.current) {
currentHighlightRef.current.style.backgroundColor = 'yellow';
currentHighlightRef.current.style.color = 'black';
}

if (element) {
element.style.backgroundColor = 'rgb(255, 161, 10)';
element.style.color = 'white';
currentHighlightRef.current = element;
} else {
currentHighlightRef.current = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Well... that's not a great idea my dude. Use CSS classes, or even MUI's css-in-js based on some supplied prop. Don't mutate the DOM like this unless there's no other way (sometimes that is the case, but it's not right now).

If you're not sure why I'm saying this, feel free to ask. I urge anyone working with us to not just absorb feedback if they aren't convinced.

Comment on lines +68 to +101
const handleSearch = useCallback((query: string) => {
setSearchQuery(query);
isSearchingRef.current = !!query;

if (!query || query.length < 3) {
setCurrentMatch(0);
currentMatchRef.current = 0;
setTotalMatches(0);
totalMatchesRef.current = 0;
setHighlightedElements([]);
highlightedElementsRef.current = [];
updateHighlight(null);
return;
}
}, [updateHighlight]);

useLayoutEffect(() => {
if (!debouncedSearchQuery || debouncedSearchQuery.length < 3) return;

const elements = Array.from(
containerRef.current?.querySelectorAll('span[style*="background-color: yellow"]') ?? []
) as HTMLElement[];

setHighlightedElements(elements);
highlightedElementsRef.current = elements;
const newTotalMatches = elements.length;
setTotalMatches(newTotalMatches);
totalMatchesRef.current = newTotalMatches;

if (currentMatchRef.current === 0) {
setCurrentMatch(newTotalMatches ? 1 : 0);
currentMatchRef.current = newTotalMatches ? 1 : 0;
}
}, [debouncedSearchQuery]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same feedback as my above comments about reducers and state machines.

This code is too easy to add bugs to.

Copy link
Contributor

Choose a reason for hiding this comment

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

Other easily avoidable problems:

  • Direct DOM reads and manipulation
  • Too many ref changes tracked and updated

Copy link
Contributor

Choose a reason for hiding this comment

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

Also...
💀 (in this context)
querySelectorAll('span[style*="background-color: yellow"]')

-100 points to Gryffindor.

Comment on lines +107 to +126
requestAnimationFrame(() => {
let newIndex = currentMatchRef.current;
const total = totalMatchesRef.current;

if (direction === 'next') {
newIndex = newIndex < total ? newIndex + 1 : 1;
} else {
newIndex = newIndex > 1 ? newIndex - 1 : total;
}
};
}, [showScrollDownButton]);

setCurrentMatch(newIndex);
currentMatchRef.current = newIndex;
const element = highlightedElementsRef.current[newIndex - 1];
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
updateHighlight(element);
}
isNavigatingRef.current = false;
});
}, [updateHighlight]);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an example of adding hacky code to make existing hacky code work.

Using a proper state machine architecture will make this SO MUCH EASIER to handle.

Comment on lines +193 to +196
marginLeft: `calc(${containerRef.current
? containerRef.current.clientWidth / 2 - 67
: 0
}px)`
}px)`
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this an intentional change? Or something that the linter did? Or maybe your IDE?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants