Compare commits
3 Commits
858f42c39b
...
0c86009271
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c86009271 | |||
| 03bba20b50 | |||
| b1f7d8196a |
15
src/App.tsx
15
src/App.tsx
@ -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}>
|
||||||
<AppHeader />
|
<GlobalStateProvider>
|
||||||
<Routes>
|
<AppHeader />
|
||||||
<Route path="/" element={<Home />} />
|
<Routes>
|
||||||
{/* <Route path="*" element={<NotFound404 />} /> */}
|
<Route path="/" element={<Home />} />
|
||||||
</Routes>
|
{/* <Route path="*" element={<NotFound404 />} /> */}
|
||||||
|
</Routes>
|
||||||
|
</GlobalStateProvider>
|
||||||
</ThemeProvider>
|
</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 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>
|
|
||||||
</Stack>
|
<TextField
|
||||||
</Container>
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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">
|
||||||
environment:
|
{/* <Typography component="span" variant={"h5"} key={env.name} sx={{ marginRight: "1rem" }}>
|
||||||
|
environment:
|
||||||
|
</Typography> */}
|
||||||
|
{env.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
{env.name}
|
<TenantList tenants={env.tenants} />
|
||||||
</Typography>
|
</Container>
|
||||||
<TenantList tenants={env.tenants} />
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,22 +2,30 @@ 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();
|
||||||
return (
|
|
||||||
<Grid key={service.name} sx={{ display: "flex" }}>
|
const serviceItems = services
|
||||||
<ServiceCard service={service} />
|
.filter(
|
||||||
</Grid>
|
(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 (
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/scss/_env-tabs-switcher.scss
Normal file
6
src/scss/_env-tabs-switcher.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@import "vars";
|
||||||
|
|
||||||
|
.env-tab-switcher {
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid lighten($header-background, 60%);
|
||||||
|
}
|
||||||
19
src/scss/_environment-list.scss
Normal file
19
src/scss/_environment-list.scss
Normal 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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
2
src/scss/_service-list.scss
Normal file
2
src/scss/_service-list.scss
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.service-list {
|
||||||
|
}
|
||||||
11
src/scss/_tenant-list.scss
Normal file
11
src/scss/_tenant-list.scss
Normal 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
5
src/scss/_vars.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
$header-background: #15232d;
|
||||||
|
|
||||||
|
$color-ok: #1dad24;
|
||||||
|
$color-warning: #ed6c02;
|
||||||
|
$color-danger: #d32f2f;
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user