mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 10:39:40 -06:00
Feat: Uptime Monitor Filters
This commit is contained in:
@@ -1,108 +1,78 @@
|
||||
import {
|
||||
Checkbox,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from "@mui/material";
|
||||
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 selectStyles = {
|
||||
"& .MuiOutlinedInput-input": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiSelect-icon": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
};
|
||||
|
||||
const menuItemStyles = {
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControl
|
||||
sx={{ m: theme.spacing(2), minWidth: 120 }}
|
||||
size="small"
|
||||
<FormControl
|
||||
sx={{ m: theme.spacing(2), minWidth: "10%" }}
|
||||
size="small"
|
||||
>
|
||||
<Select
|
||||
multiple={multiple}
|
||||
IconComponent={(props) => (
|
||||
<AddCircleOutlineIcon
|
||||
{...props}
|
||||
sx={{ fontSize: "medium" }}
|
||||
/>
|
||||
)}
|
||||
displayEmpty
|
||||
value={value ?? []}
|
||||
onChange={onChange}
|
||||
renderValue={(selected) => {
|
||||
if (!selected?.length) {
|
||||
return header;
|
||||
}
|
||||
|
||||
return selected
|
||||
.map((value) => options.find((option) => option.value === value)?.label)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
}}
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastText,
|
||||
"&.Mui-focused": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{header}
|
||||
</InputLabel>
|
||||
<Select
|
||||
multiple={multiple}
|
||||
IconComponent={(props) => (
|
||||
<AddCircleOutlineIcon
|
||||
{...props}
|
||||
sx={{ fontSize: "medium" }}
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
<Checkbox
|
||||
checked={value?.includes(option.value)}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
renderValue={(selected) => selected.join(", ")}
|
||||
sx={selectStyles}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
value={option}
|
||||
sx={menuItemStyles}
|
||||
>
|
||||
<Checkbox
|
||||
checked={value.includes(option)}
|
||||
size="small"
|
||||
/>
|
||||
<ListItemText
|
||||
primary={
|
||||
option === "http"
|
||||
? "HTTP(S)"
|
||||
: option === "ping"
|
||||
? "Ping"
|
||||
: option === "docker"
|
||||
? "Docker"
|
||||
: option === "port"
|
||||
? "Port"
|
||||
: option
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<ListItemText primary={option.label} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
FilterHeader.propTypes = {
|
||||
header: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.string),
|
||||
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,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
multiple: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,30 +1,61 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import FilterHeader from "../../../../../Components/FilterHeader";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Filter = ({ selectedTypes, setSelectedTypes, setToFilterStatus, setToFilterActive }) => {
|
||||
/**
|
||||
* 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 Filter = ({
|
||||
selectedTypes,
|
||||
setSelectedTypes,
|
||||
selectedStatus,
|
||||
setSelectedStatus,
|
||||
selectedState,
|
||||
setSelectedState,
|
||||
setToFilterStatus,
|
||||
setToFilterActive,
|
||||
handleReset,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [selectedState, setSelectedState] = useState([]);
|
||||
const [selectedStatus, setSelectedStatus] = useState([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleTypeChange = (event) => {
|
||||
setSelectedTypes(event.target.value);
|
||||
const selectedValues = event.target.value;
|
||||
setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined);
|
||||
};
|
||||
|
||||
const handleStatusChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
setSelectedStatus(selectedValues);
|
||||
setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined);
|
||||
|
||||
if (selectedValues.length === 0 || selectedValues.length === 2) {
|
||||
setToFilterStatus(null);
|
||||
} else {
|
||||
setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleStateChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
@@ -37,21 +68,30 @@ const Filter = ({ selectedTypes, setSelectedTypes, setToFilterStatus, setToFilte
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedState([]);
|
||||
setSelectedTypes([]);
|
||||
setSelectedStatus([]);
|
||||
setToFilterStatus(null);
|
||||
setToFilterActive(null);
|
||||
};
|
||||
|
||||
const isFilterActive = useMemo(() => {
|
||||
return selectedTypes.length > 0 || selectedState.length > 0 || selectedStatus.length > 0;
|
||||
return (
|
||||
(selectedTypes?.length ?? 0) > 0 ||
|
||||
(selectedState?.length ?? 0) > 0 ||
|
||||
(selectedStatus?.length ?? 0) > 0
|
||||
);
|
||||
}, [selectedState, selectedTypes, selectedStatus]);
|
||||
|
||||
const typeOptions = ["http", "ping", "docker", "port"];
|
||||
const statusOptions = ["Up", "Down"];
|
||||
const stateOptions = ["Active", "Paused"];
|
||||
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" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -59,51 +99,53 @@ const Filter = ({ selectedTypes, setSelectedTypes, setToFilterStatus, setToFilte
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
ml: theme.spacing(80),
|
||||
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={{
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
visibility: isFilterActive ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
{t("reset")}
|
||||
</Button>
|
||||
<FilterHeader
|
||||
header="Type"
|
||||
options={typeOptions}
|
||||
value={selectedTypes}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
<FilterHeader
|
||||
header="Status"
|
||||
options={statusOptions}
|
||||
value={selectedStatus}
|
||||
onChange={handleStatusChange}
|
||||
/>
|
||||
<FilterHeader
|
||||
header="State"
|
||||
options={stateOptions}
|
||||
value={selectedState}
|
||||
onChange={handleStateChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Filter.propTypes = {
|
||||
selectedTypes: PropTypes.arrayOf(PropTypes.string),
|
||||
setSelectedTypes: PropTypes.func,
|
||||
setToFilterStatus: PropTypes.func,
|
||||
setToFilterActive: PropTypes.func,
|
||||
selectedTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
setSelectedTypes: PropTypes.func.isRequired,
|
||||
selectedStatus: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
setSelectedStatus: PropTypes.func.isRequired,
|
||||
selectedState: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
setSelectedState: PropTypes.func.isRequired,
|
||||
setToFilterStatus: PropTypes.func.isRequired,
|
||||
setToFilterActive: PropTypes.func.isRequired,
|
||||
handleReset: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
|
||||
@@ -70,7 +70,9 @@ const UptimeMonitors = () => {
|
||||
const [sort, setSort] = useState(undefined);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
|
||||
const [selectedTypes, setSelectedTypes] = useState([]);
|
||||
const [selectedTypes, setSelectedTypes] = useState(undefined);
|
||||
const [selectedState, setSelectedState] = useState(undefined);
|
||||
const [selectedStatus, setSelectedStatus] = useState(undefined);
|
||||
const [toFilterStatus, setToFilterStatus] = useState(null);
|
||||
const [toFilterActive, setToFilterActive] = useState(null);
|
||||
|
||||
@@ -107,19 +109,28 @@ const UptimeMonitors = () => {
|
||||
types: TYPES,
|
||||
monitorUpdateTrigger,
|
||||
});
|
||||
|
||||
let field = sort?.field;
|
||||
let filter = search;
|
||||
if (toFilterStatus !== null) {
|
||||
field = "status";
|
||||
filter = toFilterStatus;
|
||||
} else if (toFilterActive !== null) {
|
||||
field = "isActive";
|
||||
filter = toFilterActive;
|
||||
} else {
|
||||
field = sort?.field;
|
||||
filter = search;
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedState(undefined);
|
||||
setSelectedTypes(undefined);
|
||||
setSelectedStatus(undefined);
|
||||
setToFilterStatus(null);
|
||||
setToFilterActive(null);
|
||||
};
|
||||
|
||||
const field =
|
||||
toFilterStatus !== null
|
||||
? "status"
|
||||
: toFilterActive !== null
|
||||
? "isActive"
|
||||
: sort?.field;
|
||||
|
||||
const filter =
|
||||
toFilterStatus !== null
|
||||
? toFilterStatus
|
||||
: toFilterActive !== null
|
||||
? toFilterActive
|
||||
: search;
|
||||
|
||||
const [
|
||||
monitorsWithChecks,
|
||||
@@ -199,9 +210,13 @@ const UptimeMonitors = () => {
|
||||
<Filter
|
||||
selectedTypes={selectedTypes}
|
||||
setSelectedTypes={setSelectedTypes}
|
||||
toFilterStatus={toFilterStatus}
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
selectedState={selectedState}
|
||||
setSelectedState={setSelectedState}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
setToFilterActive={setToFilterActive}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
|
||||
@@ -225,6 +225,15 @@ const baseTheme = (palette) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemText: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"& .MuiTypography-root": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
@@ -453,6 +462,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",
|
||||
@@ -316,6 +317,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