Skip to content

Commit 3a3abb0

Browse files
committed
feat: Implemented log search functionality with highlighted results
1 parent b715c16 commit 3a3abb0

File tree

4 files changed

+283
-48
lines changed

4 files changed

+283
-48
lines changed

web-server/src/components/Service/SystemLog/FormattedLog.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,32 @@ import { useCallback } from 'react';
44
import { Line } from '@/components/Text';
55
import { ParsedLog } from '@/types/resources';
66

7-
export const FormattedLog = ({ log }: { log: ParsedLog; index: number }) => {
7+
interface FormattedLogProps {
8+
log: ParsedLog;
9+
index: number;
10+
searchQuery?: string;
11+
}
12+
13+
const HighlightedText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => {
14+
if (!searchQuery) return <>{text}</>;
15+
16+
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
17+
return (
18+
<>
19+
{parts.map((part, i) =>
20+
part.toLowerCase() === searchQuery.toLowerCase() ? (
21+
<span key={i} style={{ backgroundColor: 'yellow', color: 'black' }}>
22+
{part}
23+
</span>
24+
) : (
25+
part
26+
)
27+
)}
28+
</>
29+
);
30+
};
31+
32+
export const FormattedLog = ({ log, searchQuery }: FormattedLogProps) => {
833
const theme = useTheme();
934
const getLevelColor = useCallback(
1035
(level: string) => {
@@ -36,17 +61,17 @@ export const FormattedLog = ({ log }: { log: ParsedLog; index: number }) => {
3661
return (
3762
<Line mono marginBottom={1}>
3863
<Line component="span" color="info">
39-
{timestamp}
64+
<HighlightedText text={timestamp} searchQuery={searchQuery} />
4065
</Line>{' '}
4166
{ip && (
4267
<Line component="span" color="primary">
43-
{ip}{' '}
68+
<HighlightedText text={ip} searchQuery={searchQuery} />{' '}
4469
</Line>
4570
)}
4671
<Line component="span" color={getLevelColor(logLevel)}>
47-
[{logLevel}]
72+
[<HighlightedText text={logLevel} searchQuery={searchQuery} />]
4873
</Line>{' '}
49-
{message}
74+
<HighlightedText text={message} searchQuery={searchQuery} />
5075
</Line>
5176
);
5277
};
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { Button, InputAdornment, TextField, Typography, Box } from '@mui/material';
2+
import { Search as SearchIcon, Clear as ClearIcon, NavigateNext, NavigateBefore } from '@mui/icons-material';
3+
import { useState, useCallback } from 'react';
4+
import { styled } from '@mui/material/styles';
5+
import { MotionBox } from '@/components/MotionComponents';
6+
7+
const SearchContainer = styled('div')(() => ({
8+
position: 'sticky',
9+
top: 0,
10+
zIndex: 1,
11+
gap: 5,
12+
paddingBottom: 8,
13+
alignItems: 'center',
14+
backdropFilter: 'blur(10px)',
15+
borderRadius: 5,
16+
}));
17+
18+
const SearchControls = styled(Box)(({ theme }) => ({
19+
display: 'flex',
20+
alignItems: 'center',
21+
gap: theme.spacing(1),
22+
marginTop: 8,
23+
}));
24+
25+
const StyledTextField = styled(TextField)(({ theme }) => ({
26+
'& .MuiOutlinedInput-root': {
27+
backgroundColor: theme.palette.background.paper,
28+
transition: 'all 0.2s ease-in-out',
29+
'&:hover': {
30+
backgroundColor: theme.palette.background.paper,
31+
boxShadow: `0 0 0 1px ${theme.palette.primary.main}`,
32+
},
33+
'&.Mui-focused': {
34+
backgroundColor: theme.palette.background.paper,
35+
boxShadow: `0 0 0 2px ${theme.palette.primary.main}`,
36+
},
37+
},
38+
}));
39+
40+
interface LogSearchProps {
41+
onSearch: (query: string) => void;
42+
onNavigate: (direction: 'prev' | 'next') => void;
43+
currentMatch: number;
44+
totalMatches: number;
45+
}
46+
47+
export const LogSearch = ({ onSearch, onNavigate, currentMatch, totalMatches }: LogSearchProps) => {
48+
const [searchQuery, setSearchQuery] = useState('');
49+
50+
const handleSearchChange = useCallback(
51+
(event: React.ChangeEvent<HTMLInputElement>) => {
52+
const query = event.target.value;
53+
setSearchQuery(query);
54+
onSearch(query);
55+
},
56+
[onSearch]
57+
);
58+
59+
const handleClear = useCallback(() => {
60+
setSearchQuery('');
61+
onSearch('');
62+
}, [onSearch]);
63+
64+
const handleKeyDown = useCallback(
65+
(event: React.KeyboardEvent) => {
66+
if (event.key === 'Enter' && event.shiftKey) {
67+
onNavigate('prev');
68+
} else if (event.key === 'Enter') {
69+
onNavigate('next');
70+
}
71+
},
72+
[onNavigate]
73+
);
74+
75+
return (
76+
<SearchContainer>
77+
<StyledTextField
78+
fullWidth
79+
variant="outlined"
80+
placeholder="Search logs..."
81+
value={searchQuery}
82+
onChange={handleSearchChange}
83+
onKeyDown={handleKeyDown}
84+
InputProps={{
85+
startAdornment: (
86+
<InputAdornment position="start">
87+
<SearchIcon color="action" />
88+
</InputAdornment>
89+
),
90+
endAdornment: searchQuery && (
91+
<InputAdornment position="end">
92+
<ClearIcon
93+
style={{ cursor: 'pointer' }}
94+
onClick={handleClear}
95+
color="action"
96+
/>
97+
</InputAdornment>
98+
),
99+
}}
100+
/>
101+
{searchQuery && totalMatches > 0 && (
102+
<MotionBox
103+
initial={{ y: 20, opacity: 0 }}
104+
animate={{ y: 0, opacity: 1 }}
105+
exit={{ y: 20, opacity: 0 }}
106+
transition={{
107+
type: 'tween',
108+
ease: 'easeOut',
109+
duration: 0.3,
110+
}}
111+
>
112+
<SearchControls>
113+
<Button
114+
size="small"
115+
onClick={() => onNavigate('prev')}
116+
disabled={currentMatch === 1}
117+
startIcon={<NavigateBefore />}
118+
sx={{
119+
minWidth: '20px',
120+
padding: '4px 8px',
121+
}}
122+
/>
123+
<Typography variant="body2" color="text.secondary">
124+
{currentMatch} of {totalMatches}
125+
</Typography>
126+
<Button
127+
size="small"
128+
onClick={() => onNavigate('next')}
129+
disabled={currentMatch === totalMatches}
130+
startIcon={<NavigateNext />}
131+
sx={{
132+
minWidth: '20px',
133+
padding: '4px 8px',
134+
}}
135+
/>
136+
</SearchControls>
137+
</MotionBox>
138+
)}
139+
</SearchContainer>
140+
);
141+
};
Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
import { Line } from '@/components/Text';
22

3-
export const PlainLog = ({ log }: { log: string; index: number }) => {
3+
interface PlainLogProps {
4+
log: string;
5+
index: number;
6+
searchQuery?: string;
7+
}
8+
9+
const HighlightedText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => {
10+
if (!searchQuery) return <>{text}</>;
11+
12+
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
13+
return (
14+
<>
15+
{parts.map((part, i) =>
16+
part.toLowerCase() === searchQuery.toLowerCase() ? (
17+
<span key={i} style={{ backgroundColor: 'yellow', color: 'black' }}>
18+
{part}
19+
</span>
20+
) : (
21+
part
22+
)
23+
)}
24+
</>
25+
);
26+
};
27+
28+
export const PlainLog = ({ log, searchQuery }: PlainLogProps) => {
429
return (
530
<Line mono marginBottom={1}>
6-
{log}
31+
<HighlightedText text={log} searchQuery={searchQuery} />
732
</Line>
833
);
934
};

0 commit comments

Comments
 (0)