Compare commits

..

3 Commits

Author SHA1 Message Date
0c86009271 Nicer colors 2024-01-28 12:33:00 +01:00
03bba20b50 Nicer colors 2024-01-28 12:26:29 +01:00
b1f7d8196a Global search filter 2024-01-28 10:35:01 +01:00
18 changed files with 192 additions and 72 deletions

View File

@ -1,18 +1,21 @@
import React from "react"; import React from "react";
import { ThemeProvider } from "@mui/material";
import { theme } from "./theme"; import { theme } from "./theme";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import AppHeader from "./components/AppHeader"; import AppHeader from "./components/AppHeader";
import Home from "./routes/Home"; import Home from "./routes/Home";
import ThemeProvider from "@mui/material/styles/ThemeProvider";
import { GlobalStateProvider } from "./GlobalStateProvider";
function App() { function App() {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<GlobalStateProvider>
<AppHeader /> <AppHeader />
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
{/* <Route path="*" element={<NotFound404 />} /> */} {/* <Route path="*" element={<NotFound404 />} /> */}
</Routes> </Routes>
</GlobalStateProvider>
</ThemeProvider> </ThemeProvider>
); );
} }

View 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;
};

View File

@ -1,17 +1,57 @@
import React from "react"; import React from "react";
import { Container, Typography } from "@mui/material";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import RadarIcon from "@mui/icons-material/Radar"; 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() { 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 ( return (
<>
<Container maxWidth={false} className="app-header"> <Container maxWidth={false} className="app-header">
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}> <Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<RadarIcon fontSize="large" className="icon" /> <RadarIcon fontSize="large" className="icon" />
<Typography component="h1">pingator</Typography> <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> </Stack>
</Container> </Container>
</>
); );
} }

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import EnvTabsContainer from "./EnvTabsContainer"; import EnvTabsSwitcher from "./EnvTabsSwitcher";
import LoadingIndicator from "./LoadingIndicator"; import LoadingIndicator from "./LoadingIndicator";
import { DashboardLoadError, DashboardResponseContent, EnvTab, Environment } from "../types"; import { DashboardLoadError, DashboardResponseContent, EnvTab, Environment } from "../types";
import { ENVIRONMENT_TABS } from "../const"; import { ENVIRONMENT_TABS } from "../const";
@ -52,7 +52,7 @@ export default function Dashboard() {
return ( return (
<> <>
<LoadingIndicator active={loadingEnv} /> <LoadingIndicator active={loadingEnv} />
<EnvTabsContainer selectedEnv={selectedEnv} onSelect={onSelectEnvTab} /> <EnvTabsSwitcher selectedEnv={selectedEnv} onSelect={onSelectEnvTab} />
{loadingErr && <LoadingError error={loadingErr} />} {loadingErr && <LoadingError error={loadingErr} />}
{activeEnvironments && <EnvironmentList environments={activeEnvironments} />} {activeEnvironments && <EnvironmentList environments={activeEnvironments} />}
</> </>

View File

@ -9,12 +9,12 @@ interface OnSelectTab {
(tab: EnvTab): void; (tab: EnvTab): void;
} }
interface EnvTabsContainerProps { interface EnvTabsSwitcherProps {
selectedEnv: EnvTab; selectedEnv: EnvTab;
onSelect: OnSelectTab; onSelect: OnSelectTab;
} }
export default function EnvTabsContainer({ selectedEnv, onSelect }: EnvTabsContainerProps) { export default function EnvTabsSwitcher({ selectedEnv, onSelect }: EnvTabsSwitcherProps) {
const [selected, setSelected] = React.useState(selectedEnv); const [selected, setSelected] = React.useState(selectedEnv);
const handleChange = (event: React.SyntheticEvent, newValue: EnvTab) => { const handleChange = (event: React.SyntheticEvent, newValue: EnvTab) => {
@ -27,12 +27,10 @@ export default function EnvTabsContainer({ selectedEnv, onSelect }: EnvTabsConta
}); });
return ( return (
<Box sx={{ width: "100%", marginBottom: "2rem" }}> <Box className="env-tab-switcher">
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={selected} onChange={handleChange}> <Tabs value={selected} onChange={handleChange}>
{tabs} {tabs}
</Tabs> </Tabs>
</Box> </Box>
</Box>
); );
} }

View File

@ -3,6 +3,7 @@ import { Environment } from "../../types";
import TenantList from "./TenantList"; import TenantList from "./TenantList";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
interface EnvironmentListProps { interface EnvironmentListProps {
environments: Environment[]; environments: Environment[];
@ -12,13 +13,15 @@ export default function EnvironmentList({ environments }: EnvironmentListProps)
const envItems = environments.map((env) => { const envItems = environments.map((env) => {
return ( return (
<Box className="environment" key={env.name} sx={{ marginBottom: "2rem" }}> <Box className="environment" key={env.name} sx={{ marginBottom: "2rem" }}>
<Typography variant={"h2"} key={env.name} className="title"> <Container maxWidth={false} className="main-container">
<Typography component="span" variant={"h5"} key={env.name} sx={{ marginRight: "1rem" }}> <Typography variant={"h2"} key={env.name} className="environment-name">
{/* <Typography component="span" variant={"h5"} key={env.name} sx={{ marginRight: "1rem" }}>
environment: environment:
</Typography> </Typography> */}
{env.name} {env.name}
</Typography> </Typography>
<TenantList tenants={env.tenants} /> <TenantList tenants={env.tenants} />
</Container>
</Box> </Box>
); );
}); });

View File

@ -5,7 +5,6 @@ import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent"; import CardContent from "@mui/material/CardContent";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Chip from "@mui/material/Chip";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
@ -22,24 +21,6 @@ function normalizeServiceName(name: string): string {
.replace(/\b\w/g, (s) => s.toUpperCase()); .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 { function getNodeNameElement(node: Node): React.ReactElement {
let statusMessage; let statusMessage;
const statusOk = node.health_check_status?.status_ok; const statusOk = node.health_check_status?.status_ok;
@ -69,7 +50,11 @@ function TenantsStatus({ statusPerTenant }: TenantsStatusProps) {
for (const [tenantId, statusOk] of Object.entries(statusPerTenant)) { for (const [tenantId, statusOk] of Object.entries(statusPerTenant)) {
statuses.push( 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} {tenantId}
</Typography> </Typography>
); );

View File

@ -2,13 +2,21 @@ import React from "react";
import { Service } from "../../types"; import { Service } from "../../types";
import Grid from "@mui/material/Unstable_Grid2"; import Grid from "@mui/material/Unstable_Grid2";
import ServiceCard from "./ServiceCard"; import ServiceCard from "./ServiceCard";
import { useGlobalState } from "../../GlobalStateProvider";
interface ServiceListProps { interface ServiceListProps {
services: Service[]; services: Service[];
} }
export default function ServiceList({ services }: ServiceListProps) { export default function ServiceList({ services }: ServiceListProps) {
const serviceItems = services.map((service) => { const { state } = useGlobalState();
const serviceItems = services
.filter(
(service) =>
!state.searchFilter || service.name.toLocaleLowerCase().includes(state.searchFilter.toLocaleLowerCase())
)
.map((service) => {
return ( return (
<Grid key={service.name} sx={{ display: "flex" }}> <Grid key={service.name} sx={{ display: "flex" }}>
<ServiceCard service={service} /> <ServiceCard service={service} />
@ -17,7 +25,7 @@ export default function ServiceList({ services }: ServiceListProps) {
}); });
return ( return (
<Grid container rowSpacing={2} columnSpacing={2} className="service-list" sx={{ marginBottom: "1rem" }}> <Grid container rowSpacing={2} columnSpacing={2} className="service-list">
{serviceItems} {serviceItems}
</Grid> </Grid>
); );

View File

@ -1,11 +1,6 @@
import React from "react"; import React from "react";
import Container from "@mui/material/Container";
import Dashboard from "../components/Dashboard"; import Dashboard from "../components/Dashboard";
export default function Home() { export default function Home() {
return ( return <Dashboard />;
<Container maxWidth={false} className="main-container">
<Dashboard />
</Container>
);
} }

View File

@ -1,5 +1,7 @@
@import "vars";
.app-header { .app-header {
background-color: #15232d; background-color: $header-background;
color: whitesmoke; color: whitesmoke;
padding: 1rem 0 1rem 0; padding: 1rem 0 1rem 0;
box-shadow: 0px 5px 11px 0px rgba(0, 0, 0, 0.31); box-shadow: 0px 5px 11px 0px rgba(0, 0, 0, 0.31);
@ -20,4 +22,9 @@
} }
} }
} }
.search-input {
background-color: whitesmoke;
margin-left: auto;
}
} }

View File

@ -0,0 +1,6 @@
@import "vars";
.env-tab-switcher {
width: 100%;
border-bottom: 1px solid lighten($header-background, 60%);
}

View File

@ -0,0 +1,19 @@
@import "vars";
.environment-list {
.environment {
background-color: lighten($header-background, 75%);
margin-bottom: 3rem;
padding-top: 2rem;
padding-bottom: 2rem;
&:last-child {
margin-bottom: 0;
}
.environment-name {
margin-bottom: 1rem;
color: lighten($header-background, 30%);
}
}
}

View File

@ -1,14 +1,11 @@
@use "sass:color"; @use "sass:color";
@import "vars";
$color-ok: #1dad24;
$color-warning: #ed6c02;
$color-danger: #d32f2f;
.service-card { .service-card {
width: 450px; width: 450px;
.service-title-container { .service-title-container {
color: white; color: white;
background-color: #15232d; background-color: lighten($header-background, 20%);
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;

View File

@ -0,0 +1,2 @@
.service-list {
}

View File

@ -0,0 +1,11 @@
.tenant-list {
.tenant {
margin-bottom: 2rem !important;
&:last-child {
margin-bottom: 0 !important;
}
.tenant-name {
}
}
}

5
src/scss/_vars.scss Normal file
View File

@ -0,0 +1,5 @@
$header-background: #15232d;
$color-ok: #1dad24;
$color-warning: #ed6c02;
$color-danger: #d32f2f;

View File

@ -1,4 +1,9 @@
@import "vars";
@import "common"; @import "common";
@import "app-header"; @import "app-header";
@import "linear-progress"; @import "linear-progress";
@import "env-tabs-switcher";
@import "environment-list";
@import "tenant-list";
@import "service-list";
@import "service-card"; @import "service-card";

View File

@ -1,4 +1,4 @@
import { createTheme } from "@mui/material"; import createTheme from "@mui/material/styles/createTheme";
const PRIMARY_COLOR = "#0d6efd"; const PRIMARY_COLOR = "#0d6efd";
const SECONDARY_COLOR = "#6c757d"; const SECONDARY_COLOR = "#6c757d";