mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Implement pathPrefix handling
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
5fea050fed
commit
33a753c2f8
|
@ -60,9 +60,26 @@ import ErrorBoundary from "./ErrorBoundary";
|
||||||
import { ThemeSelector } from "./ThemeSelector";
|
import { ThemeSelector } from "./ThemeSelector";
|
||||||
import { SettingsContext } from "./settings";
|
import { SettingsContext } from "./settings";
|
||||||
import { Notifications } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
import { useAppDispatch } from "./state/hooks";
|
||||||
|
import { updateSettings } from "./state/settingsSlice";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const mainNavPages = [
|
||||||
|
{
|
||||||
|
title: "Query",
|
||||||
|
path: "/query",
|
||||||
|
icon: <IconDatabaseSearch style={{ width: rem(14), height: rem(14) }} />,
|
||||||
|
element: <QueryPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Alerts",
|
||||||
|
path: "/alerts",
|
||||||
|
icon: <IconBellFilled style={{ width: rem(14), height: rem(14) }} />,
|
||||||
|
element: <AlertsPage />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const monitoringStatusPages = [
|
const monitoringStatusPages = [
|
||||||
{
|
{
|
||||||
title: "Targets",
|
title: "Targets",
|
||||||
|
@ -114,6 +131,7 @@ const serverStatusPages = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const allStatusPages = [...monitoringStatusPages, ...serverStatusPages];
|
const allStatusPages = [...monitoringStatusPages, ...serverStatusPages];
|
||||||
|
const allPages = [...mainNavPages, ...allStatusPages];
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
colors: {
|
colors: {
|
||||||
|
@ -132,6 +150,21 @@ const theme = createTheme({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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 pagePath = allPages.find((p) => path.endsWith(p.path))?.path;
|
||||||
|
if (pagePath === undefined) {
|
||||||
|
throw new Error(`Could not find base path for ${path}`);
|
||||||
|
}
|
||||||
|
return path.slice(0, path.length - pagePath.length);
|
||||||
|
};
|
||||||
|
|
||||||
const navLinkIconSize = 15;
|
const navLinkIconSize = 15;
|
||||||
const navLinkXPadding = "md";
|
const navLinkXPadding = "md";
|
||||||
|
|
||||||
|
@ -139,26 +172,24 @@ function App() {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const [opened, { toggle }] = useDisclosure();
|
||||||
const { agentMode } = useContext(SettingsContext);
|
const { agentMode } = useContext(SettingsContext);
|
||||||
|
|
||||||
|
const pathPrefix = getPathPrefix(window.location.pathname);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
dispatch(updateSettings({ pathPrefix }));
|
||||||
|
|
||||||
const navLinks = (
|
const navLinks = (
|
||||||
<>
|
<>
|
||||||
<Button
|
{mainNavPages.map((p) => (
|
||||||
component={NavLink}
|
<Button
|
||||||
to="/query"
|
key={p.path}
|
||||||
className={classes.link}
|
component={NavLink}
|
||||||
leftSection={<IconDatabaseSearch size={navLinkIconSize} />}
|
to={p.path}
|
||||||
px={navLinkXPadding}
|
className={classes.link}
|
||||||
>
|
leftSection={p.icon}
|
||||||
Query
|
px={navLinkXPadding}
|
||||||
</Button>
|
>
|
||||||
<Button
|
{p.title}
|
||||||
component={NavLink}
|
</Button>
|
||||||
to="/alerts"
|
))}
|
||||||
className={classes.link}
|
|
||||||
leftSection={<IconBellFilled size={navLinkIconSize} />}
|
|
||||||
px={navLinkXPadding}
|
|
||||||
>
|
|
||||||
Alerts
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Menu shadow="md" width={230}>
|
<Menu shadow="md" width={230}>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
@ -246,7 +277,7 @@ function App() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter basename={pathPrefix}>
|
||||||
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
||||||
<Notifications position="top-right" />
|
<Notifications position="top-right" />
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
import { useAppSelector } from "../state/hooks";
|
||||||
|
|
||||||
export const API_PATH = "api/v1";
|
export const API_PATH = "api/v1";
|
||||||
|
|
||||||
|
@ -17,18 +18,29 @@ export type ErrorAPIResponse = {
|
||||||
export type APIResponse<T> = SuccessAPIResponse<T> | ErrorAPIResponse;
|
export type APIResponse<T> = SuccessAPIResponse<T> | ErrorAPIResponse;
|
||||||
|
|
||||||
const createQueryFn =
|
const createQueryFn =
|
||||||
<T>({ path, params }: { path: string; params?: Record<string, string> }) =>
|
<T>({
|
||||||
|
pathPrefix,
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
pathPrefix: string;
|
||||||
|
path: string;
|
||||||
|
params?: Record<string, string>;
|
||||||
|
}) =>
|
||||||
async ({ signal }: { signal: AbortSignal }) => {
|
async ({ signal }: { signal: AbortSignal }) => {
|
||||||
const queryString = params
|
const queryString = params
|
||||||
? `?${new URLSearchParams(params).toString()}`
|
? `?${new URLSearchParams(params).toString()}`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/${API_PATH}/${path}${queryString}`, {
|
const res = await fetch(
|
||||||
cache: "no-store",
|
`${pathPrefix}/${API_PATH}${path}${queryString}`,
|
||||||
credentials: "same-origin",
|
{
|
||||||
signal,
|
cache: "no-store",
|
||||||
});
|
credentials: "same-origin",
|
||||||
|
signal,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!res.ok &&
|
!res.ok &&
|
||||||
|
@ -74,21 +86,32 @@ type QueryOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAPIQuery = <T>({ key, path, params, enabled }: QueryOptions) =>
|
export const useAPIQuery = <T>({
|
||||||
useQuery<SuccessAPIResponse<T>>({
|
key,
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
enabled,
|
||||||
|
}: QueryOptions) => {
|
||||||
|
const pathPrefix = useAppSelector((state) => state.settings.pathPrefix);
|
||||||
|
|
||||||
|
return useQuery<SuccessAPIResponse<T>>({
|
||||||
queryKey: key ? [key] : [path, params],
|
queryKey: key ? [key] : [path, params],
|
||||||
retry: false,
|
retry: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
enabled,
|
enabled,
|
||||||
queryFn: createQueryFn({ path, params }),
|
queryFn: createQueryFn({ pathPrefix, path, params }),
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useSuspenseAPIQuery = <T>({ key, path, params }: QueryOptions) =>
|
export const useSuspenseAPIQuery = <T>({ key, path, params }: QueryOptions) => {
|
||||||
useSuspenseQuery<SuccessAPIResponse<T>>({
|
const pathPrefix = useAppSelector((state) => state.settings.pathPrefix);
|
||||||
|
|
||||||
|
return useSuspenseQuery<SuccessAPIResponse<T>>({
|
||||||
queryKey: key ? [key] : [path, params],
|
queryKey: key ? [key] : [path, params],
|
||||||
retry: false,
|
retry: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
queryFn: createQueryFn({ path, params }),
|
queryFn: createQueryFn({ pathPrefix, path, params }),
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
23
web/ui/mantine-ui/src/state/settingsSlice.ts
Normal file
23
web/ui/mantine-ui/src/state/settingsSlice.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
interface Settings {
|
||||||
|
pathPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: Settings = {
|
||||||
|
pathPrefix: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const settingsSlice = createSlice({
|
||||||
|
name: "settings",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
updateSettings: (state, { payload }: PayloadAction<Partial<Settings>>) => {
|
||||||
|
Object.assign(state, payload);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { updateSettings } = settingsSlice.actions;
|
||||||
|
|
||||||
|
export default settingsSlice.reducer;
|
|
@ -1,9 +1,11 @@
|
||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import queryPageSlice from "./queryPageSlice";
|
import queryPageSlice from "./queryPageSlice";
|
||||||
import { prometheusApi } from "./api";
|
import { prometheusApi } from "./api";
|
||||||
|
import settingsSlice from "./settingsSlice";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
|
settings: settingsSlice,
|
||||||
queryPage: queryPageSlice,
|
queryPage: queryPageSlice,
|
||||||
[prometheusApi.reducerPath]: prometheusApi.reducer,
|
[prometheusApi.reducerPath]: prometheusApi.reducer,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue