prometheus/web/ui/mantine-ui/src/App.tsx
Julius Volz d22e721d39 Add initial Service Discovery page
Signed-off-by: Julius Volz <julius.volz@gmail.com>
2024-08-28 14:56:35 +02:00

435 lines
13 KiB
TypeScript

import "@mantine/core/styles.css";
import "@mantine/code-highlight/styles.css";
import "@mantine/notifications/styles.css";
import "@mantine/dates/styles.css";
import classes from "./App.module.css";
import PrometheusLogo from "./images/prometheus-logo.svg";
import {
AppShell,
Box,
Burger,
Button,
Group,
MantineProvider,
Menu,
Skeleton,
Text,
createTheme,
rem,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import {
IconBellFilled,
IconChevronDown,
IconChevronRight,
IconCloudDataConnection,
IconDatabase,
IconFlag,
IconHeartRateMonitor,
IconInfoCircle,
IconSearch,
IconServer,
IconServerCog,
} from "@tabler/icons-react";
import {
BrowserRouter,
Link,
NavLink,
Navigate,
Route,
Routes,
} from "react-router-dom";
import { IconTable } from "@tabler/icons-react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import QueryPage from "./pages/query/QueryPage";
import AlertsPage from "./pages/AlertsPage";
import RulesPage from "./pages/RulesPage";
import TargetsPage from "./pages/targets/TargetsPage";
import StatusPage from "./pages/StatusPage";
import TSDBStatusPage from "./pages/TSDBStatusPage";
import FlagsPage from "./pages/FlagsPage";
import ConfigPage from "./pages/ConfigPage";
import AgentPage from "./pages/AgentPage";
import { Suspense } from "react";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeSelector } from "./components/ThemeSelector";
import { Notifications } from "@mantine/notifications";
import { useAppDispatch } from "./state/hooks";
import { updateSettings, useSettings } from "./state/settingsSlice";
import SettingsMenu from "./components/SettingsMenu";
import ReadinessWrapper from "./components/ReadinessWrapper";
import { QueryParamProvider } from "use-query-params";
import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6";
import ServiceDiscoveryPage from "./pages/service-discovery/ServiceDiscoveryPage";
const queryClient = new QueryClient();
const navIconStyle = { width: rem(16), height: rem(16) };
const mainNavPages = [
{
title: "Query",
path: "/query",
icon: <IconSearch style={navIconStyle} />,
element: <QueryPage />,
inAgentMode: false,
},
{
title: "Alerts",
path: "/alerts",
icon: <IconBellFilled style={navIconStyle} />,
element: <AlertsPage />,
inAgentMode: false,
},
];
const monitoringStatusPages = [
{
title: "Target health",
path: "/targets",
icon: <IconHeartRateMonitor style={navIconStyle} />,
element: <TargetsPage />,
inAgentMode: true,
},
{
title: "Rule health",
path: "/rules",
icon: <IconTable style={navIconStyle} />,
element: <RulesPage />,
inAgentMode: false,
},
{
title: "Service discovery",
path: "/service-discovery",
icon: <IconCloudDataConnection style={navIconStyle} />,
element: <ServiceDiscoveryPage />,
inAgentMode: true,
},
];
const serverStatusPages = [
{
title: "Runtime & build information",
path: "/status",
icon: <IconInfoCircle style={navIconStyle} />,
element: <StatusPage />,
inAgentMode: true,
},
{
title: "TSDB status",
path: "/tsdb-status",
icon: <IconDatabase style={navIconStyle} />,
element: <TSDBStatusPage />,
inAgentMode: false,
},
{
title: "Command-line flags",
path: "/flags",
icon: <IconFlag style={navIconStyle} />,
element: <FlagsPage />,
inAgentMode: true,
},
{
title: "Configuration",
path: "/config",
icon: <IconServerCog style={navIconStyle} />,
element: <ConfigPage />,
inAgentMode: true,
},
];
const allStatusPages = [...monitoringStatusPages, ...serverStatusPages];
const theme = createTheme({
colors: {
"codebox-bg": [
"#f5f5f5",
"#e7e7e7",
"#cdcdcd",
"#b2b2b2",
"#9a9a9a",
"#8b8b8b",
"#848484",
"#717171",
"#656565",
"#575757",
],
},
});
// This dynamically/generically determines the pathPrefix by stripping the first known
// endpoint suffix from the window location path. It works out of the box for both direct
// hosting and reverse proxy deployments with no additional configurations required.
const getPathPrefix = (path: string) => {
if (path.endsWith("/")) {
path = path.slice(0, -1);
}
const pagePaths = [
...mainNavPages,
...allStatusPages,
{ path: "/agent" },
].map((p) => p.path);
const pagePath = pagePaths.find((p) => path.endsWith(p));
return path.slice(0, path.length - (pagePath || "").length);
};
const navLinkXPadding = "md";
function App() {
const [opened, { toggle }] = useDisclosure();
const pathPrefix = getPathPrefix(window.location.pathname);
const dispatch = useAppDispatch();
dispatch(updateSettings({ pathPrefix }));
const { agentMode } = useSettings();
const navLinks = (
<>
{mainNavPages
.filter((p) => !agentMode || p.inAgentMode)
.map((p) => (
<Button
key={p.path}
component={NavLink}
to={p.path}
className={classes.link}
leftSection={p.icon}
px={navLinkXPadding}
>
{p.title}
</Button>
))}
<Menu shadow="md" width={240}>
<Routes>
{allStatusPages
.filter((p) => !agentMode || p.inAgentMode)
.map((p) => (
<Route
key={p.path}
path={p.path}
element={
<Menu.Target>
<Button
component={NavLink}
to={p.path}
className={classes.link}
leftSection={p.icon}
rightSection={<IconChevronDown style={navIconStyle} />}
px={navLinkXPadding}
>
Status <IconChevronRight style={navIconStyle} /> {p.title}
</Button>
</Menu.Target>
}
/>
))}
<Route
path="*"
element={
<Menu.Target>
<Button
component={NavLink}
to="/"
className={classes.link}
leftSection={<IconServer style={navIconStyle} />}
rightSection={<IconChevronDown style={navIconStyle} />}
onClick={(e) => {
e.preventDefault();
}}
px={navLinkXPadding}
>
Status
</Button>
</Menu.Target>
}
/>
</Routes>
<Menu.Dropdown>
<Menu.Label>Monitoring status</Menu.Label>
{monitoringStatusPages
.filter((p) => !agentMode || p.inAgentMode)
.map((p) => (
<Menu.Item
key={p.path}
component={NavLink}
to={p.path}
leftSection={p.icon}
>
{p.title}
</Menu.Item>
))}
<Menu.Divider />
<Menu.Label>Server status</Menu.Label>
{serverStatusPages
.filter((p) => !agentMode || p.inAgentMode)
.map((p) => (
<Menu.Item
key={p.path}
component={NavLink}
to={p.path}
leftSection={p.icon}
>
{p.title}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
{/* <Button
component="a"
href="https://prometheus.io/docs/prometheus/latest/getting_started/"
className={classes.link}
leftSection={<IconHelp style={navIconStyle} />}
target="_blank"
px={navLinkXPadding}
>
Help
</Button> */}
</>
);
return (
<BrowserRouter basename={pathPrefix}>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<MantineProvider defaultColorScheme="auto" theme={theme}>
<Notifications position="top-right" />
<QueryClientProvider client={queryClient}>
<AppShell
header={{ height: 56 }}
navbar={{
width: 300,
// TODO: On pages with a long title like "/status", the navbar
// breaks in an ugly way for narrow windows. Fix this.
breakpoint: "sm",
collapsed: { desktop: true, mobile: !opened },
}}
padding="md"
>
<AppShell.Header bg="rgb(65, 73, 81)" c="#fff">
<Group h="100%" px="md">
<Group style={{ flex: 1 }} justify="space-between">
<Group gap={65}>
<Link
to="/"
style={{ textDecoration: "none", color: "white" }}
>
<Group gap={10}>
<img src={PrometheusLogo} height={30} />
<Text fz={20}>Prometheus{agentMode && " Agent"}</Text>
</Group>
</Link>
<Group gap={12} visibleFrom="sm">
{navLinks}
</Group>
</Group>
<Group visibleFrom="xs">
<ThemeSelector />
<SettingsMenu />
</Group>
</Group>
<Burger
opened={opened}
onClick={toggle}
hiddenFrom="sm"
size="sm"
color="gray.2"
/>
</Group>
</AppShell.Header>
<AppShell.Navbar py="md" px={4} bg="rgb(65, 73, 81)" c="#fff">
{navLinks}
<Group mt="md" hiddenFrom="xs" justify="center">
<ThemeSelector />
<SettingsMenu />
</Group>
</AppShell.Navbar>
<AppShell.Main>
<ErrorBoundary key={location.pathname}>
<Suspense
fallback={
<Box mt="lg">
{Array.from(Array(10), (_, i) => (
<Skeleton
key={i}
height={40}
mb={15}
width={1000}
mx="auto"
/>
))}
</Box>
}
>
<Routes>
<Route
path="/"
element={
<Navigate
to={agentMode ? "/agent" : "/query"}
replace
/>
}
/>
{agentMode ? (
<Route
path="/agent"
element={
<ReadinessWrapper>
<AgentPage />
</ReadinessWrapper>
}
/>
) : (
<>
<Route
path="/query"
element={
<ReadinessWrapper>
<QueryPage />
</ReadinessWrapper>
}
/>
<Route
path="/alerts"
element={
<ReadinessWrapper>
<AlertsPage />
</ReadinessWrapper>
}
/>
</>
)}
{allStatusPages.map((p) => (
<Route
key={p.path}
path={p.path}
element={
<ReadinessWrapper>{p.element}</ReadinessWrapper>
}
/>
))}
</Routes>
</Suspense>
</ErrorBoundary>
</AppShell.Main>
</AppShell>
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
</QueryClientProvider>
</MantineProvider>
</QueryParamProvider>
</BrowserRouter>
);
}
export default App;