Skip to content

Commit f75f3b8

Browse files
feat: search alerts
1 parent 7b4b0bb commit f75f3b8

4 files changed

Lines changed: 140 additions & 49 deletions

File tree

src/atoms/alertAtom.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ export const alertAtom = atom(initialState);
1717
export const alertHowManyAtom = atom({
1818
howManyAlerts: 0,
1919
howManyAlertSoft: 0,
20-
});
20+
});
21+
22+
export const alertSearchTextAtom = atom('');

src/components/alerts/AlertItems.tsx

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

19-
import { Component } from 'react';
19+
import { useState } from 'react';
2020
import { translate } from '../../helpers/language';
2121

2222
import AlertItem from './AlertItem';
2323
import QuietFor from './QuietFor';
2424

2525
import { Alert } from 'types/hostAndServiceTypes';
2626
import { ClientSettings } from 'types/settings';
27+
import { useAtomValue } from 'jotai';
28+
import { alertSearchTextAtom } from '../../atoms/alertAtom';
2729
// CSS
2830
import '../animation.css';
2931
import '../services/ServiceItems.css';
@@ -35,92 +37,90 @@ interface AlertItemsProps {
3537
isDemoMode: boolean;
3638
}
3739

38-
class AlertItems extends Component<AlertItemsProps> {
40+
const AlertItems = ({ items, settings, isDemoMode }: AlertItemsProps) => {
3941

40-
constructor(props: AlertItemsProps) {
41-
super(props);
42+
const alertSearchText = useAtomValue(alertSearchTextAtom);
4243

43-
this.showMore = this.showMore.bind(this);
44-
this.showLess = this.showLess.bind(this);
45-
}
44+
const [howManyToRender, setHowManyToRender] = useState(100);
45+
const pageSize = 100;
4646

47-
state = {
48-
howManyToRender: 100, // This value is updated when user clicks showMore or showLess.
49-
pageSize: 100 // This stays const
47+
const showMore = () => {
48+
setHowManyToRender(prev => prev + pageSize);
5049
};
5150

52-
showMore() {
53-
this.setState({
54-
howManyToRender: this.state.howManyToRender + this.state.pageSize
55-
});
56-
}
57-
58-
showLess() {
59-
this.setState({
60-
howManyToRender: this.state.howManyToRender - this.state.pageSize
61-
});
62-
}
63-
64-
render() {
51+
const showLess = () => {
52+
setHowManyToRender(prev => prev - pageSize);
53+
};
6554

66-
const filteredHistoryArray = this.props.items.filter(item => {
67-
if (this.props.settings.hideAlertSoft) {
68-
if (item.state_type === 2) { return false; }
55+
const filteredHistoryArray = items.filter(item => {
56+
if (settings.hideAlertSoft) {
57+
if (item.state_type === 2) { return false; }
58+
}
59+
// search filter
60+
if (alertSearchText) {
61+
const searchLower = alertSearchText.toLowerCase();
62+
const matchesSearch =
63+
(item.name && item.name.toLowerCase().includes(searchLower)) ||
64+
(item.host_name && item.host_name.toLowerCase().includes(searchLower)) ||
65+
(item.description && item.description.toLowerCase().includes(searchLower)) ||
66+
(item.plugin_output && item.plugin_output.toLowerCase().includes(searchLower));
67+
if (!matchesSearch) {
68+
return false;
6969
}
70-
return true;
71-
});
70+
}
71+
return true;
72+
});
7273

73-
let trimmedItems = [...filteredHistoryArray];
74-
trimmedItems.length = this.state.howManyToRender;
75-
const { language, locale, dateFormat } = this.props.settings;
74+
let trimmedItems = [...filteredHistoryArray];
75+
trimmedItems.length = howManyToRender;
76+
const { language, locale, dateFormat } = settings;
7677

77-
return (
78+
return (
7879
<div className="AlertItems">
7980
{/* always show one quiet for (if we have at least 1 item) */}
80-
{this.props.items.length > 1 &&
81+
{items.length > 1 &&
8182
<QuietFor
8283
nowtime={new Date().getTime()}
83-
prevtime={this.props.items[0].timestamp}
84-
//showEmoji={this.props.settings.showEmoji}
84+
prevtime={items[0].timestamp}
85+
//showEmoji={settings.showEmoji}
8586
language={language}
8687
/>
8788
}
8889

8990
{/* loop through the trimmed items */}
9091
{trimmedItems.map((e, i) => {
9192
const host = (e.object_type === 1 ? e.name : e.host_name);
92-
const prevtime = (i > 0 ? this.props.items[i - 1].timestamp : 0);
93+
const prevtime = (i > 0 ? items[i - 1].timestamp : 0);
9394
return (
9495
<AlertItem
9596
key={'alert-' + host + '-' + e.object_type + '-' + e.timestamp + '-' + i}
9697
e={e}
9798
i={i}
9899
prevtime={prevtime}
99-
//showEmoji={this.props.showEmoji}
100+
//showEmoji={showEmoji}
100101
language={language}
101102
locale={locale}
102103
dateFormat={dateFormat}
103-
settings={this.props.settings}
104-
isDemoMode={this.props.isDemoMode}
104+
settings={settings}
105+
isDemoMode={isDemoMode}
105106
/>
106107
);
107108
})}
108109

109110
<div className="ShowMoreArea">
110-
{this.state.howManyToRender > this.state.pageSize &&
111+
{howManyToRender > pageSize &&
111112
<span>
112-
<button className="uppercase-first" onClick={this.showLess}>{translate('show less', language)}</button>
113+
<button className="uppercase-first" onClick={showLess}>{translate('show less', language)}</button>
113114
</span>
114115
}
115-
{this.props.items.length > this.state.howManyToRender &&
116+
{items.length > howManyToRender &&
116117
<span>
117-
<button className="uppercase-first" onClick={this.showMore}>{translate('show more', language)}</button>
118+
<button className="uppercase-first" onClick={showMore}>{translate('show more', language)}</button>
118119
</span>
119120
}
120121
</div>
121122
</div>
122-
);
123-
}
124-
}
123+
);
124+
};
125125

126126
export default AlertItems;

src/components/alerts/AlertSection.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,33 @@
1919
text-align: center;
2020
font-size: 1em;
2121
color: #555;
22+
}
23+
24+
/* alert search */
25+
26+
.AlertSection .alert-search-input {
27+
padding: 1px 6px;
28+
font-size: 0.9em;
29+
border: 1px solid #444;
30+
border-radius: 4px;
31+
width: 200px;
32+
position: relative;
33+
top: -1px;
34+
margin-left: 5px;
35+
}
36+
.AlertSection .alert-search-input:focus {
37+
outline: none;
38+
border-color: #888;
39+
}
40+
.AlertSection .alert-search-clear {
41+
margin-left: 4px;
42+
background: none;
43+
border: none;
44+
cursor: pointer;
45+
font-size: 0.9em;
46+
color: #888;
47+
padding: 2px 4px;
48+
}
49+
.AlertSection .alert-search-clear:hover {
50+
color: #333;
2251
}

src/components/alerts/AlertSection.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

19-
import { useCallback, useEffect } from 'react';
19+
import { useCallback, useEffect, useMemo, useState } from 'react';
2020
import { AnimatePresence } from "motion/react";
2121
import * as motion from "motion/react-client";
2222
// State Management
2323
import { useAtom, useAtomValue } from 'jotai';
2424
import { bigStateAtom, clientSettingsAtom, clientSettingsInitial } from '../../atoms/settingsState';
25-
import { alertIsFetchingAtom, alertAtom, alertHowManyAtom } from '../../atoms/alertAtom';
25+
import { alertIsFetchingAtom, alertAtom, alertHowManyAtom, alertSearchTextAtom } from '../../atoms/alertAtom';
2626

2727
import { translate } from '../../helpers/language';
2828

@@ -44,6 +44,34 @@ const AlertSection = () => {
4444

4545
//console.log('AlertSection run');
4646

47+
const [alertSearchText, setAlertSearchText] = useAtom(alertSearchTextAtom);
48+
const [localSearchText, setLocalSearchText] = useState(alertSearchText);
49+
50+
// Debounce updating the shared atom so filtering doesn't run on every keystroke
51+
const debouncedSetSearchText = useMemo(
52+
() => _.debounce((value: string) => setAlertSearchText(value), 300),
53+
[setAlertSearchText],
54+
);
55+
56+
// Cleanup debounce on unmount
57+
useEffect(() => {
58+
return () => {
59+
debouncedSetSearchText.cancel();
60+
};
61+
}, [debouncedSetSearchText]);
62+
63+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
64+
const value = e.target.value;
65+
setLocalSearchText(value);
66+
debouncedSetSearchText(value);
67+
};
68+
69+
const handleSearchClear = () => {
70+
setLocalSearchText('');
71+
debouncedSetSearchText.cancel();
72+
setAlertSearchText('');
73+
};
74+
4775
// State Management state (this section)
4876
const [alertIsFetching, setAlertIsFetching] = useAtom(alertIsFetchingAtom);
4977
const [alertState, setAlertState] = useAtom(alertAtom);
@@ -234,6 +262,18 @@ const AlertSection = () => {
234262
return false;
235263
}
236264
}
265+
// search filter
266+
if (alertSearchText) {
267+
const searchLower = alertSearchText.toLowerCase();
268+
const matchesSearch =
269+
(alert.name && alert.name.toLowerCase().includes(searchLower)) ||
270+
(alert.host_name && alert.host_name.toLowerCase().includes(searchLower)) ||
271+
(alert.description && alert.description.toLowerCase().includes(searchLower)) ||
272+
(alert.plugin_output && alert.plugin_output.toLowerCase().includes(searchLower));
273+
if (!matchesSearch) {
274+
return false;
275+
}
276+
}
237277
return true;
238278
});
239279

@@ -258,6 +298,26 @@ const AlertSection = () => {
258298
howManyAlertSoft={alertHowManyState.howManyAlertSoft}
259299
/>
260300

301+
{/* alert search */}
302+
<div style={{ display: 'inline-block', marginLeft: '10px' }}>
303+
<input
304+
type="text"
305+
className="alert-search-input"
306+
placeholder="Search alerts..."
307+
value={localSearchText}
308+
onChange={handleSearchChange}
309+
/>
310+
{localSearchText && (
311+
<button
312+
className="alert-search-clear"
313+
onClick={handleSearchClear}
314+
title="Clear search"
315+
>
316+
317+
</button>
318+
)}
319+
</div>
320+
261321
{/* loading spinner */}
262322
<PollingSpinner
263323
isFetching={alertIsFetching}

0 commit comments

Comments
 (0)