From eb596e2cb61c67fa02f5d16dbf641cb4e9dbafdb Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Tue, 23 Apr 2024 19:58:39 -0500 Subject: [PATCH 01/10] draft --- backend/app/models/listeners.py | 1 + backend/app/routers/listeners.py | 53 +++ frontend/src/actions/listeners.js | 20 + frontend/src/components/Layout.tsx | 12 + .../src/components/listeners/AllListeners.tsx | 353 ++++++++++++++++++ .../src/components/listeners/ListenerItem.tsx | 6 +- .../src/components/listeners/Listeners.tsx | 1 + .../src/openapi/v2/models/EventListenerOut.ts | 1 + .../openapi/v2/services/ListenersService.ts | 74 ++++ frontend/src/reducers/listeners.ts | 17 + frontend/src/routes.tsx | 10 + frontend/src/types/action.ts | 7 + 12 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/listeners/AllListeners.tsx diff --git a/backend/app/models/listeners.py b/backend/app/models/listeners.py index 2d855c6ff..f86bb05b5 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: Optional[bool] = None properties: Optional[ExtractorInfo] = None class Settings: diff --git a/backend/app/routers/listeners.py b/backend/app/routers/listeners.py index 3febfe633..7ef4aab76 100644 --- a/backend/app/routers/listeners.py +++ b/backend/app/routers/listeners.py @@ -25,6 +25,8 @@ from fastapi import APIRouter, Depends, HTTPException from packaging import version +from backend.app.routers.authentication import get_admin_mode, get_admin + router = APIRouter() legacy_router = APIRouter() # for back-compatibilty with v1 extractors @@ -373,6 +375,57 @@ async def edit_listener( raise HTTPException(status_code=500, detail=e.args[0]) 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), +): + """Enable an Event Listener. Only admins can enable listeners. + + Arguments: + listener_id -- UUID of the listener to be enabled + """ + return set_active_flag(listener_id, True) + + +@router.put("/{listener_id}/disable", response_model=EventListenerOut) +async def enable_listener( + listener_id: str, + user_id=Depends(get_user), +): + """Enable an Event Listener. Only admins can enable listeners. + + Arguments: + listener_id -- UUID of the listener to be enabled + """ + return set_active_flag(listener_id, False) + +@router.put("/{listener_id}/toggle", response_model=EventListenerOut) +async def set_active_flag( + listener_id: str, + active: bool, + admin=Depends(get_admin), + admin_mode: bool = Depends(get_admin_mode) +): + """Enable an Event Listener. Only admins can enable listeners. + + Arguments: + listener_id -- UUID of the listener to be enabled + """ + if not(admin and admin_mode): + raise HTTPException(status_code=403, detail=f"Only admins can enable/disable listeners") + 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( diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js index 41d24f42e..d704a7179 100644 --- a/frontend/src/actions/listeners.js +++ b/frontend/src/actions/listeners.js @@ -49,6 +49,26 @@ export function fetchListeners( }; } +export const TOGGLE_ACTIVE_FLAG_LISTENER = "TOGGLE_ACTIVE_FLAG_LISTENER"; +export function toggleActiveFlagListener(id, value) { + return (dispatch) => { + return V2.ListenersService.setActiveFlagApiV2ListenersListenerIdTogglePut(id, value).then((json) => { + // dispatch({ + // type: TOGGLE_ACTIVE_FLAG_LISTENER, + // listener: json, + // }); + dispatch(fetchListeners()); + }).catch((reason) => { + dispatch( + handleErrors( + reason, + toggleActiveFlagListener(id, value) + ) + ); + }); + } +} + 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..dbc04b51f 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..09e6bd708 --- /dev/null +++ b/frontend/src/components/listeners/AllListeners.tsx @@ -0,0 +1,353 @@ +import React, { ChangeEvent, useEffect, useState } from "react"; +import { + Box, Checkbox, + Divider, + FormControl, + FormControlLabel, + Grid, Input, + InputLabel, + List, + MenuItem, + Pagination, + Paper, + Select, + Switch, +} from "@mui/material"; + +import { RootState } from "../../types/data"; +import { useDispatch, useSelector } from "react-redux"; +import { + fetchListenerCategories, + fetchListenerLabels, + fetchListeners, + queryListeners, toggleActiveFlagListener, +} 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"; + +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 + ) + ); + 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 toggleActiveFlag = (id: string, active: boolean) => + dispatch(toggleActiveFlagListener(id, active)); + + 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 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(); + }, []); + + // 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]); + + useEffect(() => { + // reset page and reset search text with each new search term + setCurrPageNum(1); + setSearchText(""); + listListeners( + 0, + limit, + 0, + selectedCategory, + selectedLabel, + aliveOnly, + process + ); + }, [aliveOnly]); + + // 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, + ]); + + 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); + }; + + const setActiveListeners = (event: React.ChangeEvent) => { + console.log("setting the active flag for", event.target.id, " : ", event.target.value) + toggleActiveFlag(event.target.id, event.target.value) + }; + + return ( + +
+ + + {/*searchbox*/} + + + + + {/*categories*/} + + + Filter by category + + + + + + Filter by labels + + + + + { + setAliveOnly(!aliveOnly); + }} + /> + } + label="Alive Extractors" + /> + + + + + + + {listeners !== undefined ? ( + listeners.map((listener) => { + return ( +
+ + + +
+ ); + }) + ) : ( + <> + )} +
+ + + +
+
+
+ +
+
+); +} diff --git a/frontend/src/components/listeners/ListenerItem.tsx b/frontend/src/components/listeners/ListenerItem.tsx index b149ded1a..d0bfc2f09 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,7 +93,8 @@ export default function ListenerItem(props: ListenerCardProps) { margin: "auto", }} > - + )} ); diff --git a/frontend/src/components/listeners/Listeners.tsx b/frontend/src/components/listeners/Listeners.tsx index 4556bb40d..ab240e604 100644 --- a/frontend/src/components/listeners/Listeners.tsx +++ b/frontend/src/components/listeners/Listeners.tsx @@ -305,6 +305,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/ListenersService.ts b/frontend/src/openapi/v2/services/ListenersService.ts index d0fa80add..d317b8242 100644 --- a/frontend/src/openapi/v2/services/ListenersService.ts +++ b/frontend/src/openapi/v2/services/ListenersService.ts @@ -244,4 +244,78 @@ export class ListenersService { }); } + /** + * Enable Listener + * Enable an Event Listener. Only admins can enable listeners. + * + * Arguments: + * listener_id -- UUID of the listener to be enabled + * @param listenerId + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static enableListenerApiV2ListenersListenerIdEnablePut( + listenerId: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/enable`, + 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 + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static enableListenerApiV2ListenersListenerIdDisablePut( + listenerId: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/disable`, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Set Active Flag + * Enable an Event Listener. Only admins can enable listeners. + * + * Arguments: + * listener_id -- UUID of the listener to be enabled + * @param listenerId + * @param active + * @param datasetId + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static setActiveFlagApiV2ListenersListenerIdTogglePut( + listenerId: string, + active: boolean, + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/toggle`, + query: { + 'active': active, + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + } \ No newline at end of file diff --git a/frontend/src/reducers/listeners.ts b/frontend/src/reducers/listeners.ts index 935288b58..dfae5e481 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,22 @@ const listeners = (state = defaultState, action: DataAction) => { switch (action.type) { case RECEIVE_LISTENERS: return Object.assign({}, state, { listeners: action.listeners }); + case TOGGLE_ACTIVE_FLAG_LISTENER: + console.log(action.listener.id); + console.log(action.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 + console.log("here"); + return action.listener; + } + }); + return Object.assign({}, state, { listeners: 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..43912f5c3 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -41,6 +41,8 @@ import { ManageUsers } from "./components/users/ManageUsers"; import config from "./app.config"; import { MetadataDefinitions } from "./components/metadata/MetadataDefinitions"; import { MetadataDefinitionEntry } from "./components/metadata/MetadataDefinitionEntry"; +import {Listeners} from "./components/listeners/Listeners"; +import {AllListeners} from "./components/listeners/AllListeners"; // https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5 const PrivateRoute = (props): JSX.Element => { @@ -231,6 +233,14 @@ export const AppRoutes = (): JSX.Element => { } /> + + + + } + /> Date: Wed, 24 Apr 2024 10:36:55 -0500 Subject: [PATCH 02/10] removing overhead of calling fetchListeners --- backend/app/routers/listeners.py | 14 ++-- frontend/src/actions/listeners.js | 29 ++++---- .../src/components/listeners/AllListeners.tsx | 48 +++++++----- .../src/components/listeners/ListenerItem.tsx | 28 +++---- .../src/openapi/v2/models/EventListenerOut.ts | 1 - .../openapi/v2/services/ListenersService.ts | 74 ------------------- frontend/src/reducers/listeners.ts | 22 +++--- frontend/src/routes.tsx | 4 +- frontend/src/types/action.ts | 1 - 9 files changed, 81 insertions(+), 140 deletions(-) diff --git a/backend/app/routers/listeners.py b/backend/app/routers/listeners.py index 7ef4aab76..194d97919 100644 --- a/backend/app/routers/listeners.py +++ b/backend/app/routers/listeners.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, Depends, HTTPException from packaging import version -from backend.app.routers.authentication import get_admin_mode, get_admin +from backend.app.routers.authentication import get_admin, get_admin_mode router = APIRouter() legacy_router = APIRouter() # for back-compatibilty with v1 extractors @@ -375,6 +375,7 @@ async def edit_listener( raise HTTPException(status_code=500, detail=e.args[0]) 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, @@ -389,7 +390,7 @@ async def enable_listener( @router.put("/{listener_id}/disable", response_model=EventListenerOut) -async def enable_listener( +async def disable_listener( listener_id: str, user_id=Depends(get_user), ): @@ -400,20 +401,23 @@ async def enable_listener( """ return set_active_flag(listener_id, False) + @router.put("/{listener_id}/toggle", response_model=EventListenerOut) async def set_active_flag( listener_id: str, active: bool, admin=Depends(get_admin), - admin_mode: bool = Depends(get_admin_mode) + admin_mode: bool = Depends(get_admin_mode), ): """Enable an Event Listener. Only admins can enable listeners. Arguments: listener_id -- UUID of the listener to be enabled """ - if not(admin and admin_mode): - raise HTTPException(status_code=403, detail=f"Only admins can enable/disable listeners") + if not (admin and admin_mode): + raise HTTPException( + status_code=403, detail="Only admins can enable/disable listeners" + ) listener = await EventListenerDB.find_one( EventListenerDB.id == ObjectId(listener_id) ) diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js index d704a7179..8695dde30 100644 --- a/frontend/src/actions/listeners.js +++ b/frontend/src/actions/listeners.js @@ -52,21 +52,22 @@ export function fetchListeners( export const TOGGLE_ACTIVE_FLAG_LISTENER = "TOGGLE_ACTIVE_FLAG_LISTENER"; export function toggleActiveFlagListener(id, value) { return (dispatch) => { - return V2.ListenersService.setActiveFlagApiV2ListenersListenerIdTogglePut(id, value).then((json) => { - // dispatch({ - // type: TOGGLE_ACTIVE_FLAG_LISTENER, - // listener: json, - // }); - dispatch(fetchListeners()); - }).catch((reason) => { - dispatch( - handleErrors( - reason, - toggleActiveFlagListener(id, value) - ) - ); + return V2.ListenersService.setActiveFlagApiV2ListenersListenerIdTogglePut( + id, + value + ) + .then((json) => { + // We could have called fetchListeners but it would be an overhead since we are just toggling the active flag. + // 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, toggleActiveFlagListener(id, value))); }); - } + }; } export const SEARCH_LISTENERS = "SEARCH_LISTENERS"; diff --git a/frontend/src/components/listeners/AllListeners.tsx b/frontend/src/components/listeners/AllListeners.tsx index 09e6bd708..b4f4264b4 100644 --- a/frontend/src/components/listeners/AllListeners.tsx +++ b/frontend/src/components/listeners/AllListeners.tsx @@ -1,10 +1,12 @@ import React, { ChangeEvent, useEffect, useState } from "react"; import { - Box, Checkbox, + Box, + Checkbox, Divider, FormControl, FormControlLabel, - Grid, Input, + Grid, + Input, InputLabel, List, MenuItem, @@ -20,7 +22,8 @@ import { fetchListenerCategories, fetchListenerLabels, fetchListeners, - queryListeners, toggleActiveFlagListener, + queryListeners, + toggleActiveFlagListener, } from "../../actions/listeners"; import ListenerItem from "./ListenerItem"; import SubmitExtraction from "./SubmitExtraction"; @@ -34,7 +37,7 @@ type ListenerProps = { }; export function AllListeners(props: ListenerProps) { - const {process } = props; + const { process } = props; // Redux connect equivalent const dispatch = useDispatch(); const listListeners = ( @@ -212,8 +215,13 @@ export function AllListeners(props: ListenerProps) { }; const setActiveListeners = (event: React.ChangeEvent) => { - console.log("setting the active flag for", event.target.id, " : ", event.target.value) - toggleActiveFlag(event.target.id, event.target.value) + console.log( + "setting the active flag for", + event.target.id, + " : ", + event.target.value + ); + toggleActiveFlag(event.target.id, event.target.value); }; return ( @@ -235,12 +243,12 @@ export function AllListeners(props: ListenerProps) { {/*categories*/} - + Filter by category - + {listeners !== undefined ? ( listeners.map((listener) => { return ( -
- + - +
); }) @@ -327,7 +337,7 @@ export function AllListeners(props: ListenerProps) { <> )}
- + -); + ); } diff --git a/frontend/src/components/listeners/ListenerItem.tsx b/frontend/src/components/listeners/ListenerItem.tsx index d0bfc2f09..473e8419d 100644 --- a/frontend/src/components/listeners/ListenerItem.tsx +++ b/frontend/src/components/listeners/ListenerItem.tsx @@ -29,7 +29,7 @@ export default function ListenerItem(props: ListenerCardProps) { setOpenSubmitExtraction, setInfoOnly, setSelectedExtractor, - showSubmit + showSubmit, } = props; return ( @@ -95,19 +95,19 @@ export default function ListenerItem(props: ListenerCardProps) { > {showSubmit && ( { - setOpenSubmitExtraction(true); - setSelectedExtractor(extractor); - setInfoOnly(false); - }} - > - - + color="primary" + disabled={ + !(fileId !== undefined || datasetId !== undefined) || + !extractor["alive"] + } + onClick={() => { + setOpenSubmitExtraction(true); + setSelectedExtractor(extractor); + setInfoOnly(false); + }} + > + + )} diff --git a/frontend/src/openapi/v2/models/EventListenerOut.ts b/frontend/src/openapi/v2/models/EventListenerOut.ts index 0e7581766..a6a57f20f 100644 --- a/frontend/src/openapi/v2/models/EventListenerOut.ts +++ b/frontend/src/openapi/v2/models/EventListenerOut.ts @@ -18,6 +18,5 @@ export type EventListenerOut = { modified?: string; lastAlive?: string; alive?: boolean; - active?: boolean; properties?: ExtractorInfo; } diff --git a/frontend/src/openapi/v2/services/ListenersService.ts b/frontend/src/openapi/v2/services/ListenersService.ts index d317b8242..d0fa80add 100644 --- a/frontend/src/openapi/v2/services/ListenersService.ts +++ b/frontend/src/openapi/v2/services/ListenersService.ts @@ -244,78 +244,4 @@ export class ListenersService { }); } - /** - * Enable Listener - * Enable an Event Listener. Only admins can enable listeners. - * - * Arguments: - * listener_id -- UUID of the listener to be enabled - * @param listenerId - * @returns EventListenerOut Successful Response - * @throws ApiError - */ - public static enableListenerApiV2ListenersListenerIdEnablePut( - listenerId: string, - ): CancelablePromise { - return __request({ - method: 'PUT', - path: `/api/v2/listeners/${listenerId}/enable`, - 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 - * @returns EventListenerOut Successful Response - * @throws ApiError - */ - public static enableListenerApiV2ListenersListenerIdDisablePut( - listenerId: string, - ): CancelablePromise { - return __request({ - method: 'PUT', - path: `/api/v2/listeners/${listenerId}/disable`, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Set Active Flag - * Enable an Event Listener. Only admins can enable listeners. - * - * Arguments: - * listener_id -- UUID of the listener to be enabled - * @param listenerId - * @param active - * @param datasetId - * @returns EventListenerOut Successful Response - * @throws ApiError - */ - public static setActiveFlagApiV2ListenersListenerIdTogglePut( - listenerId: string, - active: boolean, - datasetId?: string, - ): CancelablePromise { - return __request({ - method: 'PUT', - path: `/api/v2/listeners/${listenerId}/toggle`, - query: { - 'active': active, - 'dataset_id': datasetId, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - } \ No newline at end of file diff --git a/frontend/src/reducers/listeners.ts b/frontend/src/reducers/listeners.ts index dfae5e481..9c45592b3 100644 --- a/frontend/src/reducers/listeners.ts +++ b/frontend/src/reducers/listeners.ts @@ -31,21 +31,23 @@ const listeners = (state = defaultState, action: DataAction) => { case RECEIVE_LISTENERS: return Object.assign({}, state, { listeners: action.listeners }); case TOGGLE_ACTIVE_FLAG_LISTENER: - console.log(action.listener.id); - console.log(action.listener); // @ts-ignore // eslint-disable-next-line no-case-declarations - const updatedListeners = state.listeners.data.map(listener => { + 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 - console.log("here"); - return action.listener; - } - }); - return Object.assign({}, state, { listeners: updatedListeners }); + // Toggle the active flag of the matched item + return action.listener; + } else return listener; + }); + return Object.assign({}, state, { + listeners: { + metadata: state.listeners.metadata, + data: updatedListeners, + }, + }); - //return { ...state, listeners: 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 43912f5c3..a505d708d 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -41,8 +41,8 @@ import { ManageUsers } from "./components/users/ManageUsers"; import config from "./app.config"; import { MetadataDefinitions } from "./components/metadata/MetadataDefinitions"; import { MetadataDefinitionEntry } from "./components/metadata/MetadataDefinitionEntry"; -import {Listeners} from "./components/listeners/Listeners"; -import {AllListeners} from "./components/listeners/AllListeners"; +import { Listeners } from "./components/listeners/Listeners"; +import { AllListeners } from "./components/listeners/AllListeners"; // https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5 const PrivateRoute = (props): JSX.Element => { diff --git a/frontend/src/types/action.ts b/frontend/src/types/action.ts index b3fe63c69..279e0bab5 100644 --- a/frontend/src/types/action.ts +++ b/frontend/src/types/action.ts @@ -440,7 +440,6 @@ interface TOGGLE_ACTIVE_FLAG_LISTENER { listener: LicenseOut; } - interface SEARCH_LISTENERS { type: "SEARCH_LISTENERS"; listeners: []; From 439fcfb0c517dffbf5e9be3bd3af32cdeb3c3140 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Tue, 30 Apr 2024 11:14:40 -0500 Subject: [PATCH 03/10] fixing alive status on toggling active flag on backend --- backend/app/routers/listeners.py | 7 +- frontend/src/actions/listeners.js | 10 +-- .../src/components/listeners/AllListeners.tsx | 25 +++---- .../src/openapi/v2/models/EventListenerOut.ts | 1 + .../openapi/v2/services/ListenersService.ts | 74 +++++++++++++++++++ frontend/src/reducers/listeners.ts | 1 + frontend/src/types/action.ts | 11 ++- 7 files changed, 107 insertions(+), 22 deletions(-) diff --git a/backend/app/routers/listeners.py b/backend/app/routers/listeners.py index 194d97919..aad0e134b 100644 --- a/backend/app/routers/listeners.py +++ b/backend/app/routers/listeners.py @@ -405,7 +405,7 @@ async def disable_listener( @router.put("/{listener_id}/toggle", response_model=EventListenerOut) async def set_active_flag( listener_id: str, - active: bool, + active: Optional[bool] = None, admin=Depends(get_admin), admin_mode: bool = Depends(get_admin_mode), ): @@ -423,7 +423,10 @@ async def set_active_flag( ) if listener: try: - listener.active = active + if active is not None: + listener.active = active + else: + listener.active = not listener.active await listener.save() return listener.dict() except Exception as e: diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js index 8695dde30..e813de9ba 100644 --- a/frontend/src/actions/listeners.js +++ b/frontend/src/actions/listeners.js @@ -50,22 +50,22 @@ export function fetchListeners( } export const TOGGLE_ACTIVE_FLAG_LISTENER = "TOGGLE_ACTIVE_FLAG_LISTENER"; -export function toggleActiveFlagListener(id, value) { +export function toggleActiveFlagListener(id) { return (dispatch) => { return V2.ListenersService.setActiveFlagApiV2ListenersListenerIdTogglePut( - id, - value + id ) .then((json) => { - // We could have called fetchListeners but it would be an overhead since we are just toggling the active flag. + // 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, }); + //dispatch(fetchListeners()); }) .catch((reason) => { - dispatch(handleErrors(reason, toggleActiveFlagListener(id, value))); + dispatch(handleErrors(reason, toggleActiveFlagListener(id))); }); }; } diff --git a/frontend/src/components/listeners/AllListeners.tsx b/frontend/src/components/listeners/AllListeners.tsx index b4f4264b4..5729cf33f 100644 --- a/frontend/src/components/listeners/AllListeners.tsx +++ b/frontend/src/components/listeners/AllListeners.tsx @@ -31,6 +31,7 @@ import { capitalize } from "../../utils/common"; import config from "../../app.config"; import { GenericSearchBox } from "../search/GenericSearchBox"; import Layout from "../Layout"; +import { toBoolean } from "vega"; type ListenerProps = { process?: string; @@ -69,8 +70,8 @@ export function AllListeners(props: ListenerProps) { ) => dispatch(queryListeners(text, skip, limit, heartbeatInterval, process)); const listAvailableCategories = () => dispatch(fetchListenerCategories()); const listAvailableLabels = () => dispatch(fetchListenerLabels()); - const toggleActiveFlag = (id: string, active: boolean) => - dispatch(toggleActiveFlagListener(id, active)); + const toggleActiveFlag = (id: string) => + dispatch(toggleActiveFlagListener(id)); const listeners = useSelector( (state: RootState) => state.listener.listeners.data @@ -214,14 +215,9 @@ export function AllListeners(props: ListenerProps) { setOpenSubmitExtraction(false); }; - const setActiveListeners = (event: React.ChangeEvent) => { - console.log( - "setting the active flag for", - event.target.id, - " : ", - event.target.value - ); - toggleActiveFlag(event.target.id, event.target.value); + const setActiveListeners = (id: string) => { + console.log("setting the active flag for", id); + toggleActiveFlag(id); }; return ( @@ -308,13 +304,16 @@ export function AllListeners(props: ListenerProps) { {listeners !== undefined ? ( listeners.map((listener) => { return ( -
+
{ + setActiveListeners(listener.id); + }} /> { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/enable`, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Disable Listener + * Enable an Event Listener. Only admins can enable listeners. + * + * Arguments: + * listener_id -- UUID of the listener to be enabled + * @param listenerId + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static disableListenerApiV2ListenersListenerIdDisablePut( + listenerId: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/disable`, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Set Active Flag + * Enable an Event Listener. Only admins can enable listeners. + * + * Arguments: + * listener_id -- UUID of the listener to be enabled + * @param listenerId + * @param active + * @param datasetId + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static setActiveFlagApiV2ListenersListenerIdTogglePut( + listenerId: string, + active?: boolean, + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/toggle`, + query: { + 'active': active, + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + } \ No newline at end of file diff --git a/frontend/src/reducers/listeners.ts b/frontend/src/reducers/listeners.ts index 9c45592b3..bd51e6fac 100644 --- a/frontend/src/reducers/listeners.ts +++ b/frontend/src/reducers/listeners.ts @@ -37,6 +37,7 @@ const listeners = (state = defaultState, action: DataAction) => { // 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; }); diff --git a/frontend/src/types/action.ts b/frontend/src/types/action.ts index 279e0bab5..e1cd87b8a 100644 --- a/frontend/src/types/action.ts +++ b/frontend/src/types/action.ts @@ -1,10 +1,17 @@ -import { ExtractedMetadata, FilePreview, Folder, MetadataJsonld } from "./data"; +import { + ExtractedMetadata, + FilePreview, + Folder, + ListenerState, + MetadataJsonld, +} from "./data"; import { AuthorizationBase, DatasetOut as Dataset, DatasetRoles, EventListenerJobOut, EventListenerJobUpdateOut, + EventListenerOut, FileOut, FileOut as FileSummary, FileVersion, @@ -437,7 +444,7 @@ interface RECEIVE_LISTENERS { interface TOGGLE_ACTIVE_FLAG_LISTENER { type: "TOGGLE_ACTIVE_FLAG_LISTENER"; - listener: LicenseOut; + listener: EventListenerOut; } interface SEARCH_LISTENERS { From e8f2a94bf00d9203e1145e5aa3f4c81a68dda66d Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 2 May 2024 18:02:13 -0500 Subject: [PATCH 04/10] Adding active check on listener endpoints and adding test cases --- backend/app/routers/listeners.py | 15 +++- backend/app/tests/test_extractors.py | 20 +++++ frontend/src/actions/listeners.js | 1 - frontend/src/components/Layout.tsx | 2 +- .../src/components/listeners/AllListeners.tsx | 31 +++++--- .../src/components/listeners/Listeners.tsx | 8 +- .../src/openapi/v2/models/EventListenerOut.ts | 1 - .../openapi/v2/services/ListenersService.ts | 74 ------------------- 8 files changed, 56 insertions(+), 96 deletions(-) diff --git a/backend/app/routers/listeners.py b/backend/app/routers/listeners.py index aad0e134b..531cf31a3 100644 --- a/backend/app/routers/listeners.py +++ b/backend/app/routers/listeners.py @@ -18,6 +18,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 @@ -25,8 +26,6 @@ from fastapi import APIRouter, Depends, HTTPException from packaging import version -from backend.app.routers.authentication import get_admin, get_admin_mode - router = APIRouter() legacy_router = APIRouter() # for back-compatibilty with v1 extractors @@ -200,6 +199,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. @@ -223,6 +224,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) @@ -295,6 +298,8 @@ async def get_listeners( label: Optional[str] = None, alive_only: Optional[bool] = False, process: Optional[str] = None, + admin=Depends(get_admin), + admin_mode=Depends(get_admin_mode), ): """Get a list of all Event Listeners in the db. @@ -327,6 +332,8 @@ async def get_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) @@ -386,7 +393,7 @@ async def enable_listener( Arguments: listener_id -- UUID of the listener to be enabled """ - return set_active_flag(listener_id, True) + return await set_active_flag(listener_id, True) @router.put("/{listener_id}/disable", response_model=EventListenerOut) @@ -399,7 +406,7 @@ async def disable_listener( Arguments: listener_id -- UUID of the listener to be enabled """ - return set_active_flag(listener_id, False) + return await set_active_flag(listener_id, False) @router.put("/{listener_id}/toggle", response_model=EventListenerOut) diff --git a/backend/app/tests/test_extractors.py b/backend/app/tests/test_extractors.py index c3bcb72e8..ca32efeef 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"] == 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"] == False diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js index e813de9ba..49293e045 100644 --- a/frontend/src/actions/listeners.js +++ b/frontend/src/actions/listeners.js @@ -62,7 +62,6 @@ export function toggleActiveFlagListener(id) { type: TOGGLE_ACTIVE_FLAG_LISTENER, listener: json, }); - //dispatch(fetchListeners()); }) .catch((reason) => { dispatch(handleErrors(reason, toggleActiveFlagListener(id))); diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index dbc04b51f..0a99302c9 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -427,7 +427,7 @@ export default function PersistentDrawerLeft(props) { - + diff --git a/frontend/src/components/listeners/AllListeners.tsx b/frontend/src/components/listeners/AllListeners.tsx index 5729cf33f..5ac9eb7f6 100644 --- a/frontend/src/components/listeners/AllListeners.tsx +++ b/frontend/src/components/listeners/AllListeners.tsx @@ -82,6 +82,8 @@ export function AllListeners(props: ListenerProps) { 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); @@ -100,7 +102,7 @@ export function AllListeners(props: ListenerProps) { listListeners(0, limit, 0, null, null, aliveOnly, process); listAvailableCategories(); listAvailableLabels(); - }, []); + }, [adminMode]); // search useEffect(() => { @@ -119,7 +121,7 @@ export function AllListeners(props: ListenerProps) { aliveOnly, process ); - }, [searchText]); + }, [searchText, adminMode]); useEffect(() => { // reset page and reset search text with each new search term @@ -134,7 +136,7 @@ export function AllListeners(props: ListenerProps) { aliveOnly, process ); - }, [aliveOnly]); + }, [aliveOnly, adminMode]); // any of the change triggers timer to fetch the extractor status useEffect(() => { @@ -165,6 +167,7 @@ export function AllListeners(props: ListenerProps) { selectedCategory, selectedLabel, aliveOnly, + adminMode, ]); const handleListenerSearch = () => { @@ -216,7 +219,6 @@ export function AllListeners(props: ListenerProps) { }; const setActiveListeners = (id: string) => { - console.log("setting the active flag for", id); toggleActiveFlag(id); }; @@ -307,14 +309,19 @@ export function AllListeners(props: ListenerProps) {
- { - setActiveListeners(listener.id); - }} - /> + {admin && adminMode ? ( + { + setActiveListeners(listener.id); + }} + /> + ) : ( + <> + )} 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 = () => { diff --git a/frontend/src/openapi/v2/models/EventListenerOut.ts b/frontend/src/openapi/v2/models/EventListenerOut.ts index 0e7581766..a6a57f20f 100644 --- a/frontend/src/openapi/v2/models/EventListenerOut.ts +++ b/frontend/src/openapi/v2/models/EventListenerOut.ts @@ -18,6 +18,5 @@ export type EventListenerOut = { modified?: string; lastAlive?: string; alive?: boolean; - active?: boolean; properties?: ExtractorInfo; } diff --git a/frontend/src/openapi/v2/services/ListenersService.ts b/frontend/src/openapi/v2/services/ListenersService.ts index 7cf20d168..d0fa80add 100644 --- a/frontend/src/openapi/v2/services/ListenersService.ts +++ b/frontend/src/openapi/v2/services/ListenersService.ts @@ -244,78 +244,4 @@ export class ListenersService { }); } - /** - * Enable Listener - * Enable an Event Listener. Only admins can enable listeners. - * - * Arguments: - * listener_id -- UUID of the listener to be enabled - * @param listenerId - * @returns EventListenerOut Successful Response - * @throws ApiError - */ - public static enableListenerApiV2ListenersListenerIdEnablePut( - listenerId: string, - ): CancelablePromise { - return __request({ - method: 'PUT', - path: `/api/v2/listeners/${listenerId}/enable`, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Disable Listener - * Enable an Event Listener. Only admins can enable listeners. - * - * Arguments: - * listener_id -- UUID of the listener to be enabled - * @param listenerId - * @returns EventListenerOut Successful Response - * @throws ApiError - */ - public static disableListenerApiV2ListenersListenerIdDisablePut( - listenerId: string, - ): CancelablePromise { - return __request({ - method: 'PUT', - path: `/api/v2/listeners/${listenerId}/disable`, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Set Active Flag - * Enable an Event Listener. Only admins can enable listeners. - * - * Arguments: - * listener_id -- UUID of the listener to be enabled - * @param listenerId - * @param active - * @param datasetId - * @returns EventListenerOut Successful Response - * @throws ApiError - */ - public static setActiveFlagApiV2ListenersListenerIdTogglePut( - listenerId: string, - active?: boolean, - datasetId?: string, - ): CancelablePromise { - return __request({ - method: 'PUT', - path: `/api/v2/listeners/${listenerId}/toggle`, - query: { - 'active': active, - 'dataset_id': datasetId, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - } \ No newline at end of file From b50b57dbd42f16b85047399f8bdd076ba8a11d97 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 2 May 2024 18:19:59 -0500 Subject: [PATCH 05/10] fixing codegen --- backend/app/tests/test_extractors.py | 4 +- .../src/openapi/v2/models/EventListenerOut.ts | 1 + .../openapi/v2/services/ListenersService.ts | 80 +++++++++++++++++++ frontend/src/routes.tsx | 1 - 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/backend/app/tests/test_extractors.py b/backend/app/tests/test_extractors.py index ca32efeef..ff27e4f3d 100644 --- a/backend/app/tests/test_extractors.py +++ b/backend/app/tests/test_extractors.py @@ -72,11 +72,11 @@ def test_enable_disable_extractor(client: TestClient, headers: dict): f"{settings.API_V2_STR}/listeners/{extractor_id}/enable", headers=headers ) assert response.status_code == 200 - assert response.json()["active"] == True + 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"] == False + assert response.json()["active"] is False 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/ListenersService.ts b/frontend/src/openapi/v2/services/ListenersService.ts index d0fa80add..2f5c4c97d 100644 --- a/frontend/src/openapi/v2/services/ListenersService.ts +++ b/frontend/src/openapi/v2/services/ListenersService.ts @@ -39,6 +39,7 @@ export class ListenersService { * @param label * @param aliveOnly * @param process + * @param datasetId * @returns Paged Successful Response * @throws ApiError */ @@ -50,6 +51,7 @@ export class ListenersService { label?: string, aliveOnly: boolean = false, process?: string, + datasetId?: string, ): CancelablePromise { return __request({ method: 'GET', @@ -62,6 +64,7 @@ export class ListenersService { 'label': label, 'alive_only': aliveOnly, 'process': process, + 'dataset_id': datasetId, }, errors: { 422: `Validation Error`, @@ -103,6 +106,7 @@ export class ListenersService { * @param limit * @param heartbeatInterval * @param process + * @param datasetId * @returns Paged Successful Response * @throws ApiError */ @@ -112,6 +116,7 @@ export class ListenersService { limit: number = 2, heartbeatInterval: number = 300, process?: string, + datasetId?: string, ): CancelablePromise { return __request({ method: 'GET', @@ -122,6 +127,7 @@ export class ListenersService { 'limit': limit, 'heartbeat_interval': heartbeatInterval, 'process': process, + 'dataset_id': datasetId, }, errors: { 422: `Validation Error`, @@ -244,4 +250,78 @@ export class ListenersService { }); } + /** + * Enable Listener + * Enable an Event Listener. Only admins can enable listeners. + * + * Arguments: + * listener_id -- UUID of the listener to be enabled + * @param listenerId + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static enableListenerApiV2ListenersListenerIdEnablePut( + listenerId: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/enable`, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Disable Listener + * Enable an Event Listener. Only admins can enable listeners. + * + * Arguments: + * listener_id -- UUID of the listener to be enabled + * @param listenerId + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static disableListenerApiV2ListenersListenerIdDisablePut( + listenerId: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/disable`, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Set Active Flag + * Enable an Event Listener. Only admins can enable listeners. + * + * Arguments: + * listener_id -- UUID of the listener to be enabled + * @param listenerId + * @param active + * @param datasetId + * @returns EventListenerOut Successful Response + * @throws ApiError + */ + public static setActiveFlagApiV2ListenersListenerIdTogglePut( + listenerId: string, + active?: boolean, + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'PUT', + path: `/api/v2/listeners/${listenerId}/toggle`, + query: { + 'active': active, + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + } \ No newline at end of file diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index a505d708d..b709d770b 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -41,7 +41,6 @@ import { ManageUsers } from "./components/users/ManageUsers"; import config from "./app.config"; import { MetadataDefinitions } from "./components/metadata/MetadataDefinitions"; import { MetadataDefinitionEntry } from "./components/metadata/MetadataDefinitionEntry"; -import { Listeners } from "./components/listeners/Listeners"; import { AllListeners } from "./components/listeners/AllListeners"; // https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5 From 1c1445dbd2f6aaa26472ac7198c113f19ff185c9 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 6 May 2024 15:36:08 -0500 Subject: [PATCH 06/10] added Listener Authorization as depends and added it as check for all routes --- backend/app/deps/authorization_deps.py | 38 +++++++++++++++++++ backend/app/routers/feeds.py | 30 ++++++++++++--- backend/app/routers/listeners.py | 20 ++++++---- .../src/openapi/v2/services/FeedsService.ts | 14 ++++++- .../openapi/v2/services/ListenersService.ts | 28 ++++++++++++++ 5 files changed, 115 insertions(+), 15 deletions(-) diff --git a/backend/app/deps/authorization_deps.py b/backend/app/deps/authorization_deps.py index af6104e01..53787e461 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,43 @@ 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/. + We want not admins users not to access listeners which are not active""" + + # 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/routers/feeds.py b/backend/app/routers/feeds.py index 42f9dc110..17fed37f6 100644 --- a/backend/app/routers/feeds.py +++ b/backend/app/routers/feeds.py @@ -6,11 +6,14 @@ 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 from pika.adapters.blocking_connection import BlockingChannel +from app.deps.authorization_deps import ListenerAuthorization + router = APIRouter() @@ -23,7 +26,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 +125,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 +136,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 531cf31a3..c0ba1e084 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 @@ -265,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)) @@ -279,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 ( @@ -332,6 +338,7 @@ async def get_listeners( aggregation_pipeline.append( {"$match": {"properties.process.dataset": {"$exists": True}}} ) + # Non admin users can access only active listeners if not admin or not admin_mode: aggregation_pipeline.append({"$match": {"active": True}}) # Add pagination @@ -360,6 +367,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.. @@ -387,6 +395,7 @@ async def edit_listener( 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. @@ -400,6 +409,7 @@ async def enable_listener( async def disable_listener( listener_id: str, user_id=Depends(get_user), + allow: bool = Depends(ListenerAuthorization()), ): """Enable an Event Listener. Only admins can enable listeners. @@ -413,18 +423,13 @@ async def disable_listener( async def set_active_flag( listener_id: str, active: Optional[bool] = None, - admin=Depends(get_admin), - admin_mode: bool = Depends(get_admin_mode), + allow: bool = Depends(ListenerAuthorization()), ): """Enable an Event Listener. Only admins can enable listeners. Arguments: listener_id -- UUID of the listener to be enabled """ - if not (admin and admin_mode): - raise HTTPException( - status_code=403, detail="Only admins can enable/disable listeners" - ) listener = await EventListenerDB.find_one( EventListenerDB.id == ObjectId(listener_id) ) @@ -445,6 +450,7 @@ async def set_active_flag( 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/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 2f5c4c97d..574253744 100644 --- a/frontend/src/openapi/v2/services/ListenersService.ts +++ b/frontend/src/openapi/v2/services/ListenersService.ts @@ -165,15 +165,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`, }, @@ -189,16 +194,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: { @@ -211,15 +221,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`, }, @@ -231,18 +246,21 @@ 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`, @@ -257,15 +275,20 @@ export class ListenersService { * 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`, }, @@ -279,15 +302,20 @@ export class ListenersService { * 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`, }, From 8f09d9d3cbdf69d446d13a6db774a898a426ea87 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Mon, 13 May 2024 15:42:12 -0500 Subject: [PATCH 07/10] addressed feedbacks --- backend/app/deps/authorization_deps.py | 6 ++--- backend/app/models/listeners.py | 2 +- backend/app/routers/feeds.py | 3 +-- backend/app/routers/listeners.py | 4 ++-- frontend/src/components/Layout.tsx | 24 +++++++++++-------- .../openapi/v2/services/ListenersService.ts | 4 ++-- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/backend/app/deps/authorization_deps.py b/backend/app/deps/authorization_deps.py index 53787e461..308406e4f 100644 --- a/backend/app/deps/authorization_deps.py +++ b/backend/app/deps/authorization_deps.py @@ -393,7 +393,7 @@ async def __call__( 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/. - We want not admins users not to access listeners which are not active""" + Regular users are not allowed to run non-active listeners""" # def __init__(self, optional_arg: str = None): # self.optional_arg = optional_arg @@ -422,9 +422,7 @@ async def __call__( 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" - ) + raise HTTPException(status_code=404, detail=f"Listener {listener_id} not found") class CheckStatus: diff --git a/backend/app/models/listeners.py b/backend/app/models/listeners.py index f86bb05b5..1bd33789b 100644 --- a/backend/app/models/listeners.py +++ b/backend/app/models/listeners.py @@ -69,7 +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: Optional[bool] = None + active: bool = False properties: Optional[ExtractorInfo] = None class Settings: diff --git a/backend/app/routers/feeds.py b/backend/app/routers/feeds.py index 17fed37f6..0500bc30a 100644 --- a/backend/app/routers/feeds.py +++ b/backend/app/routers/feeds.py @@ -1,5 +1,6 @@ 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 @@ -12,8 +13,6 @@ from fastapi import APIRouter, Depends, HTTPException from pika.adapters.blocking_connection import BlockingChannel -from app.deps.authorization_deps import ListenerAuthorization - router = APIRouter() diff --git a/backend/app/routers/listeners.py b/backend/app/routers/listeners.py index c0ba1e084..4fb9d1dfc 100644 --- a/backend/app/routers/listeners.py +++ b/backend/app/routers/listeners.py @@ -411,7 +411,7 @@ async def disable_listener( user_id=Depends(get_user), allow: bool = Depends(ListenerAuthorization()), ): - """Enable an Event Listener. Only admins can enable listeners. + """Disable an Event Listener. Only admins can enable listeners. Arguments: listener_id -- UUID of the listener to be enabled @@ -425,7 +425,7 @@ async def set_active_flag( active: Optional[bool] = None, allow: bool = Depends(ListenerAuthorization()), ): - """Enable an Event Listener. Only admins can enable listeners. + """Toggle the active flag of an Event Listener. Only admins can enable/disbale listeners. Arguments: listener_id -- UUID of the listener to be enabled diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 0a99302c9..2317b84e9 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -421,16 +421,20 @@ export default function PersistentDrawerLeft(props) { - - - - - - - - - - + {currUserProfile.admin && adminMode ? ( + + + + + + + + + + + ) : ( + <> + )}
diff --git a/frontend/src/openapi/v2/services/ListenersService.ts b/frontend/src/openapi/v2/services/ListenersService.ts index 574253744..86fda9258 100644 --- a/frontend/src/openapi/v2/services/ListenersService.ts +++ b/frontend/src/openapi/v2/services/ListenersService.ts @@ -297,7 +297,7 @@ export class ListenersService { /** * Disable Listener - * Enable an Event Listener. Only admins can enable listeners. + * Disable an Event Listener. Only admins can enable listeners. * * Arguments: * listener_id -- UUID of the listener to be enabled @@ -324,7 +324,7 @@ export class ListenersService { /** * Set Active Flag - * Enable an Event Listener. Only admins can enable listeners. + * Toggle the active flag of an Event Listener. Only admins can enable/disbale listeners. * * Arguments: * listener_id -- UUID of the listener to be enabled From 275ccc7cd340169212da4b906e371295e70e5930 Mon Sep 17 00:00:00 2001 From: Dipannita Dey Date: Thu, 16 May 2024 11:44:12 -0500 Subject: [PATCH 08/10] initial implementation of Feeds page --- backend/app/models/feeds.py | 4 +- backend/app/routers/feeds.py | 51 +++-- frontend/src/actions/listeners.js | 17 ++ frontend/src/app.config.ts | 2 + frontend/src/components/Layout.tsx | 12 +- frontend/src/components/listeners/Feeds.tsx | 190 ++++++++++++++++++ frontend/src/openapi/v2/models/FeedIn.ts | 1 + frontend/src/openapi/v2/models/FeedOut.ts | 1 + .../src/openapi/v2/services/FeedsService.ts | 14 +- frontend/src/reducers/feeds.ts | 19 ++ frontend/src/reducers/index.ts | 2 + frontend/src/routes.tsx | 9 + frontend/src/types/action.ts | 9 +- frontend/src/types/data.ts | 7 +- 14 files changed, 313 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/listeners/Feeds.tsx create mode 100644 frontend/src/reducers/feeds.ts diff --git a/backend/app/models/feeds.py b/backend/app/models/feeds.py index bc48d9bad..2e0a2b821 100644 --- a/backend/app/models/feeds.py +++ b/backend/app/models/feeds.py @@ -13,13 +13,11 @@ class JobFeed(BaseModel): resources match the saved search criteria for the Feed.""" name: str + description: str = "" search: SearchObject listeners: List[FeedListener] = [] -class FeedBase(JobFeed): - description: str = "" - class FeedIn(JobFeed): pass diff --git a/backend/app/routers/feeds.py b/backend/app/routers/feeds.py index 0500bc30a..a69b5681f 100644 --- a/backend/app/routers/feeds.py +++ b/backend/app/routers/feeds.py @@ -10,9 +10,18 @@ from app.routers.authentication import get_admin, get_admin_mode from app.search.connect import check_search_result from beanie import PydanticObjectId +from beanie.operators import Or, RegEx from fastapi import APIRouter, Depends, HTTPException from pika.adapters.blocking_connection import BlockingChannel +from app.deps.authorization_deps import ListenerAuthorization + +from app.models.pages import Paged + +from app.models.pages import _construct_page_metadata + +from app.models.pages import _get_page_query + router = APIRouter() @@ -71,28 +80,44 @@ async def save_feed( return feed.dict() -@router.get("", response_model=List[FeedOut]) +@router.get("", response_model=Paged) async def get_feeds( - name: Optional[str] = None, + searchTerm: Optional[str] = None, user=Depends(get_current_user), skip: int = 0, limit: int = 10, + admin=Depends(get_admin), + admin_mode=Depends(get_admin_mode) ): """Fetch all existing Feeds.""" - if name is not None: - feeds = ( - await FeedDB.find(FeedDB.name == name) - .sort(-FeedDB.created) - .skip(skip) - .limit(limit) + criteria_list = [] + # if not admin or not admin_mode: + # criteria_list.append(FeedDB.creator == user) + if searchTerm is not None: + criteria_list.append( + Or( + RegEx(field=FeedDB.name, pattern=searchTerm), + RegEx(field=FeedDB.description, pattern=searchTerm), + )) + + feeds_and_count = ( + await FeedDB.find( + *criteria_list, + ) + .aggregate( + [_get_page_query(skip, limit, sort_field="created", ascending=False)], + ) .to_list() ) - else: - feeds = ( - await FeedDB.find().sort(-FeedDB.created).skip(skip).limit(limit).to_list() + print(feeds_and_count) + page_metadata = _construct_page_metadata(feeds_and_count, skip, limit) + page = Paged( + metadata=page_metadata, + data=[ + FeedOut(id=item.pop("_id"), **item) for item in feeds_and_count[0]["data"] + ], ) - - return [feed.dict() for feed in feeds] + return page.dict() @router.get("/{feed_id}", response_model=FeedOut) diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js index 49293e045..eabf35fd1 100644 --- a/frontend/src/actions/listeners.js +++ b/frontend/src/actions/listeners.js @@ -250,3 +250,20 @@ export function resetJobUpdates() { }); }; } + +export const RECEIVE_FEEDS = "RECEIVE_FEEDS" +export function fetchFeeds(name, skip=0, limit= 20) { + return (dispatch) => { + return V2.FeedsService.getFeedsApiV2FeedsGet(name, skip, limit) + .then((json) => { + dispatch({ + type: RECEIVE_FEEDS, + feeds: json, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(handleErrors(reason, fetchFeeds(skip, limit))); + }); + }; +} diff --git a/frontend/src/app.config.ts b/frontend/src/app.config.ts index f06933279..60ba2c7b4 100644 --- a/frontend/src/app.config.ts +++ b/frontend/src/app.config.ts @@ -27,6 +27,7 @@ interface Config { defaultUserPerPage: number; defaultApikeyPerPage: number; defaultExtractors: number; + defaultFeeds: number; defaultExtractionJobs: number; defaultMetadataDefintionPerPage: number; } @@ -87,6 +88,7 @@ config["defaultGroupPerPage"] = 5; config["defaultUserPerPage"] = 5; config["defaultApikeyPerPage"] = 5; config["defaultExtractors"] = 5; +config["defaultFeeds"] = 5; config["defaultExtractionJobs"] = 5; config["defaultMetadataDefintionPerPage"] = 5; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2317b84e9..4ae8c125e 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -35,7 +35,7 @@ import { getAdminModeStatus as getAdminModeStatusAction, toggleAdminMode as toggleAdminModeAction, } from "../actions/user"; -import { AdminPanelSettings } from "@mui/icons-material"; +import {AdminPanelSettings, SavedSearch} from "@mui/icons-material"; import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; import { Footer } from "./navigation/Footer"; @@ -435,6 +435,16 @@ export default function PersistentDrawerLeft(props) { ) : ( <> )} + + + + + + + + + +
diff --git a/frontend/src/components/listeners/Feeds.tsx b/frontend/src/components/listeners/Feeds.tsx new file mode 100644 index 000000000..424244c66 --- /dev/null +++ b/frontend/src/components/listeners/Feeds.tsx @@ -0,0 +1,190 @@ +import React, { ChangeEvent, useEffect, useState } from "react"; +import { + Box, Button, + Checkbox, + Divider, + FormControl, + FormControlLabel, + Grid, IconButton, + Input, + InputLabel, + List, + MenuItem, + Pagination, + Paper, + Select, + Switch, +} from "@mui/material"; + +import { RootState } from "../../types/data"; +import { useDispatch, useSelector } from "react-redux"; +import { + fetchFeeds +} from "../../actions/listeners"; +import config from "../../app.config"; +import { GenericSearchBox } from "../search/GenericSearchBox"; +import Layout from "../Layout"; +import TableContainer from "@mui/material/TableContainer"; +import Table from "@mui/material/Table"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import TableCell from "@mui/material/TableCell"; +import TableBody from "@mui/material/TableBody"; +import {Link} from "react-router-dom"; +import DeleteIcon from "@mui/icons-material/Delete"; + + +export function Feeds() { + // Redux connect equivalent + const dispatch = useDispatch(); + const listFeeds = ( + name: string| undefined, + skip: number | undefined, + limit: number | undefined + ) => + dispatch( + fetchFeeds( + name, + skip, + limit + ) + ); + + useEffect(() => { + listFeeds("", 0, 20); + }, []); + + const feeds = useSelector( + (state: RootState) => state.feed.feeds.data + ); + const pageMetadata = useSelector( + (state: RootState) => state.feed.feeds.metadata + ); + const [searchTerm, setSearchTerm] = useState(""); + const handlePageChange = (_: ChangeEvent, value: number) => { + const newSkip = (value - 1) * limit; + setCurrPageNum(value); + listFeeds(searchTerm, newSkip, limit); + }; + const handleFeedsSearch = () => { + listFeeds(searchTerm, (currPageNum - 1) * limit, limit); + //setSearchTerm("") + }; + const [currPageNum, setCurrPageNum] = useState(1); + const [limit] = useState(config.defaultFeeds); + + + // component did mount + + + + useEffect(() => { + listFeeds("", 0, limit); + }, []); + + useEffect(() => { + // reset page and reset category with each new search term + setCurrPageNum(1); + + listFeeds(searchTerm, 0, limit); + }, [searchTerm]); + + // @ts-ignore + return ( + +
+ + + {/*searchbox*/} + + + + + + + + + + Feed Name + + + Description + + + + + + {feeds.map((feed) => { + return ( + + + + + + {feed.description} + + + { + }} + > + + + + + ); + })} + +
+ + + +
+
+
+
+ ); +} diff --git a/frontend/src/openapi/v2/models/FeedIn.ts b/frontend/src/openapi/v2/models/FeedIn.ts index 0cd8528f0..c8b37d5e0 100644 --- a/frontend/src/openapi/v2/models/FeedIn.ts +++ b/frontend/src/openapi/v2/models/FeedIn.ts @@ -11,6 +11,7 @@ import type { SearchObject } from './SearchObject'; */ export type FeedIn = { name: string; + description?: string; search: SearchObject; listeners?: Array; } diff --git a/frontend/src/openapi/v2/models/FeedOut.ts b/frontend/src/openapi/v2/models/FeedOut.ts index f591afe44..64dae1f07 100644 --- a/frontend/src/openapi/v2/models/FeedOut.ts +++ b/frontend/src/openapi/v2/models/FeedOut.ts @@ -20,6 +20,7 @@ import type { SearchObject } from './SearchObject'; */ export type FeedOut = { name: string; + description?: string; search: SearchObject; listeners?: Array; id?: string; diff --git a/frontend/src/openapi/v2/services/FeedsService.ts b/frontend/src/openapi/v2/services/FeedsService.ts index 18301925e..ce18de407 100644 --- a/frontend/src/openapi/v2/services/FeedsService.ts +++ b/frontend/src/openapi/v2/services/FeedsService.ts @@ -4,6 +4,7 @@ import type { FeedIn } from '../models/FeedIn'; import type { FeedListener } from '../models/FeedListener'; import type { FeedOut } from '../models/FeedOut'; +import type { Paged } from '../models/Paged'; import type { CancelablePromise } from '../core/CancelablePromise'; import { request as __request } from '../core/request'; @@ -12,24 +13,27 @@ export class FeedsService { /** * Get Feeds * Fetch all existing Feeds. - * @param name + * @param searchTerm * @param skip * @param limit - * @returns FeedOut Successful Response + * @param datasetId + * @returns Paged Successful Response * @throws ApiError */ public static getFeedsApiV2FeedsGet( - name?: string, + searchTerm?: string, skip?: number, limit: number = 10, - ): CancelablePromise> { + datasetId?: string, + ): CancelablePromise { return __request({ method: 'GET', path: `/api/v2/feeds`, query: { - 'name': name, + 'searchTerm': searchTerm, 'skip': skip, 'limit': limit, + 'dataset_id': datasetId, }, errors: { 422: `Validation Error`, diff --git a/frontend/src/reducers/feeds.ts b/frontend/src/reducers/feeds.ts new file mode 100644 index 000000000..60d283cfc --- /dev/null +++ b/frontend/src/reducers/feeds.ts @@ -0,0 +1,19 @@ +import {FeedState} from "../types/data"; +import {FeedListener, FeedOut, Paged, PageMetadata} from "../openapi/v2"; +import {DataAction} from "../types/action"; +import {RECEIVE_FEEDS} from "../actions/listeners"; + +const defaultState: FeedState = { + feeds: { metadata: {}, data: >[] } +} + +const feeds = (state = defaultState, action: DataAction) => { + switch (action.type) { + case RECEIVE_FEEDS: + return Object.assign({}, state, {feeds: action.feeds}); + default: + return state; + } +} + +export default feeds; diff --git a/frontend/src/reducers/index.ts b/frontend/src/reducers/index.ts index 30f81c161..6e4df8f5c 100644 --- a/frontend/src/reducers/index.ts +++ b/frontend/src/reducers/index.ts @@ -11,6 +11,7 @@ import listeners from "./listeners"; import group from "./group"; import visualization from "./visualization"; import publicVisualization from "./public_visualization"; +import feeds from "./feeds"; const rootReducer = combineReducers({ file: file, @@ -25,6 +26,7 @@ const rootReducer = combineReducers({ group: group, visualization: visualization, publicVisualization: publicVisualization, + feed: feeds }); export default rootReducer; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index b709d770b..5976bf89c 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -42,6 +42,7 @@ import config from "./app.config"; import { MetadataDefinitions } from "./components/metadata/MetadataDefinitions"; import { MetadataDefinitionEntry } from "./components/metadata/MetadataDefinitionEntry"; import { AllListeners } from "./components/listeners/AllListeners"; +import { Feeds } from "./components/listeners/Feeds"; // https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5 const PrivateRoute = (props): JSX.Element => { @@ -240,6 +241,14 @@ export const AppRoutes = (): JSX.Element => { } /> + + + + } + /> Date: Thu, 16 May 2024 12:01:43 -0500 Subject: [PATCH 09/10] Revert "initial implementation of Feeds page" This reverts commit 275ccc7cd340169212da4b906e371295e70e5930. --- backend/app/models/feeds.py | 4 +- backend/app/routers/feeds.py | 51 ++--- frontend/src/actions/listeners.js | 17 -- frontend/src/app.config.ts | 2 - frontend/src/components/Layout.tsx | 12 +- frontend/src/components/listeners/Feeds.tsx | 190 ------------------ frontend/src/openapi/v2/models/FeedIn.ts | 1 - frontend/src/openapi/v2/models/FeedOut.ts | 1 - .../src/openapi/v2/services/FeedsService.ts | 14 +- frontend/src/reducers/feeds.ts | 19 -- frontend/src/reducers/index.ts | 2 - frontend/src/routes.tsx | 9 - frontend/src/types/action.ts | 9 +- frontend/src/types/data.ts | 7 +- 14 files changed, 25 insertions(+), 313 deletions(-) delete mode 100644 frontend/src/components/listeners/Feeds.tsx delete mode 100644 frontend/src/reducers/feeds.ts diff --git a/backend/app/models/feeds.py b/backend/app/models/feeds.py index 2e0a2b821..bc48d9bad 100644 --- a/backend/app/models/feeds.py +++ b/backend/app/models/feeds.py @@ -13,11 +13,13 @@ class JobFeed(BaseModel): resources match the saved search criteria for the Feed.""" name: str - description: str = "" search: SearchObject listeners: List[FeedListener] = [] +class FeedBase(JobFeed): + description: str = "" + class FeedIn(JobFeed): pass diff --git a/backend/app/routers/feeds.py b/backend/app/routers/feeds.py index a69b5681f..0500bc30a 100644 --- a/backend/app/routers/feeds.py +++ b/backend/app/routers/feeds.py @@ -10,18 +10,9 @@ from app.routers.authentication import get_admin, get_admin_mode from app.search.connect import check_search_result from beanie import PydanticObjectId -from beanie.operators import Or, RegEx from fastapi import APIRouter, Depends, HTTPException from pika.adapters.blocking_connection import BlockingChannel -from app.deps.authorization_deps import ListenerAuthorization - -from app.models.pages import Paged - -from app.models.pages import _construct_page_metadata - -from app.models.pages import _get_page_query - router = APIRouter() @@ -80,44 +71,28 @@ async def save_feed( return feed.dict() -@router.get("", response_model=Paged) +@router.get("", response_model=List[FeedOut]) async def get_feeds( - searchTerm: Optional[str] = None, + name: Optional[str] = None, user=Depends(get_current_user), skip: int = 0, limit: int = 10, - admin=Depends(get_admin), - admin_mode=Depends(get_admin_mode) ): """Fetch all existing Feeds.""" - criteria_list = [] - # if not admin or not admin_mode: - # criteria_list.append(FeedDB.creator == user) - if searchTerm is not None: - criteria_list.append( - Or( - RegEx(field=FeedDB.name, pattern=searchTerm), - RegEx(field=FeedDB.description, pattern=searchTerm), - )) - - feeds_and_count = ( - await FeedDB.find( - *criteria_list, - ) - .aggregate( - [_get_page_query(skip, limit, sort_field="created", ascending=False)], - ) + if name is not None: + feeds = ( + await FeedDB.find(FeedDB.name == name) + .sort(-FeedDB.created) + .skip(skip) + .limit(limit) .to_list() ) - print(feeds_and_count) - page_metadata = _construct_page_metadata(feeds_and_count, skip, limit) - page = Paged( - metadata=page_metadata, - data=[ - FeedOut(id=item.pop("_id"), **item) for item in feeds_and_count[0]["data"] - ], + else: + feeds = ( + await FeedDB.find().sort(-FeedDB.created).skip(skip).limit(limit).to_list() ) - return page.dict() + + return [feed.dict() for feed in feeds] @router.get("/{feed_id}", response_model=FeedOut) diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js index eabf35fd1..49293e045 100644 --- a/frontend/src/actions/listeners.js +++ b/frontend/src/actions/listeners.js @@ -250,20 +250,3 @@ export function resetJobUpdates() { }); }; } - -export const RECEIVE_FEEDS = "RECEIVE_FEEDS" -export function fetchFeeds(name, skip=0, limit= 20) { - return (dispatch) => { - return V2.FeedsService.getFeedsApiV2FeedsGet(name, skip, limit) - .then((json) => { - dispatch({ - type: RECEIVE_FEEDS, - feeds: json, - receivedAt: Date.now(), - }); - }) - .catch((reason) => { - dispatch(handleErrors(reason, fetchFeeds(skip, limit))); - }); - }; -} diff --git a/frontend/src/app.config.ts b/frontend/src/app.config.ts index 60ba2c7b4..f06933279 100644 --- a/frontend/src/app.config.ts +++ b/frontend/src/app.config.ts @@ -27,7 +27,6 @@ interface Config { defaultUserPerPage: number; defaultApikeyPerPage: number; defaultExtractors: number; - defaultFeeds: number; defaultExtractionJobs: number; defaultMetadataDefintionPerPage: number; } @@ -88,7 +87,6 @@ config["defaultGroupPerPage"] = 5; config["defaultUserPerPage"] = 5; config["defaultApikeyPerPage"] = 5; config["defaultExtractors"] = 5; -config["defaultFeeds"] = 5; config["defaultExtractionJobs"] = 5; config["defaultMetadataDefintionPerPage"] = 5; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 4ae8c125e..2317b84e9 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -35,7 +35,7 @@ import { getAdminModeStatus as getAdminModeStatusAction, toggleAdminMode as toggleAdminModeAction, } from "../actions/user"; -import {AdminPanelSettings, SavedSearch} from "@mui/icons-material"; +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"; @@ -435,16 +435,6 @@ export default function PersistentDrawerLeft(props) { ) : ( <> )} - - - - - - - - - -
diff --git a/frontend/src/components/listeners/Feeds.tsx b/frontend/src/components/listeners/Feeds.tsx deleted file mode 100644 index 424244c66..000000000 --- a/frontend/src/components/listeners/Feeds.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React, { ChangeEvent, useEffect, useState } from "react"; -import { - Box, Button, - Checkbox, - Divider, - FormControl, - FormControlLabel, - Grid, IconButton, - Input, - InputLabel, - List, - MenuItem, - Pagination, - Paper, - Select, - Switch, -} from "@mui/material"; - -import { RootState } from "../../types/data"; -import { useDispatch, useSelector } from "react-redux"; -import { - fetchFeeds -} from "../../actions/listeners"; -import config from "../../app.config"; -import { GenericSearchBox } from "../search/GenericSearchBox"; -import Layout from "../Layout"; -import TableContainer from "@mui/material/TableContainer"; -import Table from "@mui/material/Table"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import TableCell from "@mui/material/TableCell"; -import TableBody from "@mui/material/TableBody"; -import {Link} from "react-router-dom"; -import DeleteIcon from "@mui/icons-material/Delete"; - - -export function Feeds() { - // Redux connect equivalent - const dispatch = useDispatch(); - const listFeeds = ( - name: string| undefined, - skip: number | undefined, - limit: number | undefined - ) => - dispatch( - fetchFeeds( - name, - skip, - limit - ) - ); - - useEffect(() => { - listFeeds("", 0, 20); - }, []); - - const feeds = useSelector( - (state: RootState) => state.feed.feeds.data - ); - const pageMetadata = useSelector( - (state: RootState) => state.feed.feeds.metadata - ); - const [searchTerm, setSearchTerm] = useState(""); - const handlePageChange = (_: ChangeEvent, value: number) => { - const newSkip = (value - 1) * limit; - setCurrPageNum(value); - listFeeds(searchTerm, newSkip, limit); - }; - const handleFeedsSearch = () => { - listFeeds(searchTerm, (currPageNum - 1) * limit, limit); - //setSearchTerm("") - }; - const [currPageNum, setCurrPageNum] = useState(1); - const [limit] = useState(config.defaultFeeds); - - - // component did mount - - - - useEffect(() => { - listFeeds("", 0, limit); - }, []); - - useEffect(() => { - // reset page and reset category with each new search term - setCurrPageNum(1); - - listFeeds(searchTerm, 0, limit); - }, [searchTerm]); - - // @ts-ignore - return ( - -
- - - {/*searchbox*/} - - - - - - - - - - Feed Name - - - Description - - - - - - {feeds.map((feed) => { - return ( - - - - - - {feed.description} - - - { - }} - > - - - - - ); - })} - -
- - - -
-
-
-
- ); -} diff --git a/frontend/src/openapi/v2/models/FeedIn.ts b/frontend/src/openapi/v2/models/FeedIn.ts index c8b37d5e0..0cd8528f0 100644 --- a/frontend/src/openapi/v2/models/FeedIn.ts +++ b/frontend/src/openapi/v2/models/FeedIn.ts @@ -11,7 +11,6 @@ import type { SearchObject } from './SearchObject'; */ export type FeedIn = { name: string; - description?: string; search: SearchObject; listeners?: Array; } diff --git a/frontend/src/openapi/v2/models/FeedOut.ts b/frontend/src/openapi/v2/models/FeedOut.ts index 64dae1f07..f591afe44 100644 --- a/frontend/src/openapi/v2/models/FeedOut.ts +++ b/frontend/src/openapi/v2/models/FeedOut.ts @@ -20,7 +20,6 @@ import type { SearchObject } from './SearchObject'; */ export type FeedOut = { name: string; - description?: string; search: SearchObject; listeners?: Array; id?: string; diff --git a/frontend/src/openapi/v2/services/FeedsService.ts b/frontend/src/openapi/v2/services/FeedsService.ts index ce18de407..18301925e 100644 --- a/frontend/src/openapi/v2/services/FeedsService.ts +++ b/frontend/src/openapi/v2/services/FeedsService.ts @@ -4,7 +4,6 @@ import type { FeedIn } from '../models/FeedIn'; import type { FeedListener } from '../models/FeedListener'; import type { FeedOut } from '../models/FeedOut'; -import type { Paged } from '../models/Paged'; import type { CancelablePromise } from '../core/CancelablePromise'; import { request as __request } from '../core/request'; @@ -13,27 +12,24 @@ export class FeedsService { /** * Get Feeds * Fetch all existing Feeds. - * @param searchTerm + * @param name * @param skip * @param limit - * @param datasetId - * @returns Paged Successful Response + * @returns FeedOut Successful Response * @throws ApiError */ public static getFeedsApiV2FeedsGet( - searchTerm?: string, + name?: string, skip?: number, limit: number = 10, - datasetId?: string, - ): CancelablePromise { + ): CancelablePromise> { return __request({ method: 'GET', path: `/api/v2/feeds`, query: { - 'searchTerm': searchTerm, + 'name': name, 'skip': skip, 'limit': limit, - 'dataset_id': datasetId, }, errors: { 422: `Validation Error`, diff --git a/frontend/src/reducers/feeds.ts b/frontend/src/reducers/feeds.ts deleted file mode 100644 index 60d283cfc..000000000 --- a/frontend/src/reducers/feeds.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {FeedState} from "../types/data"; -import {FeedListener, FeedOut, Paged, PageMetadata} from "../openapi/v2"; -import {DataAction} from "../types/action"; -import {RECEIVE_FEEDS} from "../actions/listeners"; - -const defaultState: FeedState = { - feeds: { metadata: {}, data: >[] } -} - -const feeds = (state = defaultState, action: DataAction) => { - switch (action.type) { - case RECEIVE_FEEDS: - return Object.assign({}, state, {feeds: action.feeds}); - default: - return state; - } -} - -export default feeds; diff --git a/frontend/src/reducers/index.ts b/frontend/src/reducers/index.ts index 6e4df8f5c..30f81c161 100644 --- a/frontend/src/reducers/index.ts +++ b/frontend/src/reducers/index.ts @@ -11,7 +11,6 @@ import listeners from "./listeners"; import group from "./group"; import visualization from "./visualization"; import publicVisualization from "./public_visualization"; -import feeds from "./feeds"; const rootReducer = combineReducers({ file: file, @@ -26,7 +25,6 @@ const rootReducer = combineReducers({ group: group, visualization: visualization, publicVisualization: publicVisualization, - feed: feeds }); export default rootReducer; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 5976bf89c..b709d770b 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -42,7 +42,6 @@ import config from "./app.config"; import { MetadataDefinitions } from "./components/metadata/MetadataDefinitions"; import { MetadataDefinitionEntry } from "./components/metadata/MetadataDefinitionEntry"; import { AllListeners } from "./components/listeners/AllListeners"; -import { Feeds } from "./components/listeners/Feeds"; // https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5 const PrivateRoute = (props): JSX.Element => { @@ -241,14 +240,6 @@ export const AppRoutes = (): JSX.Element => { } /> - - - - } - /> Date: Thu, 16 May 2024 14:13:33 -0500 Subject: [PATCH 10/10] Used toggle switch button --- backend/app/routers/listeners.py | 24 ++- frontend/src/actions/listeners.js | 31 +++- frontend/src/components/Layout.tsx | 24 ++- .../src/components/listeners/AllListeners.tsx | 153 +++++++++++------- .../openapi/v2/services/ListenersService.ts | 34 +--- 5 files changed, 144 insertions(+), 122 deletions(-) diff --git a/backend/app/routers/listeners.py b/backend/app/routers/listeners.py index 4fb9d1dfc..949dea0f8 100644 --- a/backend/app/routers/listeners.py +++ b/backend/app/routers/listeners.py @@ -304,6 +304,7 @@ 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), ): @@ -316,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 = [ @@ -338,8 +340,8 @@ async def get_listeners( aggregation_pipeline.append( {"$match": {"properties.process.dataset": {"$exists": True}}} ) - # Non admin users can access only active listeners - if not admin or not admin_mode: + # 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( @@ -402,7 +404,7 @@ async def enable_listener( Arguments: listener_id -- UUID of the listener to be enabled """ - return await set_active_flag(listener_id, True) + return await _set_active_flag(listener_id, True) @router.put("/{listener_id}/disable", response_model=EventListenerOut) @@ -416,29 +418,25 @@ async def disable_listener( Arguments: listener_id -- UUID of the listener to be enabled """ - return await set_active_flag(listener_id, False) + return await _set_active_flag(listener_id, False) -@router.put("/{listener_id}/toggle", response_model=EventListenerOut) -async def set_active_flag( +async def _set_active_flag( listener_id: str, - active: Optional[bool] = None, + active: bool, allow: bool = Depends(ListenerAuthorization()), ): - """Toggle the active flag of an Event Listener. Only admins can enable/disbale listeners. + """Set the active flag of an Event Listener. Only admins can enable/disable listeners. Arguments: - listener_id -- UUID of the listener to be enabled + listener_id -- UUID of the listener to be enabled/disabled """ listener = await EventListenerDB.find_one( EventListenerDB.id == ObjectId(listener_id) ) if listener: try: - if active is not None: - listener.active = active - else: - listener.active = not listener.active + listener.active = active await listener.save() return listener.dict() except Exception as e: diff --git a/frontend/src/actions/listeners.js b/frontend/src/actions/listeners.js index 49293e045..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({ @@ -50,9 +52,28 @@ export function fetchListeners( } export const TOGGLE_ACTIVE_FLAG_LISTENER = "TOGGLE_ACTIVE_FLAG_LISTENER"; -export function toggleActiveFlagListener(id) { +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.setActiveFlagApiV2ListenersListenerIdTogglePut( + return V2.ListenersService.disableListenerApiV2ListenersListenerIdDisablePut( id ) .then((json) => { @@ -64,7 +85,7 @@ export function toggleActiveFlagListener(id) { }); }) .catch((reason) => { - dispatch(handleErrors(reason, toggleActiveFlagListener(id))); + dispatch(handleErrors(reason, disableListener(id))); }); }; } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2317b84e9..0a99302c9 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -421,20 +421,16 @@ export default function PersistentDrawerLeft(props) { - {currUserProfile.admin && adminMode ? ( - - - - - - - - - - - ) : ( - <> - )} + + + + + + + + + +
diff --git a/frontend/src/components/listeners/AllListeners.tsx b/frontend/src/components/listeners/AllListeners.tsx index 5ac9eb7f6..6d97535df 100644 --- a/frontend/src/components/listeners/AllListeners.tsx +++ b/frontend/src/components/listeners/AllListeners.tsx @@ -1,7 +1,6 @@ import React, { ChangeEvent, useEffect, useState } from "react"; import { Box, - Checkbox, Divider, FormControl, FormControlLabel, @@ -14,8 +13,11 @@ import { 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 { @@ -23,7 +25,8 @@ import { fetchListenerLabels, fetchListeners, queryListeners, - toggleActiveFlagListener, + enableListener as enableListenerAction, + disableListener as disableListenerAction, } from "../../actions/listeners"; import ListenerItem from "./ListenerItem"; import SubmitExtraction from "./SubmitExtraction"; @@ -31,7 +34,10 @@ import { capitalize } from "../../utils/common"; import config from "../../app.config"; import { GenericSearchBox } from "../search/GenericSearchBox"; import Layout from "../Layout"; -import { toBoolean } from "vega"; +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; @@ -58,7 +64,8 @@ export function AllListeners(props: ListenerProps) { selectedCategory, selectedLabel, aliveOnly, - process + process, + true ) ); const searchListeners = ( @@ -70,8 +77,9 @@ export function AllListeners(props: ListenerProps) { ) => dispatch(queryListeners(text, skip, limit, heartbeatInterval, process)); const listAvailableCategories = () => dispatch(fetchListenerCategories()); const listAvailableLabels = () => dispatch(fetchListenerLabels()); - const toggleActiveFlag = (id: string) => - dispatch(toggleActiveFlagListener(id)); + + const enableListener = (id: string) => dispatch(enableListenerAction(id)); + const disableListener = (id: string) => dispatch(disableListenerAction(id)); const listeners = useSelector( (state: RootState) => state.listener.listeners.data @@ -218,10 +226,6 @@ export function AllListeners(props: ListenerProps) { setOpenSubmitExtraction(false); }; - const setActiveListeners = (id: string) => { - toggleActiveFlag(id); - }; - return (
@@ -302,56 +306,85 @@ export function AllListeners(props: ListenerProps) { - - {listeners !== undefined ? ( - listeners.map((listener) => { - return ( -
- {admin && adminMode ? ( - { - setActiveListeners(listener.id); - }} - /> - ) : ( - <> - )} - - -
- ); - }) - ) : ( - <> - )} -
- - - + + + {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/openapi/v2/services/ListenersService.ts b/frontend/src/openapi/v2/services/ListenersService.ts index 86fda9258..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,7 @@ export class ListenersService { * @param label * @param aliveOnly * @param process + * @param all * @param datasetId * @returns Paged Successful Response * @throws ApiError @@ -51,6 +53,7 @@ export class ListenersService { label?: string, aliveOnly: boolean = false, process?: string, + all: boolean = false, datasetId?: string, ): CancelablePromise { return __request({ @@ -64,6 +67,7 @@ export class ListenersService { 'label': label, 'alive_only': aliveOnly, 'process': process, + 'all': all, 'dataset_id': datasetId, }, errors: { @@ -322,34 +326,4 @@ export class ListenersService { }); } - /** - * Set Active Flag - * Toggle the active flag of an Event Listener. Only admins can enable/disbale listeners. - * - * Arguments: - * listener_id -- UUID of the listener to be enabled - * @param listenerId - * @param active - * @param datasetId - * @returns EventListenerOut Successful Response - * @throws ApiError - */ - public static setActiveFlagApiV2ListenersListenerIdTogglePut( - listenerId: string, - active?: boolean, - datasetId?: string, - ): CancelablePromise { - return __request({ - method: 'PUT', - path: `/api/v2/listeners/${listenerId}/toggle`, - query: { - 'active': active, - 'dataset_id': datasetId, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - } \ No newline at end of file