mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-19 17:50:51 -08:00
b826c43987
Without this, the page that is shown first renders once with an empty path prefix value, since the settings update takes a render cycle to complete. However, we only fetch certain data from the API exactly once for a given page, and not for every re-render with changed path prefix value (and we also wouldn't want to fetch it from the wrong location initially). This duplicates the served endpoint list once more, but exporting them from App.tsx would also have been dirty (hot reload only works when a file only exports one component and nothing else, thus there'd be a linter warning). Signed-off-by: Julius Volz <julius.volz@gmail.com>
450 lines
13 KiB
TypeScript
450 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 "./mantine-overrides.css";
|
|
import classes from "./App.module.css";
|
|
import PrometheusLogo from "./images/prometheus-logo.svg";
|
|
|
|
import {
|
|
ActionIcon,
|
|
AppShell,
|
|
Box,
|
|
Burger,
|
|
Button,
|
|
Group,
|
|
MantineProvider,
|
|
Menu,
|
|
Skeleton,
|
|
Text,
|
|
createTheme,
|
|
rem,
|
|
} from "@mantine/core";
|
|
import { useDisclosure } from "@mantine/hooks";
|
|
import {
|
|
IconBell,
|
|
IconBellFilled,
|
|
IconBook,
|
|
IconChevronDown,
|
|
IconChevronRight,
|
|
IconCloudDataConnection,
|
|
IconDatabase,
|
|
IconDeviceDesktopAnalytics,
|
|
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 { useSettings } from "./state/settingsSlice";
|
|
import SettingsMenu from "./components/SettingsMenu";
|
|
import ReadinessWrapper from "./components/ReadinessWrapper";
|
|
import NotificationsProvider from "./components/NotificationsProvider";
|
|
import NotificationsIcon from "./components/NotificationsIcon";
|
|
import { QueryParamProvider } from "use-query-params";
|
|
import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6";
|
|
import ServiceDiscoveryPage from "./pages/service-discovery/ServiceDiscoveryPage";
|
|
import AlertmanagerDiscoveryPage from "./pages/AlertmanagerDiscoveryPage";
|
|
import { actionIconStyle, navIconStyle } from "./styles";
|
|
|
|
const queryClient = new QueryClient();
|
|
|
|
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,
|
|
},
|
|
{
|
|
title: "Alertmanager discovery",
|
|
path: "/alertmanager-discovery",
|
|
icon: <IconBell style={navIconStyle} />,
|
|
element: <AlertmanagerDiscoveryPage />,
|
|
inAgentMode: false,
|
|
},
|
|
];
|
|
|
|
const allStatusPages = [...monitoringStatusPages, ...serverStatusPages];
|
|
|
|
const theme = createTheme({
|
|
colors: {
|
|
"codebox-bg": [
|
|
"#f5f5f5",
|
|
"#e7e7e7",
|
|
"#cdcdcd",
|
|
"#b2b2b2",
|
|
"#9a9a9a",
|
|
"#8b8b8b",
|
|
"#848484",
|
|
"#717171",
|
|
"#656565",
|
|
"#575757",
|
|
],
|
|
},
|
|
});
|
|
|
|
const navLinkXPadding = "md";
|
|
|
|
function App() {
|
|
const [opened, { toggle }] = useDisclosure();
|
|
|
|
const { agentMode, consolesLink, pathPrefix } = useSettings();
|
|
|
|
const navLinks = (
|
|
<>
|
|
{consolesLink && (
|
|
<Button
|
|
component="a"
|
|
href={consolesLink}
|
|
className={classes.link}
|
|
leftSection={<IconDeviceDesktopAnalytics style={navIconStyle} />}
|
|
px={navLinkXPadding}
|
|
>
|
|
Consoles
|
|
</Button>
|
|
)}
|
|
|
|
{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>
|
|
</>
|
|
);
|
|
|
|
const navActionIcons = (
|
|
<>
|
|
<ThemeSelector />
|
|
<NotificationsIcon />
|
|
<SettingsMenu />
|
|
<ActionIcon
|
|
component="a"
|
|
href="https://prometheus.io/docs/prometheus/latest/getting_started/"
|
|
target="_blank"
|
|
color="gray"
|
|
title="Documentation"
|
|
aria-label="Documentation"
|
|
size={rem(32)}
|
|
>
|
|
<IconBook style={actionIconStyle} />
|
|
</ActionIcon>
|
|
</>
|
|
);
|
|
|
|
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"
|
|
>
|
|
<NotificationsProvider>
|
|
<AppShell.Header bg="rgb(65, 73, 81)" c="#fff">
|
|
<Group h="100%" px="md" wrap="nowrap">
|
|
<Group
|
|
style={{ flex: 1 }}
|
|
justify="space-between"
|
|
wrap="nowrap"
|
|
>
|
|
<Group gap={65} wrap="nowrap">
|
|
<Link
|
|
to="/"
|
|
style={{ textDecoration: "none", color: "white" }}
|
|
>
|
|
<Group gap={10} wrap="nowrap">
|
|
<img src={PrometheusLogo} height={30} />
|
|
<Text fz={20}>Prometheus{agentMode && " Agent"}</Text>
|
|
</Group>
|
|
</Link>
|
|
<Group gap={12} visibleFrom="sm" wrap="nowrap">
|
|
{navLinks}
|
|
</Group>
|
|
</Group>
|
|
<Group visibleFrom="xs" wrap="nowrap" gap="xs">
|
|
{navActionIcons}
|
|
</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">
|
|
{navActionIcons}
|
|
</Group>
|
|
</AppShell.Navbar>
|
|
</NotificationsProvider>
|
|
|
|
<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;
|