Global search filter
This commit is contained in:
15
src/App.tsx
15
src/App.tsx
@ -1,18 +1,21 @@
|
||||
import React from "react";
|
||||
import { ThemeProvider } from "@mui/material";
|
||||
import { theme } from "./theme";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import AppHeader from "./components/AppHeader";
|
||||
import Home from "./routes/Home";
|
||||
import ThemeProvider from "@mui/material/styles/ThemeProvider";
|
||||
import { GlobalStateProvider } from "./GlobalStateProvider";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<AppHeader />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
{/* <Route path="*" element={<NotFound404 />} /> */}
|
||||
</Routes>
|
||||
<GlobalStateProvider>
|
||||
<AppHeader />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
{/* <Route path="*" element={<NotFound404 />} /> */}
|
||||
</Routes>
|
||||
</GlobalStateProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
36
src/GlobalStateProvider.tsx
Normal file
36
src/GlobalStateProvider.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { Dispatch, SetStateAction, createContext, useContext, useState } from "react";
|
||||
|
||||
// how to: Typesafe Global State with TypeScript, React & React Context
|
||||
// https://jamiehaywood.medium.com/typesafe-global-state-with-typescript-react-react-context-c2df743f3ce
|
||||
|
||||
interface GlobalState {
|
||||
searchFilter: string;
|
||||
}
|
||||
|
||||
const defaultGlobalState: GlobalState = {
|
||||
searchFilter: "",
|
||||
};
|
||||
|
||||
export const GlobalStateContext = createContext({
|
||||
state: {} as Partial<GlobalState>,
|
||||
setState: {} as Dispatch<SetStateAction<Partial<GlobalState>>>,
|
||||
});
|
||||
|
||||
export const GlobalStateProvider = ({
|
||||
children,
|
||||
value = {} as GlobalState,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value?: Partial<GlobalState>;
|
||||
}) => {
|
||||
const [state, setState] = useState(value);
|
||||
return <GlobalStateContext.Provider value={{ state, setState }}>{children}</GlobalStateContext.Provider>;
|
||||
};
|
||||
|
||||
export const useGlobalState = () => {
|
||||
const context = useContext(GlobalStateContext);
|
||||
if (!context) {
|
||||
throw new Error("useGlobalState must be used within a GlobalStateContext");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -1,17 +1,57 @@
|
||||
import React from "react";
|
||||
import { Container, Typography } from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import RadarIcon from "@mui/icons-material/Radar";
|
||||
import Container from "@mui/material/Container";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import CancelIcon from "@mui/icons-material/Cancel";
|
||||
import { useGlobalState } from "../GlobalStateProvider";
|
||||
|
||||
export default function AppHeader() {
|
||||
const [searchValue, setSearchValue] = React.useState("");
|
||||
const { setState: setGlobalState } = useGlobalState();
|
||||
|
||||
const handleSearchInputChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
const searchFilter = event.currentTarget.value;
|
||||
setGlobalState({
|
||||
searchFilter,
|
||||
});
|
||||
setSearchValue(event.currentTarget.value);
|
||||
};
|
||||
const handleClearSearchInputClick = () => {
|
||||
setSearchValue("");
|
||||
setGlobalState({
|
||||
searchFilter: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container maxWidth={false} className="app-header">
|
||||
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
|
||||
<RadarIcon fontSize="large" className="icon" />
|
||||
<Typography component="h1">pingator</Typography>
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
<Container maxWidth={false} className="app-header">
|
||||
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
|
||||
<RadarIcon fontSize="large" className="icon" />
|
||||
<Typography component="h1">pingator</Typography>
|
||||
|
||||
<TextField
|
||||
className="search-input"
|
||||
type="text"
|
||||
sx={{ backgroundColor: "white", marginLeft: "auto" }}
|
||||
label="Search"
|
||||
variant="filled"
|
||||
onChange={handleSearchInputChange}
|
||||
value={searchValue}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton size="small" onClick={handleClearSearchInputClick}>
|
||||
<CancelIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import Card from "@mui/material/Card";
|
||||
import CardContent from "@mui/material/CardContent";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
@ -22,24 +21,6 @@ function normalizeServiceName(name: string): string {
|
||||
.replace(/\b\w/g, (s) => s.toUpperCase());
|
||||
}
|
||||
|
||||
interface TenantsStatusTooltipProps {
|
||||
tenantsStatus: StatusPerTenant;
|
||||
}
|
||||
|
||||
function TenantsStatusTooltip({ tenantsStatus }: TenantsStatusTooltipProps) {
|
||||
const statusItems = Object.entries(tenantsStatus).map(([key, value]) => {
|
||||
console.log(`key: ${key}, value: ${value}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className="tenants-status-tooltip">
|
||||
<Typography color="inherit">Tooltip with HTML</Typography>
|
||||
<em>{"And here's"}</em> <b>{"some"}</b> <u>{"amazing content"}</u>. {"It's very engaging. Right?"}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function getNodeNameElement(node: Node): React.ReactElement {
|
||||
let statusMessage;
|
||||
const statusOk = node.health_check_status?.status_ok;
|
||||
@ -69,7 +50,11 @@ function TenantsStatus({ statusPerTenant }: TenantsStatusProps) {
|
||||
|
||||
for (const [tenantId, statusOk] of Object.entries(statusPerTenant)) {
|
||||
statuses.push(
|
||||
<Typography component="div" className={`tenant-status ${statusOk ? "status-ok" : "status-error"}`}>
|
||||
<Typography
|
||||
key={tenantId}
|
||||
component="div"
|
||||
className={`tenant-status ${statusOk ? "status-ok" : "status-error"}`}
|
||||
>
|
||||
{tenantId}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
@ -2,19 +2,27 @@ import React from "react";
|
||||
import { Service } from "../../types";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import ServiceCard from "./ServiceCard";
|
||||
import { useGlobalState } from "../../GlobalStateProvider";
|
||||
|
||||
interface ServiceListProps {
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
export default function ServiceList({ services }: ServiceListProps) {
|
||||
const serviceItems = services.map((service) => {
|
||||
return (
|
||||
<Grid key={service.name} sx={{ display: "flex" }}>
|
||||
<ServiceCard service={service} />
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
const { state } = useGlobalState();
|
||||
|
||||
const serviceItems = services
|
||||
.filter(
|
||||
(service) =>
|
||||
!state.searchFilter || service.name.toLocaleLowerCase().includes(state.searchFilter.toLocaleLowerCase())
|
||||
)
|
||||
.map((service) => {
|
||||
return (
|
||||
<Grid key={service.name} sx={{ display: "flex" }}>
|
||||
<ServiceCard service={service} />
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid container rowSpacing={2} columnSpacing={2} className="service-list" sx={{ marginBottom: "1rem" }}>
|
||||
|
||||
@ -20,4 +20,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background-color: whitesmoke;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
import createTheme from "@mui/material/styles/createTheme";
|
||||
|
||||
const PRIMARY_COLOR = "#0d6efd";
|
||||
const SECONDARY_COLOR = "#6c757d";
|
||||
|
||||
Reference in New Issue
Block a user