Skip to content

Commit 6cf0037

Browse files
authored
844 toggle on and off of the extractor livelihood (#869)
* working * toggle logic works * styling of the switch component * set back * black format * update aggregated filters * add more comments and docstrings * black format
1 parent 9ab9ef1 commit 6cf0037

File tree

5 files changed

+128
-31
lines changed

5 files changed

+128
-31
lines changed

backend/app/routers/listeners.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,31 @@ async def _check_livelihood(
9191
return True
9292

9393

94+
def _check_livelihood_query(heartbeat_interval=settings.listener_heartbeat_interval):
95+
"""
96+
Return a MongoDB aggregation query to check the livelihood of a listener.
97+
This is needed due to the alive field being a computed field.
98+
When a user call the /listeners endpoint, the alive field will be computed with the current datetime
99+
and compared with heartbeat_interval.
100+
"""
101+
if heartbeat_interval == 0:
102+
heartbeat_interval = settings.listener_heartbeat_interval
103+
104+
# Perform aggregation queries that adds alive flag using the PyMongo syntax
105+
aggregated_query = {
106+
"$addFields": {
107+
"alive": {
108+
"$lt": [
109+
{"$subtract": [datetime.datetime.utcnow(), "$lastAlive"]},
110+
heartbeat_interval * 1000, # convert to milliseconds
111+
]
112+
}
113+
}
114+
}
115+
116+
return aggregated_query
117+
118+
94119
@router.get("/instance")
95120
async def get_instance_id(
96121
user=Depends(get_current_user),
@@ -246,33 +271,40 @@ async def get_listeners(
246271
heartbeat_interval: Optional[int] = settings.listener_heartbeat_interval,
247272
category: Optional[str] = None,
248273
label: Optional[str] = None,
274+
alive_only: Optional[bool] = False,
249275
):
250276
"""Get a list of all Event Listeners in the db.
251277
252278
Arguments:
253279
skip -- number of initial records to skip (i.e. for pagination)
254280
limit -- restrict number of records to be returned (i.e. for pagination)
281+
heartbeat_interval -- number of seconds after which a listener is considered dead
255282
category -- filter by category has to be exact match
256283
label -- filter by label has to be exact match
284+
alive_only -- filter by alive status
257285
"""
258-
query = []
286+
# First compute alive flag for all listeners
287+
aggregation_pipeline = [
288+
_check_livelihood_query(heartbeat_interval=heartbeat_interval)
289+
]
290+
291+
# Add filters if applicable
259292
if category:
260-
query.append(EventListenerDB.properties.categories == category)
293+
aggregation_pipeline.append({"$match": {"properties.categories": category}})
261294
if label:
262-
query.append(EventListenerDB.properties.default_labels == label)
295+
aggregation_pipeline.append({"$match": {"properties.default_labels": label}})
296+
if alive_only:
297+
aggregation_pipeline.append({"$match": {"alive": True}}),
263298

264-
# sort by name alphabetically
265-
listeners = await EventListenerDB.find(
266-
*query, skip=skip, limit=limit, sort=EventListenerDB.name
267-
).to_list()
299+
# Sort by name alphabetically and then pagination
300+
aggregation_pipeline.append({"$sort": {"name": 1}})
301+
aggregation_pipeline.append({"$skip": skip})
302+
aggregation_pipeline.append({"$limit": limit})
268303

269-
# batch return listener statuses for easy consumption
270-
listener_response = []
271-
for listener in listeners:
272-
listener.alive = await _check_livelihood(listener, heartbeat_interval)
273-
listener_response.append(listener.dict())
304+
# Run aggregate query and return
305+
listeners = await EventListenerDB.aggregate(aggregation_pipeline).to_list()
274306

275-
return listener_response
307+
return listeners
276308

277309

278310
@router.put("/{listener_id}", response_model=EventListenerOut)

frontend/src/actions/listeners.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export function fetchListeners(
88
limit = 21,
99
heartbeatInterval = 0,
1010
category = null,
11-
label = null
11+
label = null,
12+
aliveOnly = false
1213
) {
1314
return (dispatch) => {
1415
// TODO: Parameters for dates? paging?
@@ -17,7 +18,8 @@ export function fetchListeners(
1718
limit,
1819
heartbeatInterval,
1920
category,
20-
label
21+
label,
22+
aliveOnly
2123
)
2224
.then((json) => {
2325
dispatch({
@@ -30,7 +32,14 @@ export function fetchListeners(
3032
dispatch(
3133
handleErrors(
3234
reason,
33-
fetchListeners(skip, limit, heartbeatInterval, category, label)
35+
fetchListeners(
36+
skip,
37+
limit,
38+
heartbeatInterval,
39+
category,
40+
label,
41+
aliveOnly
42+
)
3443
)
3544
);
3645
});

frontend/src/components/listeners/ExtractionHistory.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,17 @@ export const ExtractionHistory = (): JSX.Element => {
8080
limit: number | undefined,
8181
heartbeatInterval: number | undefined,
8282
selectedCategory: string | null,
83-
selectedLabel: string | null
83+
selectedLabel: string | null,
84+
aliveOnly: boolean | undefined
8485
) =>
8586
dispatch(
8687
fetchListeners(
8788
skip,
8889
limit,
8990
heartbeatInterval,
9091
selectedCategory,
91-
selectedLabel
92+
selectedLabel,
93+
aliveOnly
9294
)
9395
);
9496
const listListenerJobs = (
@@ -128,9 +130,10 @@ export const ExtractionHistory = (): JSX.Element => {
128130
const [executionJobsTableRow, setExecutionJobsTableRow] = useState([]);
129131
const [selectedStatus, setSelectedStatus] = useState(null);
130132
const [selectedCreatedTime, setSelectedCreatedTime] = useState(null);
133+
const [aliveOnly, setAliveOnly] = useState<boolean>(false);
131134

132135
useEffect(() => {
133-
listListeners(skip, limit, 0, null, null);
136+
listListeners(skip, limit, 0, null, null, aliveOnly);
134137
listListenerJobs(null, null, null, null, null, null, 0, 100);
135138
}, []);
136139

@@ -197,7 +200,7 @@ export const ExtractionHistory = (): JSX.Element => {
197200

198201
useEffect(() => {
199202
if (skip !== null && skip !== undefined) {
200-
listListeners(skip, limit, 0, null, null);
203+
listListeners(skip, limit, 0, null, null, aliveOnly);
201204
if (skip === 0) setPrevDisabled(true);
202205
else setPrevDisabled(false);
203206
}

frontend/src/components/listeners/Listeners.tsx

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import {
55
ButtonGroup,
66
Divider,
77
FormControl,
8+
FormControlLabel,
89
Grid,
910
InputLabel,
1011
List,
1112
MenuItem,
1213
Paper,
1314
Select,
15+
Switch,
1416
} from "@mui/material";
1517

1618
import { RootState } from "../../types/data";
@@ -42,15 +44,17 @@ export function Listeners(props: ListenerProps) {
4244
limit: number | undefined,
4345
heartbeatInterval: number | undefined,
4446
selectedCategory: string | null,
45-
selectedLabel: string | null
47+
selectedLabel: string | null,
48+
aliveOnly: boolean | undefined
4649
) =>
4750
dispatch(
4851
fetchListeners(
4952
skip,
5053
limit,
5154
heartbeatInterval,
5255
selectedCategory,
53-
selectedLabel
56+
selectedLabel,
57+
aliveOnly
5458
)
5559
);
5660
const searchListeners = (
@@ -81,10 +85,11 @@ export function Listeners(props: ListenerProps) {
8185
const [searchText, setSearchText] = useState<string>("");
8286
const [selectedCategory, setSelectedCategory] = useState("");
8387
const [selectedLabel, setSelectedLabel] = useState("");
88+
const [aliveOnly, setAliveOnly] = useState<boolean>(false);
8489

8590
// component did mount
8691
useEffect(() => {
87-
listListeners(skip, limit, 0, null, null);
92+
listListeners(skip, limit, 0, null, null, aliveOnly);
8893
listAvailableCategories();
8994
listAvailableLabels();
9095
}, []);
@@ -101,13 +106,20 @@ export function Listeners(props: ListenerProps) {
101106
if (searchText !== "") {
102107
handleListenerSearch();
103108
} else {
104-
listListeners(skip, limit, 0, selectedCategory, selectedLabel);
109+
listListeners(skip, limit, 0, selectedCategory, selectedLabel, aliveOnly);
105110
}
106111
}, [searchText]);
107112

113+
useEffect(() => {
114+
setSearchText("");
115+
// flip to first page
116+
setSkip(0);
117+
listListeners(0, limit, 0, selectedCategory, selectedLabel, aliveOnly);
118+
}, [aliveOnly]);
119+
108120
useEffect(() => {
109121
if (skip !== null && skip !== undefined) {
110-
listListeners(skip, limit, 0, null, null);
122+
listListeners(skip, limit, 0, null, null, aliveOnly);
111123
if (skip === 0) setPrevDisabled(true);
112124
else setPrevDisabled(false);
113125
}
@@ -123,11 +135,18 @@ export function Listeners(props: ListenerProps) {
123135
} else {
124136
// set the interval to fetch the job's log
125137
const interval = setInterval(() => {
126-
listListeners(skip, limit, 0, selectedCategory, selectedLabel);
138+
listListeners(
139+
skip,
140+
limit,
141+
0,
142+
selectedCategory,
143+
selectedLabel,
144+
aliveOnly
145+
);
127146
}, config.extractorLivelihoodInterval);
128147
return () => clearInterval(interval);
129148
}
130-
}, [searchText, listeners, skip, selectedCategory, selectedLabel]);
149+
}, [searchText, listeners, skip, selectedCategory, selectedLabel, aliveOnly]);
131150

132151
// for pagination keep flipping until the return dataset is less than the limit
133152
const previous = () => {
@@ -152,14 +171,28 @@ export function Listeners(props: ListenerProps) {
152171
const selectedCategoryValue = (event.target as HTMLInputElement).value;
153172
setSelectedCategory(selectedCategoryValue);
154173
setSearchText("");
155-
listListeners(skip, limit, 0, selectedCategoryValue, selectedLabel);
174+
listListeners(
175+
skip,
176+
limit,
177+
0,
178+
selectedCategoryValue,
179+
selectedLabel,
180+
aliveOnly
181+
);
156182
};
157183

158184
const handleLabelChange = (event: React.ChangeEvent<HTMLInputElement>) => {
159185
const selectedLabelValue = (event.target as HTMLInputElement).value;
160186
setSelectedLabel(selectedLabelValue);
161187
setSearchText("");
162-
listListeners(skip, limit, 0, selectedCategory, selectedLabelValue);
188+
listListeners(
189+
skip,
190+
limit,
191+
0,
192+
selectedCategory,
193+
selectedLabelValue,
194+
aliveOnly
195+
);
163196
};
164197

165198
const handleSubmitExtractionClose = () => {
@@ -189,7 +222,7 @@ export function Listeners(props: ListenerProps) {
189222
spacing={3}
190223
>
191224
{/*categories*/}
192-
<Grid item xs={6}>
225+
<Grid item xs={5}>
193226
<FormControl variant="standard" sx={{ width: "100%" }}>
194227
<InputLabel id="label-categories">Filter by category</InputLabel>
195228
<Select
@@ -209,7 +242,7 @@ export function Listeners(props: ListenerProps) {
209242
</Select>
210243
</FormControl>
211244
</Grid>
212-
<Grid item xs={6}>
245+
<Grid item xs={5}>
213246
<FormControl variant="standard" sx={{ width: "100%" }}>
214247
<InputLabel id="label-categories">Filter by labels</InputLabel>
215248
<Select
@@ -227,6 +260,21 @@ export function Listeners(props: ListenerProps) {
227260
</Select>
228261
</FormControl>
229262
</Grid>
263+
<Grid item xs={2}>
264+
<FormControlLabel
265+
sx={{ margin: "auto auto -2.5em auto" }}
266+
control={
267+
<Switch
268+
color="primary"
269+
checked={aliveOnly}
270+
onChange={() => {
271+
setAliveOnly(!aliveOnly);
272+
}}
273+
/>
274+
}
275+
label="Alive Extractors"
276+
/>
277+
</Grid>
230278
</Grid>
231279
<Grid container>
232280
<Grid item xs={12}>

frontend/src/openapi/v2/services/ListenersService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ export class ListenersService {
2727
* Arguments:
2828
* skip -- number of initial records to skip (i.e. for pagination)
2929
* limit -- restrict number of records to be returned (i.e. for pagination)
30+
* heartbeat_interval -- number of seconds after which a listener is considered dead
3031
* category -- filter by category has to be exact match
3132
* label -- filter by label has to be exact match
33+
* alive_only -- filter by alive status
3234
* @param skip
3335
* @param limit
3436
* @param heartbeatInterval
3537
* @param category
3638
* @param label
39+
* @param aliveOnly
3740
* @returns EventListenerOut Successful Response
3841
* @throws ApiError
3942
*/
@@ -43,6 +46,7 @@ export class ListenersService {
4346
heartbeatInterval: number = 300,
4447
category?: string,
4548
label?: string,
49+
aliveOnly: boolean = false,
4650
): CancelablePromise<Array<EventListenerOut>> {
4751
return __request({
4852
method: 'GET',
@@ -53,6 +57,7 @@ export class ListenersService {
5357
'heartbeat_interval': heartbeatInterval,
5458
'category': category,
5559
'label': label,
60+
'alive_only': aliveOnly,
5661
},
5762
errors: {
5863
422: `Validation Error`,

0 commit comments

Comments
 (0)