Merge pull request #2010 from Br0wnHammer/feat/fe/uptime-filters
Feat: Uptime Monitor Filters
This commit is contained in:
6 files changed
+305
-3
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }) => ({
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user