diff --git a/backend/app/deps/authorization_deps.py b/backend/app/deps/authorization_deps.py
index af6104e01..308406e4f 100644
--- a/backend/app/deps/authorization_deps.py
+++ b/backend/app/deps/authorization_deps.py
@@ -3,6 +3,7 @@
from app.models.datasets import DatasetDB, DatasetStatus
from app.models.files import FileDB, FileStatus
from app.models.groups import GroupDB
+from app.models.listeners import EventListenerDB
from app.models.metadata import MetadataDB
from app.routers.authentication import get_admin, get_admin_mode
from beanie import PydanticObjectId
@@ -389,6 +390,41 @@ async def __call__(
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
+class ListenerAuthorization:
+ """We use class dependency so that we can provide the `permission` parameter to the dependency.
+ For more info see https://fastapi.tiangolo.com/advanced/advanced-dependencies/.
+ Regular users are not allowed to run non-active listeners"""
+
+ # def __init__(self, optional_arg: str = None):
+ # self.optional_arg = optional_arg
+
+ async def __call__(
+ self,
+ listener_id: str,
+ current_user: str = Depends(get_current_username),
+ admin_mode: bool = Depends(get_admin_mode),
+ admin: bool = Depends(get_admin),
+ ):
+ # If the current user is admin and has turned on admin_mode, user has access irrespective of any role assigned
+ if admin and admin_mode:
+ return True
+
+ # Else check if listener is active or current user is the creator of the extractor
+ if (
+ listener := await EventListenerDB.get(PydanticObjectId(listener_id))
+ ) is not None:
+ if listener.active is True or (
+ listener.creator and listener.creator.email == current_user
+ ):
+ return True
+ else:
+ raise HTTPException(
+ status_code=403,
+ detail=f"User `{current_user} does not have permission on listener `{listener_id}`",
+ )
+ raise HTTPException(status_code=404, detail=f"Listener {listener_id} not found")
+
+
class CheckStatus:
"""We use class dependency so that we can provide the `permission` parameter to the dependency.
For more info see https://fastapi.tiangolo.com/advanced/advanced-dependencies/."""
diff --git a/backend/app/models/listeners.py b/backend/app/models/listeners.py
index 2d855c6ff..1bd33789b 100644
--- a/backend/app/models/listeners.py
+++ b/backend/app/models/listeners.py
@@ -69,6 +69,7 @@ class EventListenerDB(Document, EventListenerBase):
modified: datetime = Field(default_factory=datetime.now)
lastAlive: datetime = None
alive: Optional[bool] = None # made up field to indicate if extractor is alive
+ active: bool = False
properties: Optional[ExtractorInfo] = None
class Settings:
diff --git a/backend/app/routers/feeds.py b/backend/app/routers/feeds.py
index 42f9dc110..0500bc30a 100644
--- a/backend/app/routers/feeds.py
+++ b/backend/app/routers/feeds.py
@@ -1,11 +1,13 @@
from typing import List, Optional
+from app.deps.authorization_deps import ListenerAuthorization
from app.keycloak_auth import get_current_user, get_current_username
from app.models.feeds import FeedDB, FeedIn, FeedOut
from app.models.files import FileOut
from app.models.listeners import EventListenerDB, FeedListener
from app.models.users import UserOut
from app.rabbitmq.listeners import submit_file_job
+from app.routers.authentication import get_admin, get_admin_mode
from app.search.connect import check_search_result
from beanie import PydanticObjectId
from fastapi import APIRouter, Depends, HTTPException
@@ -23,7 +25,7 @@ async def disassociate_listener_db(feed_id: str, listener_id: str):
if (feed := await FeedDB.get(PydanticObjectId(feed_id))) is not None:
new_listeners = []
for feed_listener in feed.listeners:
- if feed_listener.listener_id != listener_id:
+ if feed_listener.listener_id != PydanticObjectId(listener_id):
new_listeners.append(feed_listener)
feed.listeners = new_listeners
await feed.save()
@@ -122,6 +124,8 @@ async def associate_listener(
feed_id: str,
listener: FeedListener,
user=Depends(get_current_user),
+ admin=Depends(get_admin),
+ admin_mode=Depends(get_admin_mode),
):
"""Associate an existing Event Listener with a Feed, e.g. so it will be triggered on new Feed results.
@@ -131,22 +135,35 @@ async def associate_listener(
"""
if (feed := await FeedDB.get(PydanticObjectId(feed_id))) is not None:
if (
- await EventListenerDB.get(PydanticObjectId(listener.listener_id))
+ listener_db := await EventListenerDB.get(
+ PydanticObjectId(listener.listener_id)
+ )
) is not None:
- feed.listeners.append(listener)
- await feed.save()
- return feed.dict()
+ if (
+ (admin and admin_mode)
+ or (listener_db.creator and listener_db.creator.email == user.email)
+ or listener_db.active
+ ):
+ feed.listeners.append(listener)
+ await feed.save()
+ return feed.dict()
+ else:
+ raise HTTPException(
+ status_code=403,
+ detail=f"User {user} doesn't have permission to submit job to listener {listener.listener_id}",
+ )
raise HTTPException(
status_code=404, detail=f"listener {listener.listener_id} not found"
)
raise HTTPException(status_code=404, detail=f"feed {feed_id} not found")
-@router.delete("/{feed_id}/listeners/{listener_id}", response_model=FeedOut)
+@router.delete("/{feed_id}/listeners/{listener_id}")
async def disassociate_listener(
feed_id: str,
listener_id: str,
user=Depends(get_current_user),
+ allow: bool = Depends(ListenerAuthorization()),
):
"""Disassociate an Event Listener from a Feed.
diff --git a/backend/app/routers/listeners.py b/backend/app/routers/listeners.py
index 3febfe633..949dea0f8 100644
--- a/backend/app/routers/listeners.py
+++ b/backend/app/routers/listeners.py
@@ -5,6 +5,7 @@
from typing import List, Optional
from app.config import settings
+from app.deps.authorization_deps import ListenerAuthorization
from app.keycloak_auth import get_current_user, get_current_username, get_user
from app.models.config import ConfigEntryDB
from app.models.feeds import FeedDB, FeedListener
@@ -18,6 +19,7 @@
from app.models.pages import Paged, _construct_page_metadata, _get_page_query
from app.models.search import SearchCriteria
from app.models.users import UserOut
+from app.routers.authentication import get_admin, get_admin_mode
from app.routers.feeds import disassociate_listener_db
from beanie import PydanticObjectId
from beanie.operators import Or, RegEx
@@ -198,6 +200,8 @@ async def search_listeners(
heartbeat_interval: Optional[int] = settings.listener_heartbeat_interval,
user=Depends(get_current_username),
process: Optional[str] = None,
+ admin=Depends(get_admin),
+ admin_mode=Depends(get_admin_mode),
):
"""Search all Event Listeners in the db based on text.
@@ -221,6 +225,8 @@ async def search_listeners(
aggregation_pipeline.append(
{"$match": {"properties.process.dataset": {"$exists": True}}}
)
+ if not admin or not admin_mode:
+ aggregation_pipeline.append({"$match": {"active": True}})
# Add pagination
aggregation_pipeline.append(
_get_page_query(skip, limit, sort_field="name", ascending=True)
@@ -260,7 +266,11 @@ async def list_default_labels(user=Depends(get_current_username)):
@router.get("/{listener_id}", response_model=EventListenerOut)
-async def get_listener(listener_id: str, user=Depends(get_current_username)):
+async def get_listener(
+ listener_id: str,
+ user=Depends(get_current_username),
+ allow: bool = Depends(ListenerAuthorization()),
+):
"""Return JSON information about an Event Listener if it exists."""
if (
listener := await EventListenerDB.get(PydanticObjectId(listener_id))
@@ -274,6 +284,7 @@ async def check_listener_livelihood(
listener_id: str,
heartbeat_interval: Optional[int] = settings.listener_heartbeat_interval,
user=Depends(get_current_username),
+ allow: bool = Depends(ListenerAuthorization()),
):
"""Return JSON information about an Event Listener if it exists."""
if (
@@ -293,6 +304,9 @@ async def get_listeners(
label: Optional[str] = None,
alive_only: Optional[bool] = False,
process: Optional[str] = None,
+ all: Optional[bool] = False,
+ admin=Depends(get_admin),
+ admin_mode=Depends(get_admin_mode),
):
"""Get a list of all Event Listeners in the db.
@@ -303,6 +317,7 @@ async def get_listeners(
category -- filter by category has to be exact match
label -- filter by label has to be exact match
alive_only -- filter by alive status
+ all -- boolean stating if we want to show all listeners irrespective of admin and admin_mode
"""
# First compute alive flag for all listeners
aggregation_pipeline = [
@@ -325,6 +340,9 @@ async def get_listeners(
aggregation_pipeline.append(
{"$match": {"properties.process.dataset": {"$exists": True}}}
)
+ # Non admin users can access only active listeners unless all is turned on for Extractor page
+ if not all and (not admin or not admin_mode):
+ aggregation_pipeline.append({"$match": {"active": True}})
# Add pagination
aggregation_pipeline.append(
_get_page_query(skip, limit, sort_field="name", ascending=True)
@@ -351,6 +369,7 @@ async def edit_listener(
listener_id: str,
listener_in: EventListenerIn,
user_id=Depends(get_user),
+ allow: bool = Depends(ListenerAuthorization()),
):
"""Update the information about an existing Event Listener..
@@ -374,10 +393,62 @@ async def edit_listener(
raise HTTPException(status_code=404, detail=f"listener {listener_id} not found")
+@router.put("/{listener_id}/enable", response_model=EventListenerOut)
+async def enable_listener(
+ listener_id: str,
+ user_id=Depends(get_user),
+ allow: bool = Depends(ListenerAuthorization()),
+):
+ """Enable an Event Listener. Only admins can enable listeners.
+
+ Arguments:
+ listener_id -- UUID of the listener to be enabled
+ """
+ return await _set_active_flag(listener_id, True)
+
+
+@router.put("/{listener_id}/disable", response_model=EventListenerOut)
+async def disable_listener(
+ listener_id: str,
+ user_id=Depends(get_user),
+ allow: bool = Depends(ListenerAuthorization()),
+):
+ """Disable an Event Listener. Only admins can enable listeners.
+
+ Arguments:
+ listener_id -- UUID of the listener to be enabled
+ """
+ return await _set_active_flag(listener_id, False)
+
+
+async def _set_active_flag(
+ listener_id: str,
+ active: bool,
+ allow: bool = Depends(ListenerAuthorization()),
+):
+ """Set the active flag of an Event Listener. Only admins can enable/disable listeners.
+
+ Arguments:
+ listener_id -- UUID of the listener to be enabled/disabled
+ """
+ listener = await EventListenerDB.find_one(
+ EventListenerDB.id == ObjectId(listener_id)
+ )
+ if listener:
+ try:
+ listener.active = active
+ await listener.save()
+ return listener.dict()
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=e.args[0])
+ raise HTTPException(status_code=404, detail=f"listener {listener_id} not found")
+
+
@router.delete("/{listener_id}")
async def delete_listener(
listener_id: str,
user=Depends(get_current_username),
+ allow: bool = Depends(ListenerAuthorization()),
):
"""Remove an Event Listener from the database. Will not clear event history for the listener."""
listener = await EventListenerDB.find_one(
diff --git a/backend/app/tests/test_extractors.py b/backend/app/tests/test_extractors.py
index c3bcb72e8..ff27e4f3d 100644
--- a/backend/app/tests/test_extractors.py
+++ b/backend/app/tests/test_extractors.py
@@ -60,3 +60,23 @@ def test_v1_mime_trigger(client: TestClient, headers: dict):
)
assert response.status_code == 200
assert len(response.json()) > 0
+
+
+def test_enable_disable_extractor(client: TestClient, headers: dict):
+ # create a new extractor
+ ext_name = "test.v1_extractor"
+ extractor_id = register_v1_extractor(client, headers, ext_name).get("id")
+
+ # enable the extractor
+ response = client.put(
+ f"{settings.API_V2_STR}/listeners/{extractor_id}/enable", headers=headers
+ )
+ assert response.status_code == 200
+ assert response.json()["active"] is True
+
+ # disable the extractor
+ response = client.put(
+ f"{settings.API_V2_STR}/listeners/{extractor_id}/disable", headers=headers
+ )
+ assert response.status_code == 200
+ assert response.json()["active"] is False
diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js
index 41d24f42e..061a44d44 100644
--- a/frontend/src/actions/listeners.js
+++ b/frontend/src/actions/listeners.js
@@ -10,7 +10,8 @@ export function fetchListeners(
category = null,
label = null,
aliveOnly = false,
- process = null
+ process = null,
+ all = false
) {
return (dispatch) => {
// TODO: Parameters for dates? paging?
@@ -21,7 +22,8 @@ export function fetchListeners(
category,
label,
aliveOnly,
- process
+ process,
+ all
)
.then((json) => {
dispatch({
@@ -49,6 +51,45 @@ export function fetchListeners(
};
}
+export const TOGGLE_ACTIVE_FLAG_LISTENER = "TOGGLE_ACTIVE_FLAG_LISTENER";
+export function enableListener(id) {
+ return (dispatch) => {
+ return V2.ListenersService.enableListenerApiV2ListenersListenerIdEnablePut(
+ id
+ )
+ .then((json) => {
+ // We could have called fetchListeners but it would be an overhead since we are just toggling the active flag for one listener.
+ // Hence we create a separate action to update the particular listener in state
+ dispatch({
+ type: TOGGLE_ACTIVE_FLAG_LISTENER,
+ listener: json,
+ });
+ })
+ .catch((reason) => {
+ dispatch(handleErrors(reason, enableListener(id)));
+ });
+ };
+}
+
+export function disableListener(id) {
+ return (dispatch) => {
+ return V2.ListenersService.disableListenerApiV2ListenersListenerIdDisablePut(
+ id
+ )
+ .then((json) => {
+ // We could have called fetchListeners but it would be an overhead since we are just toggling the active flag for one listener.
+ // Hence we create a separate action to update the particular listener in state
+ dispatch({
+ type: TOGGLE_ACTIVE_FLAG_LISTENER,
+ listener: json,
+ });
+ })
+ .catch((reason) => {
+ dispatch(handleErrors(reason, disableListener(id)));
+ });
+ };
+}
+
export const SEARCH_LISTENERS = "SEARCH_LISTENERS";
export function queryListeners(
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index fbf1e119d..0a99302c9 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -39,6 +39,7 @@ import { AdminPanelSettings } from "@mui/icons-material";
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
import { Footer } from "./navigation/Footer";
+import BuildIcon from "@mui/icons-material/Build";
const drawerWidth = 240;
@@ -419,6 +420,17 @@ export default function PersistentDrawerLeft(props) {
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/listeners/AllListeners.tsx b/frontend/src/components/listeners/AllListeners.tsx
new file mode 100644
index 000000000..6d97535df
--- /dev/null
+++ b/frontend/src/components/listeners/AllListeners.tsx
@@ -0,0 +1,402 @@
+import React, { ChangeEvent, useEffect, useState } from "react";
+import {
+ Box,
+ Divider,
+ FormControl,
+ FormControlLabel,
+ Grid,
+ Input,
+ InputLabel,
+ List,
+ MenuItem,
+ Pagination,
+ Paper,
+ Select,
+ Switch,
+ Tooltip,
+} from "@mui/material";
+
+import Table from "@mui/material/Table";
+
+import { RootState } from "../../types/data";
+import { useDispatch, useSelector } from "react-redux";
+import {
+ fetchListenerCategories,
+ fetchListenerLabels,
+ fetchListeners,
+ queryListeners,
+ enableListener as enableListenerAction,
+ disableListener as disableListenerAction,
+} from "../../actions/listeners";
+import ListenerItem from "./ListenerItem";
+import SubmitExtraction from "./SubmitExtraction";
+import { capitalize } from "../../utils/common";
+import config from "../../app.config";
+import { GenericSearchBox } from "../search/GenericSearchBox";
+import Layout from "../Layout";
+import TableCell from "@mui/material/TableCell";
+import TableRow from "@mui/material/TableRow";
+import TableBody from "@mui/material/TableBody";
+import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
+
+type ListenerProps = {
+ process?: string;
+};
+
+export function AllListeners(props: ListenerProps) {
+ const { process } = props;
+ // Redux connect equivalent
+ const dispatch = useDispatch();
+ const listListeners = (
+ skip: number | undefined,
+ limit: number | undefined,
+ heartbeatInterval: number | undefined,
+ selectedCategory: string | null,
+ selectedLabel: string | null,
+ aliveOnly: boolean | undefined,
+ process: string | undefined
+ ) =>
+ dispatch(
+ fetchListeners(
+ skip,
+ limit,
+ heartbeatInterval,
+ selectedCategory,
+ selectedLabel,
+ aliveOnly,
+ process,
+ true
+ )
+ );
+ const searchListeners = (
+ text: string,
+ skip: number | undefined,
+ limit: number | undefined,
+ heartbeatInterval: number | undefined,
+ process: string | undefined
+ ) => dispatch(queryListeners(text, skip, limit, heartbeatInterval, process));
+ const listAvailableCategories = () => dispatch(fetchListenerCategories());
+ const listAvailableLabels = () => dispatch(fetchListenerLabels());
+
+ const enableListener = (id: string) => dispatch(enableListenerAction(id));
+ const disableListener = (id: string) => dispatch(disableListenerAction(id));
+
+ const listeners = useSelector(
+ (state: RootState) => state.listener.listeners.data
+ );
+ const pageMetadata = useSelector(
+ (state: RootState) => state.listener.listeners.metadata
+ );
+ const categories = useSelector(
+ (state: RootState) => state.listener.categories
+ );
+ const admin = useSelector((state: RootState) => state.user.profile.admin);
+ const adminMode = useSelector((state: RootState) => state.user.adminMode);
+ const labels = useSelector((state: RootState) => state.listener.labels);
+
+ const [currPageNum, setCurrPageNum] = useState(1);
+ const [limit] = useState(config.defaultExtractors);
+ const [openSubmitExtraction, setOpenSubmitExtraction] =
+ useState(false);
+ const [infoOnly, setInfoOnly] = useState(true);
+ const [selectedExtractor, setSelectedExtractor] = useState();
+ const [searchText, setSearchText] = useState("");
+ const [selectedCategory, setSelectedCategory] = useState("");
+ const [selectedLabel, setSelectedLabel] = useState("");
+ const [aliveOnly, setAliveOnly] = useState(false);
+
+ // component did mount
+ useEffect(() => {
+ listListeners(0, limit, 0, null, null, aliveOnly, process);
+ listAvailableCategories();
+ listAvailableLabels();
+ }, [adminMode]);
+
+ // search
+ useEffect(() => {
+ // reset page and reset category with each new search term
+ setCurrPageNum(1);
+ setSelectedCategory("");
+
+ if (searchText !== "") searchListeners(searchText, 0, limit, 0, process);
+ else
+ listListeners(
+ 0,
+ limit,
+ 0,
+ selectedCategory,
+ selectedLabel,
+ aliveOnly,
+ process
+ );
+ }, [searchText, adminMode]);
+
+ useEffect(() => {
+ // reset page and reset search text with each new search term
+ setCurrPageNum(1);
+ setSearchText("");
+ listListeners(
+ 0,
+ limit,
+ 0,
+ selectedCategory,
+ selectedLabel,
+ aliveOnly,
+ process
+ );
+ }, [aliveOnly, adminMode]);
+
+ // any of the change triggers timer to fetch the extractor status
+ useEffect(() => {
+ if (searchText !== "") {
+ const interval = setInterval(() => {
+ handleListenerSearch();
+ }, config.extractorLivelihoodInterval);
+ return () => clearInterval(interval);
+ } else {
+ // set the interval to fetch the job's log
+ const interval = setInterval(() => {
+ listListeners(
+ (currPageNum - 1) * limit,
+ limit,
+ 0,
+ selectedCategory,
+ selectedLabel,
+ aliveOnly,
+ process
+ );
+ }, config.extractorLivelihoodInterval);
+ return () => clearInterval(interval);
+ }
+ }, [
+ searchText,
+ listeners,
+ currPageNum,
+ selectedCategory,
+ selectedLabel,
+ aliveOnly,
+ adminMode,
+ ]);
+
+ const handleListenerSearch = () => {
+ setSelectedCategory("");
+ searchListeners(searchText, (currPageNum - 1) * limit, limit, 0, process);
+ };
+
+ const handleCategoryChange = (event: React.ChangeEvent) => {
+ const selectedCategoryValue = (event.target as HTMLInputElement).value;
+ setSelectedCategory(selectedCategoryValue);
+ setSearchText("");
+ listListeners(
+ 0,
+ limit,
+ 0,
+ selectedCategoryValue,
+ selectedLabel,
+ aliveOnly,
+ process
+ );
+ };
+
+ const handleLabelChange = (event: React.ChangeEvent) => {
+ const selectedLabelValue = (event.target as HTMLInputElement).value;
+ setSelectedLabel(selectedLabelValue);
+ setSearchText("");
+ listListeners(
+ 0,
+ limit,
+ 0,
+ selectedCategory,
+ selectedLabelValue,
+ aliveOnly,
+ process
+ );
+ };
+
+ const handlePageChange = (_: ChangeEvent, value: number) => {
+ const newSkip = (value - 1) * limit;
+ setCurrPageNum(value);
+ if (searchText !== "")
+ searchListeners(searchText, newSkip, limit, 0, process);
+ else listListeners(newSkip, limit, 0, null, null, aliveOnly, process);
+ };
+
+ const handleSubmitExtractionClose = () => {
+ // Cleanup the form
+ setOpenSubmitExtraction(false);
+ };
+
+ return (
+
+
+
+
+ {/*searchbox*/}
+
+
+
+
+ {/*categories*/}
+
+
+ Filter by category
+
+
+
+
+
+ Filter by labels
+
+
+
+
+ {
+ setAliveOnly(!aliveOnly);
+ }}
+ />
+ }
+ label="Alive Extractors"
+ />
+
+
+
+
+
+
+
+ {listeners !== undefined ? (
+ listeners.map((listener) => {
+ return (
+
+
+
+
+
+ {/* extractor["alive"] ? (*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/*) : (*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {admin && adminMode ? (
+ {
+ if (listener.active) {
+ disableListener(listener.id);
+ } else {
+ enableListener(listener.id);
+ }
+ }}
+ />
+ ) : (
+
+
+
+ )}
+
+
+
+ );
+ })
+ ) : (
+ <>>
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/listeners/ListenerItem.tsx b/frontend/src/components/listeners/ListenerItem.tsx
index b149ded1a..473e8419d 100644
--- a/frontend/src/components/listeners/ListenerItem.tsx
+++ b/frontend/src/components/listeners/ListenerItem.tsx
@@ -15,6 +15,7 @@ type ListenerCardProps = {
setOpenSubmitExtraction: any;
setInfoOnly: any;
setSelectedExtractor: any;
+ showSubmit: boolean;
};
export default function ListenerItem(props: ListenerCardProps) {
@@ -28,6 +29,7 @@ export default function ListenerItem(props: ListenerCardProps) {
setOpenSubmitExtraction,
setInfoOnly,
setSelectedExtractor,
+ showSubmit,
} = props;
return (
@@ -91,20 +93,22 @@ export default function ListenerItem(props: ListenerCardProps) {
margin: "auto",
}}
>
- {
- setOpenSubmitExtraction(true);
- setSelectedExtractor(extractor);
- setInfoOnly(false);
- }}
- >
-
-
+ {showSubmit && (
+ {
+ setOpenSubmitExtraction(true);
+ setSelectedExtractor(extractor);
+ setInfoOnly(false);
+ }}
+ >
+
+
+ )}
);
diff --git a/frontend/src/components/listeners/Listeners.tsx b/frontend/src/components/listeners/Listeners.tsx
index 4556bb40d..e7c2f7d01 100644
--- a/frontend/src/components/listeners/Listeners.tsx
+++ b/frontend/src/components/listeners/Listeners.tsx
@@ -78,6 +78,7 @@ export function Listeners(props: ListenerProps) {
(state: RootState) => state.listener.categories
);
const labels = useSelector((state: RootState) => state.listener.labels);
+ const adminMode = useSelector((state: RootState) => state.user.adminMode);
const [currPageNum, setCurrPageNum] = useState(1);
const [limit] = useState(config.defaultExtractors);
@@ -95,7 +96,7 @@ export function Listeners(props: ListenerProps) {
listListeners(0, limit, 0, null, null, aliveOnly, process);
listAvailableCategories();
listAvailableLabels();
- }, []);
+ }, [adminMode]);
// search
useEffect(() => {
@@ -114,7 +115,7 @@ export function Listeners(props: ListenerProps) {
aliveOnly,
process
);
- }, [searchText]);
+ }, [searchText, adminMode]);
useEffect(() => {
// reset page and reset search text with each new search term
@@ -129,7 +130,7 @@ export function Listeners(props: ListenerProps) {
aliveOnly,
process
);
- }, [aliveOnly]);
+ }, [aliveOnly, adminMode]);
// any of the change triggers timer to fetch the extractor status
useEffect(() => {
@@ -160,6 +161,7 @@ export function Listeners(props: ListenerProps) {
selectedCategory,
selectedLabel,
aliveOnly,
+ adminMode,
]);
const handleListenerSearch = () => {
@@ -305,6 +307,7 @@ export function Listeners(props: ListenerProps) {
setInfoOnly={setInfoOnly}
setOpenSubmitExtraction={setOpenSubmitExtraction}
setSelectedExtractor={setSelectedExtractor}
+ showSubmit={true}
/>
>
diff --git a/frontend/src/openapi/v2/models/EventListenerOut.ts b/frontend/src/openapi/v2/models/EventListenerOut.ts
index a6a57f20f..0e7581766 100644
--- a/frontend/src/openapi/v2/models/EventListenerOut.ts
+++ b/frontend/src/openapi/v2/models/EventListenerOut.ts
@@ -18,5 +18,6 @@ export type EventListenerOut = {
modified?: string;
lastAlive?: string;
alive?: boolean;
+ active?: boolean;
properties?: ExtractorInfo;
}
diff --git a/frontend/src/openapi/v2/services/FeedsService.ts b/frontend/src/openapi/v2/services/FeedsService.ts
index 03e1fde19..18301925e 100644
--- a/frontend/src/openapi/v2/services/FeedsService.ts
+++ b/frontend/src/openapi/v2/services/FeedsService.ts
@@ -105,16 +105,21 @@ export class FeedsService {
* listener: JSON object with "listener_id" field and "automatic" bool field (whether to auto-trigger on new data)
* @param feedId
* @param requestBody
+ * @param datasetId
* @returns FeedOut Successful Response
* @throws ApiError
*/
public static associateListenerApiV2FeedsFeedIdListenersPost(
feedId: string,
requestBody: FeedListener,
+ datasetId?: string,
): CancelablePromise {
return __request({
method: 'POST',
path: `/api/v2/feeds/${feedId}/listeners`,
+ query: {
+ 'dataset_id': datasetId,
+ },
body: requestBody,
mediaType: 'application/json',
errors: {
@@ -132,16 +137,21 @@ export class FeedsService {
* listener_id: UUID of Event Listener that should be disassociated
* @param feedId
* @param listenerId
- * @returns FeedOut Successful Response
+ * @param datasetId
+ * @returns any Successful Response
* @throws ApiError
*/
public static disassociateListenerApiV2FeedsFeedIdListenersListenerIdDelete(
feedId: string,
listenerId: string,
- ): CancelablePromise {
+ datasetId?: string,
+ ): CancelablePromise {
return __request({
method: 'DELETE',
path: `/api/v2/feeds/${feedId}/listeners/${listenerId}`,
+ query: {
+ 'dataset_id': datasetId,
+ },
errors: {
422: `Validation Error`,
},
diff --git a/frontend/src/openapi/v2/services/ListenersService.ts b/frontend/src/openapi/v2/services/ListenersService.ts
index d0fa80add..bc64e818e 100644
--- a/frontend/src/openapi/v2/services/ListenersService.ts
+++ b/frontend/src/openapi/v2/services/ListenersService.ts
@@ -32,6 +32,7 @@ export class ListenersService {
* category -- filter by category has to be exact match
* label -- filter by label has to be exact match
* alive_only -- filter by alive status
+ * all -- boolean stating if we want to show all listeners irrespective of admin and admin_mode
* @param skip
* @param limit
* @param heartbeatInterval
@@ -39,6 +40,8 @@ export class ListenersService {
* @param label
* @param aliveOnly
* @param process
+ * @param all
+ * @param datasetId
* @returns Paged Successful Response
* @throws ApiError
*/
@@ -50,6 +53,8 @@ export class ListenersService {
label?: string,
aliveOnly: boolean = false,
process?: string,
+ all: boolean = false,
+ datasetId?: string,
): CancelablePromise {
return __request({
method: 'GET',
@@ -62,6 +67,8 @@ export class ListenersService {
'label': label,
'alive_only': aliveOnly,
'process': process,
+ 'all': all,
+ 'dataset_id': datasetId,
},
errors: {
422: `Validation Error`,
@@ -103,6 +110,7 @@ export class ListenersService {
* @param limit
* @param heartbeatInterval
* @param process
+ * @param datasetId
* @returns Paged Successful Response
* @throws ApiError
*/
@@ -112,6 +120,7 @@ export class ListenersService {
limit: number = 2,
heartbeatInterval: number = 300,
process?: string,
+ datasetId?: string,
): CancelablePromise {
return __request({
method: 'GET',
@@ -122,6 +131,7 @@ export class ListenersService {
'limit': limit,
'heartbeat_interval': heartbeatInterval,
'process': process,
+ 'dataset_id': datasetId,
},
errors: {
422: `Validation Error`,
@@ -159,15 +169,20 @@ export class ListenersService {
* Get Listener
* Return JSON information about an Event Listener if it exists.
* @param listenerId
+ * @param datasetId
* @returns EventListenerOut Successful Response
* @throws ApiError
*/
public static getListenerApiV2ListenersListenerIdGet(
listenerId: string,
+ datasetId?: string,
): CancelablePromise {
return __request({
method: 'GET',
path: `/api/v2/listeners/${listenerId}`,
+ query: {
+ 'dataset_id': datasetId,
+ },
errors: {
422: `Validation Error`,
},
@@ -183,16 +198,21 @@ export class ListenersService {
* listener_in -- JSON object including updated information
* @param listenerId
* @param requestBody
+ * @param datasetId
* @returns EventListenerOut Successful Response
* @throws ApiError
*/
public static editListenerApiV2ListenersListenerIdPut(
listenerId: string,
requestBody: EventListenerIn,
+ datasetId?: string,
): CancelablePromise {
return __request({
method: 'PUT',
path: `/api/v2/listeners/${listenerId}`,
+ query: {
+ 'dataset_id': datasetId,
+ },
body: requestBody,
mediaType: 'application/json',
errors: {
@@ -205,15 +225,20 @@ export class ListenersService {
* Delete Listener
* Remove an Event Listener from the database. Will not clear event history for the listener.
* @param listenerId
+ * @param datasetId
* @returns any Successful Response
* @throws ApiError
*/
public static deleteListenerApiV2ListenersListenerIdDelete(
listenerId: string,
+ datasetId?: string,
): CancelablePromise {
return __request({
method: 'DELETE',
path: `/api/v2/listeners/${listenerId}`,
+ query: {
+ 'dataset_id': datasetId,
+ },
errors: {
422: `Validation Error`,
},
@@ -225,18 +250,75 @@ export class ListenersService {
* Return JSON information about an Event Listener if it exists.
* @param listenerId
* @param heartbeatInterval
+ * @param datasetId
* @returns boolean Successful Response
* @throws ApiError
*/
public static checkListenerLivelihoodApiV2ListenersListenerIdStatusGet(
listenerId: string,
heartbeatInterval: number = 300,
+ datasetId?: string,
): CancelablePromise {
return __request({
method: 'GET',
path: `/api/v2/listeners/${listenerId}/status`,
query: {
'heartbeat_interval': heartbeatInterval,
+ 'dataset_id': datasetId,
+ },
+ errors: {
+ 422: `Validation Error`,
+ },
+ });
+ }
+
+ /**
+ * Enable Listener
+ * Enable an Event Listener. Only admins can enable listeners.
+ *
+ * Arguments:
+ * listener_id -- UUID of the listener to be enabled
+ * @param listenerId
+ * @param datasetId
+ * @returns EventListenerOut Successful Response
+ * @throws ApiError
+ */
+ public static enableListenerApiV2ListenersListenerIdEnablePut(
+ listenerId: string,
+ datasetId?: string,
+ ): CancelablePromise {
+ return __request({
+ method: 'PUT',
+ path: `/api/v2/listeners/${listenerId}/enable`,
+ query: {
+ 'dataset_id': datasetId,
+ },
+ errors: {
+ 422: `Validation Error`,
+ },
+ });
+ }
+
+ /**
+ * Disable Listener
+ * Disable an Event Listener. Only admins can enable listeners.
+ *
+ * Arguments:
+ * listener_id -- UUID of the listener to be enabled
+ * @param listenerId
+ * @param datasetId
+ * @returns EventListenerOut Successful Response
+ * @throws ApiError
+ */
+ public static disableListenerApiV2ListenersListenerIdDisablePut(
+ listenerId: string,
+ datasetId?: string,
+ ): CancelablePromise {
+ return __request({
+ method: 'PUT',
+ path: `/api/v2/listeners/${listenerId}/disable`,
+ query: {
+ 'dataset_id': datasetId,
},
errors: {
422: `Validation Error`,
diff --git a/frontend/src/reducers/listeners.ts b/frontend/src/reducers/listeners.ts
index 935288b58..bd51e6fac 100644
--- a/frontend/src/reducers/listeners.ts
+++ b/frontend/src/reducers/listeners.ts
@@ -5,6 +5,7 @@ import {
RECEIVE_LISTENER_JOBS,
RECEIVE_LISTENER_LABELS,
RECEIVE_LISTENERS,
+ TOGGLE_ACTIVE_FLAG_LISTENER,
RESET_JOB_SUMMARY,
RESET_JOB_UPDATES,
SEARCH_LISTENERS,
@@ -29,6 +30,25 @@ const listeners = (state = defaultState, action: DataAction) => {
switch (action.type) {
case RECEIVE_LISTENERS:
return Object.assign({}, state, { listeners: action.listeners });
+ case TOGGLE_ACTIVE_FLAG_LISTENER:
+ // @ts-ignore
+ // eslint-disable-next-line no-case-declarations
+ const updatedListeners = state.listeners.data.map((listener) => {
+ // Check if the current listener matches the one being toggled
+ if (listener.id === action.listener.id) {
+ // Toggle the active flag of the matched item
+ action.listener.alive = listener.alive;
+ return action.listener;
+ } else return listener;
+ });
+ return Object.assign({}, state, {
+ listeners: {
+ metadata: state.listeners.metadata,
+ data: updatedListeners,
+ },
+ });
+
+ //return { ...state, listeners: updatedListeners };
case SEARCH_LISTENERS:
return Object.assign({}, state, { listeners: action.listeners });
case RECEIVE_LISTENER_CATEGORIES:
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index 0e2cca00b..b709d770b 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -41,6 +41,7 @@ import { ManageUsers } from "./components/users/ManageUsers";
import config from "./app.config";
import { MetadataDefinitions } from "./components/metadata/MetadataDefinitions";
import { MetadataDefinitionEntry } from "./components/metadata/MetadataDefinitionEntry";
+import { AllListeners } from "./components/listeners/AllListeners";
// https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5
const PrivateRoute = (props): JSX.Element => {
@@ -231,6 +232,14 @@ export const AppRoutes = (): JSX.Element => {
}
/>
+
+
+
+ }
+ />