Merge pull request #853 from bluewave-labs/feat/search-component

Feat/search component
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-09-18 17:34:27 -07:00
commit b8d2dd834a
7 files changed
+157 -171

No files matched your search

@@ -1,116 +0,0 @@
import { Autocomplete, TextField } from "@mui/material";
import React from "react";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
/**
* @example
*
* const options = [
* { _id: "66d6119ef959cbc681e034f0", name: "Googler" },
* { _id: "66d6119ef959cbc681e034f0", name: "CNN" },
* { _id: "66d61a1bf959cbc681e0353f", name: "X Corp." },
* ];
*
* <AutoCompleteField options={options} />
*
*/
/**
* AutoCompleteField component.
*
* @component
* @param {Object} props - The component props.
* @param {string} props.id - The ID of the autocomplete field.
* @param {string} props.type - The type of the input field (text or number).
* @param {Array} props.options - The options for the autocomplete field.
* @param {string} props.placeholder - The placeholder text for the input field.
* @param {boolean} props.disabled - Indicates if the field is disabled.
* @returns {JSX.Element} The AutoCompleteField component.
*/
const AutoCompleteField = ({
id,
type,
options,
placeholder = "Type to search",
disabled,
}) => {
const [value, setValue] = React.useState();
const [inputValue, setInputValue] = React.useState("");
const theme = useTheme();
return (
<Autocomplete
freeSolo
className="auto-complete-field"
id={id}
value={value}
onChange={(event, newValue) => {
setValue(newValue);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
options={options}
getOptionLabel={(option) => option.name}
renderInput={(params) => (
<TextField
{...params}
type={type}
placeholder={placeholder}
disabled={disabled}
InputProps={{
...params.InputProps,
sx: {
width: 360,
height: 34,
fontSize: 13,
p: 0,
borderRadius: theme.shape.borderRadius,
"& input": {
p: 0,
},
},
}}
/>
)}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<li key={option._id} {...optionProps}>
<div>{<span>{option.name}</span>}</div>
</li>
);
}}
slotProps={{
popper: {
sx: {
"& ul": { p: 0 },
"& li": { borderRadius: theme.shape.borderRadius },
},
},
paper: {
sx: {
p: 2,
fontSize: 13,
},
},
}}
/>
);
};
AutoCompleteField.displayName = "AutoCompleteField";
AutoCompleteField.propTypes = {
id: PropTypes.string,
type: PropTypes.oneOf(["text", "number"]),
options: PropTypes.array,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
setWidth: PropTypes.string,
};
export default AutoCompleteField;
@@ -0,0 +1,138 @@
import PropTypes from "prop-types";
import { Box, ListItem, Autocomplete, TextField } from "@mui/material";
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
/**
* Search component using Material UI's Autocomplete.
*
* @param {Object} props
* @param {string} props.id - Unique identifier for the Autocomplete component
* @param {Array<Object>} props.options - Options to display in the Autocomplete dropdown
* @param {string} props.filteredBy - Key to access the option label from the options
* @param {string} props.value - Current input value for the Autocomplete
* @param {Function} props.handleChange - Function to call when the input changes
* @param {Object} props.sx - Additional styles to apply to the component
* @returns {JSX.Element} The rendered Search component
*/
const Search = ({ id, options, filteredBy, value, handleChange, sx }) => {
const theme = useTheme();
return (
<Autocomplete
id={id}
inputValue={value}
onInputChange={(event, newValue) => {
handleChange(newValue);
}}
fullWidth
freeSolo
disableClearable
options={options}
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
<TextField
{...params}
placeholder="Type to search"
InputProps={{
...params.InputProps,
startAdornment: (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
),
}}
sx={{
"& fieldset": {
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
borderColor: theme.palette.border.light,
},
}}
/>
)}
filterOptions={(options, { inputValue }) => {
const filtered = options.filter((option) =>
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
);
if (filtered.length === 0) {
return [{ [filteredBy]: "No monitors found", noOptions: true }];
}
return filtered;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
sx={
option.noOptions
? {
pointerEvents: "none",
backgroundColor: theme.palette.background.main,
}
: {}
}
>
{option[filteredBy]}
</ListItem>
);
}}
slotProps={{
popper: {
keepMounted: true,
sx: {
"& ul": { p: 2 },
"& li.MuiAutocomplete-option": {
color: theme.palette.text.secondary,
px: 4,
borderRadius: theme.shape.borderRadius,
},
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
{
backgroundColor: theme.palette.background.fill,
},
"& .MuiAutocomplete-noOptions": {
px: theme.spacing(6),
py: theme.spacing(5),
},
},
},
}}
sx={{
height: 34,
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
...sx,
}}
/>
);
};
Search.propTypes = {
id: PropTypes.string,
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
sx: PropTypes.object,
};
export default Search;
Whitespace-only changes.
-49
View File
@@ -1,49 +0,0 @@
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Chip from '@mui/material/Chip';
import Box from '@mui/material/Box';
import { useTheme } from "@mui/material";
const teamMembers = [
{ title: 'John Doe'},
{ title: 'Jane Smith'},
{ title: 'Alex Johnson'},
];
/**
* @component
* @returns {JSX.Element}
*/
export default function Search()
{
const theme = useTheme();
return (
<Box padding={theme.spacing(2)}> {/* Add padding to the container */}
<Autocomplete
multiple
id="tags-outlined"
options={teamMembers}
getOptionLabel={(option) => option.title}
filterSelectedOptions
renderInput={(params) => (
<TextField
{...params}
label="Team Members"
placeholder="Favorites"
/>
)}
renderTags={(value, getTagProps) =>
value.map((option) => (
<Chip
key={option.title}
variant="outlined"
label={option.title}
{...getTagProps({ option })}
/>
))
}
/>
</Box>
);
}
@@ -17,7 +17,7 @@ import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import { setRowsPerPage } from "../../../../Features/UI/uiSlice";
import { useState, useEffect } from "react";
import { useState, useEffect, memo } from "react";
import { useNavigate } from "react-router";
import { logger } from "../../../../Utils/Logger";
import Host from "../host";
@@ -408,4 +408,5 @@ MonitorTable.propTypes = {
isAdmin: PropTypes.bool,
};
export default MonitorTable;
const MemoizedMonitorTable = memo(MonitorTable);
export default MemoizedMonitorTable;
+14 -3
View File
@@ -1,5 +1,5 @@
import "./index.css";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
@@ -12,12 +12,14 @@ import StatusBox from "./StatusBox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Greeting from "../../../Utils/greeting";
import MonitorTable from "./MonitorTable";
import Search from "../../../Components/Inputs/Search";
const Monitors = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const monitorState = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const [search, setSearch] = useState("");
const dispatch = useDispatch({});
useEffect(() => {
@@ -26,6 +28,7 @@ const Monitors = ({ isAdmin }) => {
let loading =
monitorState?.isLoading &&
monitorState?.monitorsSummary?.monitors?.length === 0;
return (
<Stack className="monitors" gap={theme.spacing(8)}>
{loading ? (
@@ -85,7 +88,8 @@ const Monitors = ({ isAdmin }) => {
</Stack>
<Box
flex={1}
p={theme.spacing(10)}
px={theme.spacing(10)}
py={theme.spacing(8)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
@@ -113,7 +117,14 @@ const Monitors = ({ isAdmin }) => {
>
{monitorState?.monitorsSummary?.monitorCounts?.total || 0}
</Box>
{/* TODO - add search bar */}
<Box width="25%" minWidth={150} ml="auto">
<Search
options={monitorState?.monitorsSummary.monitors}
filteredBy="name"
value={search}
handleChange={setSearch}
/>
</Box>
</Stack>
<MonitorTable isAdmin={isAdmin} />
</Box>
+2 -1
View File
@@ -151,12 +151,14 @@ const lightTheme = createTheme({
styleOverrides: {
root: {
marginTop: 4,
padding: 0,
border: 1,
borderStyle: "solid",
borderColor: border.light,
borderRadius: 4,
boxShadow: shadow,
backgroundColor: background.main,
backgroundImage: "none",
},
},
},
@@ -183,7 +185,6 @@ const lightTheme = createTheme({
color: text.secondary,
fontSize: 13,
margin: 2,
marginBottom: 0,
minWidth: 100,
"&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible":
{