diff --git a/src/Components/FilterHeader/index.jsx b/src/Components/FilterHeader/index.jsx new file mode 100644 index 000000000..73c7d69ac --- /dev/null +++ b/src/Components/FilterHeader/index.jsx @@ -0,0 +1,81 @@ +import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; + +/** + * A reusable filter header component that displays a dropdown menu with selectable options. + * + * @component + * @param {Object} props - The component props. + * @param {string} props.header - The header text to display when no options are selected. + * @param {Array} props.options - An array of options to display in the dropdown menu. Each option should have a `value` and `label`. + * @param {Array} [props.value] - The currently selected values. + * @param {Function} props.onChange - The callback function to handle changes in the selected values. + * @param {boolean} [props.multiple=true] - Whether multiple options can be selected. + * @returns {JSX.Element} The rendered FilterHeader component. + */ + +const FilterHeader = ({ header, options, value, onChange, multiple = true }) => { + const theme = useTheme(); + + const controlledValue = value === undefined ? [] : value; // Ensure value is always treated as an array for controlled component purposes + + return ( + + + + ); +}; + +FilterHeader.propTypes = { + header: PropTypes.string.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, + value: PropTypes.arrayOf(PropTypes.string), + onChange: PropTypes.func.isRequired, + multiple: PropTypes.bool, +}; + +export default FilterHeader; diff --git a/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/src/Pages/Uptime/Monitors/Components/Filter/index.jsx new file mode 100644 index 000000000..7080b35f8 --- /dev/null +++ b/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -0,0 +1,151 @@ +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +import FilterHeader from "../../../../../Components/FilterHeader"; +import { useMemo } from "react"; +import { Box, Button } from "@mui/material"; +import ClearIcon from "@mui/icons-material/Clear"; +import { useTranslation } from "react-i18next"; + +/** + * Filter Component + * + * A high-level component that provides filtering options for type, status, and state. + * It allows users to select multiple options for each filter and reset the filters. + * + * @component + * @param {Object} props - The component props. + * @param {string[]} props.selectedTypes - An array of selected type values. + * @param {function} props.setSelectedTypes - A function to set the selected type values. + * @param {string[]} props.selectedStatus - An array of selected status values. + * @param {function} props.setSelectedStatus - A function to set the selected status values. + * @param {string[]} props.selectedState - An array of selected state values. + * @param {function} props.setSelectedState - A function to set the selected state values. + * @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values. + * @param {function} props.setToFilterActive - A function to set the filter active state based on selected state values. + * @param {function} props.handleReset - A function to reset all filters. + * + * @returns {JSX.Element} The rendered Filter component. + */ + +const typeOptions = [ + { value: "http", label: "HTTP(S)" }, + { value: "ping", label: "Ping" }, + { value: "docker", label: "Docker" }, + { value: "port", label: "Port" }, +]; + +const statusOptions = [ + { value: "Up", label: "Up" }, + { value: "Down", label: "Down" }, +]; + +const stateOptions = [ + { value: "Active", label: "Active" }, + { value: "Paused", label: "Paused" }, +]; + +const Filter = ({ + selectedTypes, + setSelectedTypes, + selectedStatus, + setSelectedStatus, + selectedState, + setSelectedState, + setToFilterStatus, + setToFilterActive, + handleReset, +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const handleTypeChange = (event) => { + const selectedValues = event.target.value; + setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined); + }; + + const handleStatusChange = (event) => { + const selectedValues = event.target.value; + setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined); + + if (selectedValues.length === 0 || selectedValues.length === 2) { + setToFilterStatus(undefined); + } else { + setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false"); + } + }; + + const handleStateChange = (event) => { + const selectedValues = event.target.value; + setSelectedState(selectedValues); + + if (selectedValues.length === 0 || selectedValues.length === 2) { + setToFilterActive(undefined); + } else { + setToFilterActive(selectedValues[0] === "Active" ? "true" : "false"); + } + }; + + const isFilterActive = useMemo(() => { + return ( + (selectedTypes?.length ?? 0) > 0 || + (selectedState?.length ?? 0) > 0 || + (selectedStatus?.length ?? 0) > 0 + ); + }, [selectedState, selectedTypes, selectedStatus]); + + return ( + + + + + + + ); +}; + +Filter.propTypes = { + selectedTypes: PropTypes.arrayOf(PropTypes.string), + setSelectedTypes: PropTypes.func.isRequired, + selectedStatus: PropTypes.arrayOf(PropTypes.string), + setSelectedStatus: PropTypes.func.isRequired, + selectedState: PropTypes.arrayOf(PropTypes.string), + setSelectedState: PropTypes.func.isRequired, + setToFilterStatus: PropTypes.func.isRequired, + setToFilterActive: PropTypes.func.isRequired, + handleReset: PropTypes.func.isRequired, +}; + +export default Filter; diff --git a/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx b/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx index 749f921a8..0c6eec854 100644 --- a/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx +++ b/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx @@ -28,6 +28,7 @@ const SearchComponent = ({ monitors = [], onSearchChange, setIsSearching }) => { width="25%" minWidth={150} ml="auto" + mt={2} > { const [sort, setSort] = useState(undefined); const [isSearching, setIsSearching] = useState(false); const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); + const [selectedTypes, setSelectedTypes] = useState(undefined); + const [selectedState, setSelectedState] = useState(undefined); + const [selectedStatus, setSelectedStatus] = useState(undefined); + const [toFilterStatus, setToFilterStatus] = useState(undefined); + const [toFilterActive, setToFilterActive] = useState(undefined); // Utils const theme = useTheme(); @@ -104,6 +110,25 @@ const UptimeMonitors = () => { monitorUpdateTrigger, }); + const handleReset = () => { + setSelectedState(undefined); + setSelectedTypes(undefined); + setSelectedStatus(undefined); + setToFilterStatus(undefined); + setToFilterActive(undefined); + }; + + const filterLookup = new Map([ + [toFilterStatus, "status"], + [toFilterActive, "isActive"] + ]); + + const activeFilter = [...filterLookup].find(([key]) => key !== undefined); + const field = activeFilter?.[1] || sort?.field; + const filter = activeFilter?.[0] || search; + + const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; + const [ monitorsWithChecks, monitorsWithChecksCount, @@ -111,12 +136,12 @@ const UptimeMonitors = () => { monitorsWithChecksNetworkError, ] = useFetchMonitorsWithChecks({ teamId, - types: TYPES, + types: effectiveTypes, limit: 25, page: page, rowsPerPage: rowsPerPage, - filter: search, - field: sort?.field, + filter: filter, + field: field, order: sort?.order, monitorUpdateTrigger, }); @@ -179,6 +204,17 @@ const UptimeMonitors = () => { monitorCount={monitorsSummary?.totalMonitors} heading={"Uptime monitors"} > + ({ }, }, }, + MuiListItemText: { + styleOverrides: { + root: ({ theme }) => ({ + "& .MuiTypography-root": { + color: theme.palette.primary.contrastText, + }, + }), + }, + }, MuiMenuItem: { styleOverrides: { root: ({ theme }) => ({ @@ -462,6 +471,28 @@ const baseTheme = (palette) => ({ }), }, }, + MuiSelect: { + styleOverrides: { + root: ({ theme }) => ({ + "& .MuiOutlinedInput-input": { + color: theme.palette.primary.contrastText, + }, + "& .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.primary.lowContrast, + borderRadius: theme.shape.borderRadius, + }, + "& .MuiSelect-icon": { + color: theme.palette.primary.contrastTextSecondary, // Dropdown + color + }, + "&:hover": { + backgroundColor: theme.palette.primary.main, // Background on hover + }, + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.primary.lowContrast, + }, + }), + }, + }, MuiButtonGroup: { styleOverrides: { root: ({ theme }) => ({ diff --git a/src/locales/gb.json b/src/locales/gb.json index 5dfe9b4ca..b935f5b39 100644 --- a/src/locales/gb.json +++ b/src/locales/gb.json @@ -111,6 +111,7 @@ "configure": "Configure", "networkError": "Network error", "responseTime": "Response time:", + "reset": "Reset", "ms": "ms", "bar": "Bar", "area": "Area", @@ -314,6 +315,7 @@ "statusCode": "Status code", "date&Time": "Date & Time", "type": "Type", + "state": "State", "statusPageName": "Status page name", "publicURL": "Public URL", "repeat": "Repeat",