-
Notifications
You must be signed in to change notification settings - Fork 120
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
3a3abb0
940f740
ae17ef3
901cd26
e2e5018
65c860e
113e483
0a10568
1d41efb
75ea1fa
ebdb7bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,32 @@ import { useCallback } from 'react'; | |
import { Line } from '@/components/Text'; | ||
import { ParsedLog } from '@/types/resources'; | ||
|
||
export const FormattedLog = ({ log }: { log: ParsedLog; index: number }) => { | ||
interface FormattedLogProps { | ||
log: ParsedLog; | ||
index: number; | ||
searchQuery?: string; | ||
} | ||
|
||
const HighlightedText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => { | ||
if (!searchQuery) return <>{text}</>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 parts = text.split(new RegExp(`(${searchQuery})`, 'gi')); | ||
return ( | ||
<> | ||
{parts.map((part, i) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
part.toLowerCase() === searchQuery.toLowerCase() ? ( | ||
<span key={i} style={{ backgroundColor: 'yellow', color: 'black' }}> | ||
{part} | ||
</span> | ||
harshit078 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) : ( | ||
part | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't really need to wrap in a parenthesis |
||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export const FormattedLog = ({ log, searchQuery }: FormattedLogProps) => { | ||
const theme = useTheme(); | ||
const getLevelColor = useCallback( | ||
(level: string) => { | ||
|
@@ -36,17 +61,17 @@ export const FormattedLog = ({ log }: { log: ParsedLog; index: number }) => { | |
return ( | ||
<Line mono marginBottom={1}> | ||
<Line component="span" color="info"> | ||
{timestamp} | ||
<HighlightedText text={timestamp} searchQuery={searchQuery} /> | ||
</Line>{' '} | ||
{ip && ( | ||
<Line component="span" color="primary"> | ||
{ip}{' '} | ||
<HighlightedText text={ip} searchQuery={searchQuery} />{' '} | ||
</Line> | ||
)} | ||
<Line component="span" color={getLevelColor(logLevel)}> | ||
[{logLevel}] | ||
[<HighlightedText text={logLevel} searchQuery={searchQuery} />] | ||
</Line>{' '} | ||
{message} | ||
<HighlightedText text={message} searchQuery={searchQuery} /> | ||
</Line> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { Button, InputAdornment, TextField, Typography, Box } from '@mui/material'; | ||
import { Search as SearchIcon, Clear as ClearIcon, NavigateNext, NavigateBefore } from '@mui/icons-material'; | ||
import { useState, useCallback } from 'react'; | ||
import { styled } from '@mui/material/styles'; | ||
import { MotionBox } from '@/components/MotionComponents'; | ||
|
||
const SearchContainer = styled('div')(() => ({ | ||
position: 'sticky', | ||
top: 0, | ||
zIndex: 1, | ||
gap: 5, | ||
paddingBottom: 8, | ||
alignItems: 'center', | ||
backdropFilter: 'blur(10px)', | ||
borderRadius: 5, | ||
})); | ||
|
||
const SearchControls = styled(Box)(({ theme }) => ({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: theme.spacing(1), | ||
marginTop: 8, | ||
})); | ||
|
||
const StyledTextField = styled(TextField)(({ theme }) => ({ | ||
'& .MuiOutlinedInput-root': { | ||
backgroundColor: theme.palette.background.paper, | ||
transition: 'all 0.2s ease-in-out', | ||
'&:hover': { | ||
backgroundColor: theme.palette.background.paper, | ||
boxShadow: `0 0 0 1px ${theme.palette.primary.main}`, | ||
}, | ||
'&.Mui-focused': { | ||
backgroundColor: theme.palette.background.paper, | ||
boxShadow: `0 0 0 2px ${theme.palette.primary.main}`, | ||
}, | ||
}, | ||
})); | ||
|
||
interface LogSearchProps { | ||
onSearch: (query: string) => void; | ||
onNavigate: (direction: 'prev' | 'next') => void; | ||
Comment on lines
+54
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a required change. General tip. Unless it's important for you to specify that a function must return nothing, you can just set the return type as There are good reasons for a function to return nothing, when you think the function might accidentally be incorrectly used with its return value when it's not intended to. But unless that's the case, you're fine with BUT... don't take this as an endorsement that using |
||
currentMatch: number; | ||
totalMatches: number; | ||
} | ||
|
||
export const LogSearch = ({ onSearch, onNavigate, currentMatch, totalMatches }: LogSearchProps) => { | ||
const [searchQuery, setSearchQuery] = useState(''); | ||
|
||
const handleSearchChange = useCallback( | ||
(event: React.ChangeEvent<HTMLInputElement>) => { | ||
const query = event.target.value; | ||
setSearchQuery(query); | ||
onSearch(query); | ||
}, | ||
[onSearch] | ||
); | ||
|
||
const handleClear = useCallback(() => { | ||
setSearchQuery(''); | ||
onSearch(''); | ||
}, [onSearch]); | ||
|
||
const handleKeyDown = useCallback( | ||
(event: React.KeyboardEvent) => { | ||
if (event.key === 'Enter' && event.shiftKey) { | ||
onNavigate('prev'); | ||
} else if (event.key === 'Enter') { | ||
onNavigate('next'); | ||
} | ||
}, | ||
[onNavigate] | ||
); | ||
|
||
return ( | ||
<SearchContainer> | ||
<StyledTextField | ||
fullWidth | ||
variant="outlined" | ||
placeholder="Search logs..." | ||
value={searchQuery} | ||
onChange={handleSearchChange} | ||
onKeyDown={handleKeyDown} | ||
InputProps={{ | ||
startAdornment: ( | ||
<InputAdornment position="start"> | ||
<SearchIcon color="action" /> | ||
</InputAdornment> | ||
), | ||
endAdornment: searchQuery && ( | ||
<InputAdornment position="end"> | ||
<ClearIcon | ||
style={{ cursor: 'pointer' }} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer using MUI's Common styles are combined into the same generated classname to keep the bundle-size small. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do this for any other instances where it's an MUI component. |
||
onClick={handleClear} | ||
color="action" | ||
/> | ||
</InputAdornment> | ||
), | ||
}} | ||
/> | ||
{searchQuery && totalMatches > 0 && ( | ||
<MotionBox | ||
initial={{ y: 20, opacity: 0 }} | ||
animate={{ y: 0, opacity: 1 }} | ||
exit={{ y: 20, opacity: 0 }} | ||
transition={{ | ||
type: 'tween', | ||
ease: 'easeOut', | ||
duration: 0.3, | ||
}} | ||
> | ||
<SearchControls> | ||
<Button | ||
size="small" | ||
onClick={() => onNavigate('prev')} | ||
disabled={currentMatch === 1} | ||
startIcon={<NavigateBefore />} | ||
sx={{ | ||
minWidth: '20px', | ||
padding: '4px 8px', | ||
}} | ||
/> | ||
<Typography variant="body2" color="text.secondary"> | ||
{currentMatch} of {totalMatches} | ||
</Typography> | ||
<Button | ||
size="small" | ||
onClick={() => onNavigate('next')} | ||
disabled={currentMatch === totalMatches} | ||
startIcon={<NavigateNext />} | ||
sx={{ | ||
minWidth: '20px', | ||
padding: '4px 8px', | ||
}} | ||
/> | ||
</SearchControls> | ||
</MotionBox> | ||
)} | ||
</SearchContainer> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,34 @@ | ||
import { Line } from '@/components/Text'; | ||
|
||
export const PlainLog = ({ log }: { log: string; index: number }) => { | ||
interface PlainLogProps { | ||
log: string; | ||
index: number; | ||
searchQuery?: string; | ||
} | ||
|
||
const HighlightedText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This component is duplicated. Figure out a way to avoid duplication to avoid future code drift. |
||
if (!searchQuery) return <>{text}</>; | ||
|
||
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi')); | ||
return ( | ||
<> | ||
{parts.map((part, i) => | ||
part.toLowerCase() === searchQuery.toLowerCase() ? ( | ||
<span key={i} style={{ backgroundColor: 'yellow', color: 'black' }}> | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{part} | ||
</span> | ||
) : ( | ||
part | ||
) | ||
)} | ||
</> | ||
); | ||
}; | ||
harshit078 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export const PlainLog = ({ log, searchQuery }: PlainLogProps) => { | ||
return ( | ||
<Line mono marginBottom={1}> | ||
{log} | ||
<HighlightedText text={log} searchQuery={searchQuery} /> | ||
</Line> | ||
); | ||
}; |
There was a problem hiding this comment.
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.