diff --git a/client/src/Pages/Infrastructure/Monitors/Components/LoadingSpinner/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/LoadingSpinner/index.jsx new file mode 100644 index 000000000..a649473af --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/Components/LoadingSpinner/index.jsx @@ -0,0 +1,46 @@ +import { CircularProgress, Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +const LoadingSpinner = ({ shouldRender }) => { + const theme = useTheme(); + if (shouldRender === false) { + return; + } + + return ( + <> + + + + + + ); +}; + +LoadingSpinner.propTypes = { + shouldRender: PropTypes.bool, +}; + +export default LoadingSpinner; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx index 0ec435c60..02c32cc8c 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx @@ -1,9 +1,11 @@ // Components +import { Box } from "@mui/material"; import DataTable from "../../../../../Components/Table"; import Host from "../../../../../Components/Host"; import { StatusLabel } from "../../../../../Components/Label"; import { Stack } from "@mui/material"; import { InfrastructureMenu } from "../MonitorsTableMenu"; +import LoadingSpinner from "../LoadingSpinner"; // Assets import CPUChipIcon from "../../../../../assets/icons/cpu-chip.svg?react"; import CustomGauge from "../../../../../Components/Charts/CustomGauge"; @@ -15,7 +17,7 @@ import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete }) => { +const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete, isSearching }) => { // Utils const theme = useTheme(); const { t } = useTranslation(); @@ -119,23 +121,26 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete }); return ( - + + openDetails(row.id), - emptyView: "No monitors found", - }} - /> + onRowClick: (row) => openDetails(row.id), + emptyView: "No monitors found", + }} + /> + ); }; @@ -144,6 +149,7 @@ MonitorsTable.propTypes = { monitors: PropTypes.array, isAdmin: PropTypes.bool, handleActionMenuDelete: PropTypes.func, + isSearching: PropTypes.bool, }; export default MonitorsTable; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/SearchComponent/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/SearchComponent/index.jsx new file mode 100644 index 000000000..0c6eec854 --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/Components/SearchComponent/index.jsx @@ -0,0 +1,49 @@ +import { useState } from "react"; +import Search from "../../../../../Components/Inputs/Search"; +import { Box } from "@mui/material"; +import useDebounce from "../../Hooks/useDebounce"; +import { useEffect, useRef } from "react"; +import PropTypes from "prop-types"; + +const SearchComponent = ({ monitors = [], onSearchChange, setIsSearching }) => { + const isFirstRender = useRef(true); + const [localSearch, setLocalSearch] = useState(""); + const debouncedSearch = useDebounce(localSearch, 500); + useEffect(() => { + if (isFirstRender.current === true) { + isFirstRender.current = false; + return; + } + onSearchChange(debouncedSearch); + setIsSearching(false); + }, [debouncedSearch, onSearchChange, setIsSearching]); + + const handleSearch = (value) => { + setLocalSearch(value); + setIsSearching(true); + }; + + return ( + + + + ); +}; + +SearchComponent.propTypes = { + monitors: PropTypes.array, + onSearchChange: PropTypes.func, + setIsSearching: PropTypes.func, +}; + +export default SearchComponent; diff --git a/client/src/Pages/Infrastructure/Monitors/Hooks/useDebounce.jsx b/client/src/Pages/Infrastructure/Monitors/Hooks/useDebounce.jsx new file mode 100644 index 000000000..fad002e48 --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/Hooks/useDebounce.jsx @@ -0,0 +1,18 @@ +import { useState, useEffect } from "react"; + +const useDebounce = (value, delay) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + return debouncedValue; +}; + +export default useDebounce; diff --git a/client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx b/client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx index 883205c77..2ef725ffd 100644 --- a/client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx @@ -3,7 +3,7 @@ import { useSelector } from "react-redux"; import { networkService } from "../../../../main"; import { createToast } from "../../../../Utils/toastUtils"; -const useMonitorFetch = ({ page, field, filter, rowsPerPage, updateTrigger }) => { +const useMonitorFetch = ({ page, field, filter, rowsPerPage, updateTrigger, search }) => { // Redux state const { user } = useSelector((state) => state.auth); @@ -24,6 +24,7 @@ const useMonitorFetch = ({ page, field, filter, rowsPerPage, updateTrigger }) => types: ["hardware"], page: page, rowsPerPage: rowsPerPage, + search: search }); setMonitors(response?.data?.data?.filteredMonitors ?? []); setSummary(response?.data?.data?.summary ?? {}); @@ -38,7 +39,7 @@ const useMonitorFetch = ({ page, field, filter, rowsPerPage, updateTrigger }) => }; fetchMonitors(); - }, [page, field, filter, rowsPerPage, user.teamId, updateTrigger]); + }, [page, field, filter, rowsPerPage, user.teamId, updateTrigger, search]); return { monitors, summary, isLoading, networkError }; }; diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index bfba843c0..7af0be373 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -8,6 +8,7 @@ import Pagination from "../../..//Components/Table/TablePagination"; import GenericFallback from "../../../Components/GenericFallback"; import Fallback from "../../../Components/Fallback"; import Filter from "./Components/Filters"; +import SearchComponent from "./Components/SearchComponent"; // Utils import { useTheme } from "@emotion/react"; import { useMonitorFetch } from "./Hooks/useMonitorFetch"; @@ -24,6 +25,8 @@ const InfrastructureMonitors = () => { const [updateTrigger, setUpdateTrigger] = useState(false); const [selectedStatus, setSelectedStatus] = useState(undefined); const [toFilterStatus, setToFilterStatus] = useState(undefined); + const [search, setSearch] = useState(undefined); + const [isSearching, setIsSearching] = useState(false); // Utils const theme = useTheme(); @@ -56,6 +59,7 @@ const InfrastructureMonitors = () => { filter: toFilterStatus, rowsPerPage, updateTrigger, + search }); if (networkError === true) { @@ -108,12 +112,19 @@ const InfrastructureMonitors = () => { setToFilterStatus={setToFilterStatus} handleReset={handleReset} /> + + } The response from the axios GET request. */ async getMonitorsByTeamId(config) { - const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config; + const { teamId, limit, types, page, rowsPerPage, filter, field, order, search } = config; const params = new URLSearchParams(); if (limit) params.append("limit", limit); @@ -200,6 +201,7 @@ class NetworkService { if (filter) params.append("filter", filter); if (field) params.append("field", field); if (order) params.append("order", order); + if (search) params.append("search", search); return this.axiosInstance.get(`/monitors/team/${teamId}?${params.toString()}`, { headers: { diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js index 3d9adab39..0e90c9c7f 100755 --- a/server/db/mongo/modules/monitorModule.js +++ b/server/db/mongo/modules/monitorModule.js @@ -510,7 +510,7 @@ const getMonitorById = async (monitorId) => { }; const getMonitorsByTeamId = async (req) => { - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; + let { limit, type, page, rowsPerPage, filter, field, order, search } = req.query; limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); @@ -523,6 +523,9 @@ const getMonitorsByTeamId = async (req) => { if (type !== undefined) { matchStage.type = Array.isArray(type) ? { $in: type } : type; } + if (search) { + matchStage.name = { $regex: search, $options: "i" }; + } const summaryResult = await Monitor.aggregate( buildMonitorSummaryByTeamIdPipeline({ matchStage }) diff --git a/server/validation/joi.js b/server/validation/joi.js index 271cd2d21..40acc35c2 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -147,6 +147,7 @@ const getMonitorsByTeamIdQueryValidation = joi.object({ filter: joi.string(), field: joi.string(), order: joi.string().valid("asc", "desc"), + search: joi.string() }); const getMonitorStatsByIdParamValidation = joi.object({