Merge pull request #2010 from Br0wnHammer/feat/fe/uptime-filters

Feat: Uptime Monitor Filters
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-04-03 12:01:51 -07:00
commit 390a711f6a
6 files changed
+305 -3

No files matched your search

+81
View File
@@ -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 (
<FormControl
sx={{ m: theme.spacing(2), minWidth: "10%" }}
size="small"
>
<Select
multiple={multiple}
IconComponent={(props) => (
<AddCircleOutlineIcon
{...props}
sx={{ fontSize: "medium" }}
/>
)}
displayEmpty
value={controlledValue}
onChange={onChange}
renderValue={(selected) => {
if (!selected?.length) {
return header;
}
return selected
.map((value) => options.find((option) => option.value === value)?.label)
.filter(Boolean)
.join(", ");
}}
>
{options.map((option) => (
<MenuItem
key={option.value}
value={option.value}
>
<Checkbox
checked={controlledValue.includes(option.value)}
size="small"
/>
<ListItemText primary={option.label} />
</MenuItem>
))}
</Select>
</FormControl>
);
};
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;
@@ -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 (
<Box
sx={{
display: "flex",
flexDirection: "row",
alignItems: "center",
ml: theme.spacing(4),
gap: theme.spacing(2),
}}
>
<FilterHeader
header={t("type")}
options={typeOptions}
value={selectedTypes}
onChange={handleTypeChange}
/>
<FilterHeader
header={t("status")}
options={statusOptions}
value={selectedStatus}
onChange={handleStatusChange}
/>
<FilterHeader
header={t("state")}
options={stateOptions}
value={selectedState}
onChange={handleStateChange}
/>
<Button
color={theme.palette.primary.contrastText}
onClick={handleReset}
variant="contained"
endIcon={<ClearIcon />}
sx={{
visibility: isFilterActive ? "visible" : "hidden",
}}
>
{t("reset")}
</Button>
</Box>
);
};
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;
@@ -28,6 +28,7 @@ const SearchComponent = ({ monitors = [], onSearchChange, setIsSearching }) => {
width="25%"
minWidth={150}
ml="auto"
mt={2}
>
<Search
options={monitors}
+39 -3
View File
@@ -13,6 +13,7 @@ import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
import Fallback from "../../../Components/Fallback";
import GenericFallback from "../../../Components/GenericFallback";
import SearchComponent from "./Components/SearchComponent";
import Filter from "./Components/Filter";
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
@@ -69,6 +70,11 @@ const UptimeMonitors = () => {
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"}
></MonitorCountHeader>
<Filter
selectedTypes={selectedTypes}
setSelectedTypes={setSelectedTypes}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
setToFilterStatus={setToFilterStatus}
setToFilterActive={setToFilterActive}
handleReset={handleReset}
/>
<SearchComponent
monitors={monitors}
onSearchChange={setSearch}
+31
View File
@@ -225,6 +225,15 @@ const baseTheme = (palette) => ({
},
},
},
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 }) => ({
+2
View File
@@ -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",