mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-12 06:17:27 -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 { SettingsContext } from "./settings";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import { useAppDispatch } from "./state/hooks";
|
||||
import { updateSettings } from "./state/settingsSlice";
|
||||
|
||||
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 = [
|
||||
{
|
||||
title: "Targets",
|
||||
|
@ -114,6 +131,7 @@ const serverStatusPages = [
|
|||
];
|
||||
|
||||
const allStatusPages = [...monitoringStatusPages, ...serverStatusPages];
|
||||
const allPages = [...mainNavPages, ...allStatusPages];
|
||||
|
||||
const theme = createTheme({
|
||||
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 navLinkXPadding = "md";
|
||||
|
||||
|
@ -139,26 +172,24 @@ function App() {
|
|||
const [opened, { toggle }] = useDisclosure();
|
||||
const { agentMode } = useContext(SettingsContext);
|
||||
|
||||
const pathPrefix = getPathPrefix(window.location.pathname);
|
||||
const dispatch = useAppDispatch();
|
||||
dispatch(updateSettings({ pathPrefix }));
|
||||
|
||||
const navLinks = (
|
||||
<>
|
||||
<Button
|
||||
component={NavLink}
|
||||
to="/query"
|
||||
className={classes.link}
|
||||
leftSection={<IconDatabaseSearch size={navLinkIconSize} />}
|
||||
px={navLinkXPadding}
|
||||
>
|
||||
Query
|
||||
</Button>
|
||||
<Button
|
||||
component={NavLink}
|
||||
to="/alerts"
|
||||
className={classes.link}
|
||||
leftSection={<IconBellFilled size={navLinkIconSize} />}
|
||||
px={navLinkXPadding}
|
||||
>
|
||||
Alerts
|
||||
</Button>
|
||||
{mainNavPages.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={230}>
|
||||
<Routes>
|
||||
|
@ -246,7 +277,7 @@ function App() {
|
|||
);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<BrowserRouter basename={pathPrefix}>
|
||||
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
||||
<Notifications position="top-right" />
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useAppSelector } from "../state/hooks";
|
||||
|
||||
export const API_PATH = "api/v1";
|
||||
|
||||
|
@ -17,18 +18,29 @@ export type ErrorAPIResponse = {
|
|||
export type APIResponse<T> = SuccessAPIResponse<T> | ErrorAPIResponse;
|
||||
|
||||
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 }) => {
|
||||
const queryString = params
|
||||
? `?${new URLSearchParams(params).toString()}`
|
||||
: "";
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${API_PATH}/${path}${queryString}`, {
|
||||
cache: "no-store",
|
||||
credentials: "same-origin",
|
||||
signal,
|
||||
});
|
||||
const res = await fetch(
|
||||
`${pathPrefix}/${API_PATH}${path}${queryString}`,
|
||||
{
|
||||
cache: "no-store",
|
||||
credentials: "same-origin",
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
!res.ok &&
|
||||
|
@ -74,21 +86,32 @@ type QueryOptions = {
|
|||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const useAPIQuery = <T>({ key, path, params, enabled }: QueryOptions) =>
|
||||
useQuery<SuccessAPIResponse<T>>({
|
||||
export const useAPIQuery = <T>({
|
||||
key,
|
||||
path,
|
||||
params,
|
||||
enabled,
|
||||
}: QueryOptions) => {
|
||||
const pathPrefix = useAppSelector((state) => state.settings.pathPrefix);
|
||||
|
||||
return useQuery<SuccessAPIResponse<T>>({
|
||||
queryKey: key ? [key] : [path, params],
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 0,
|
||||
enabled,
|
||||
queryFn: createQueryFn({ path, params }),
|
||||
queryFn: createQueryFn({ pathPrefix, path, params }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSuspenseAPIQuery = <T>({ key, path, params }: QueryOptions) =>
|
||||
useSuspenseQuery<SuccessAPIResponse<T>>({
|
||||
export const useSuspenseAPIQuery = <T>({ key, path, params }: QueryOptions) => {
|
||||
const pathPrefix = useAppSelector((state) => state.settings.pathPrefix);
|
||||
|
||||
return useSuspenseQuery<SuccessAPIResponse<T>>({
|
||||
queryKey: key ? [key] : [path, params],
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
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 queryPageSlice from "./queryPageSlice";
|
||||
import { prometheusApi } from "./api";
|
||||
import settingsSlice from "./settingsSlice";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
settings: settingsSlice,
|
||||
queryPage: queryPageSlice,
|
||||
[prometheusApi.reducerPath]: prometheusApi.reducer,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue