prometheus/web/ui/mantine-ui/src/App.tsx
Julius Volz b826c43987
Calculate path prefix directly in initial settings Redux value (#14981)
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>
2024-09-30 14:22:40 +02:00

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;