From 33a753c2f81177a6b6b22d023ded050d4f4da7e7 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 8 Mar 2024 13:38:11 +0100 Subject: [PATCH] Implement pathPrefix handling Signed-off-by: Julius Volz --- web/ui/mantine-ui/src/App.tsx | 69 ++++++++++++++------ web/ui/mantine-ui/src/api/api.ts | 47 +++++++++---- web/ui/mantine-ui/src/state/settingsSlice.ts | 23 +++++++ web/ui/mantine-ui/src/state/store.ts | 2 + 4 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 web/ui/mantine-ui/src/state/settingsSlice.ts diff --git a/web/ui/mantine-ui/src/App.tsx b/web/ui/mantine-ui/src/App.tsx index f5cbcd3bf..ae8e08899 100644 --- a/web/ui/mantine-ui/src/App.tsx +++ b/web/ui/mantine-ui/src/App.tsx @@ -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: , + element: , + }, + { + title: "Alerts", + path: "/alerts", + icon: , + element: , + }, +]; + 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 = ( <> - - + {mainNavPages.map((p) => ( + + ))} @@ -246,7 +277,7 @@ function App() { ); return ( - + diff --git a/web/ui/mantine-ui/src/api/api.ts b/web/ui/mantine-ui/src/api/api.ts index b1db71f73..2cc18029f 100644 --- a/web/ui/mantine-ui/src/api/api.ts +++ b/web/ui/mantine-ui/src/api/api.ts @@ -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 = SuccessAPIResponse | ErrorAPIResponse; const createQueryFn = - ({ path, params }: { path: string; params?: Record }) => + ({ + pathPrefix, + path, + params, + }: { + pathPrefix: string; + path: string; + params?: Record; + }) => 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 = ({ key, path, params, enabled }: QueryOptions) => - useQuery>({ +export const useAPIQuery = ({ + key, + path, + params, + enabled, +}: QueryOptions) => { + const pathPrefix = useAppSelector((state) => state.settings.pathPrefix); + + return useQuery>({ queryKey: key ? [key] : [path, params], retry: false, refetchOnWindowFocus: false, gcTime: 0, enabled, - queryFn: createQueryFn({ path, params }), + queryFn: createQueryFn({ pathPrefix, path, params }), }); +}; -export const useSuspenseAPIQuery = ({ key, path, params }: QueryOptions) => - useSuspenseQuery>({ +export const useSuspenseAPIQuery = ({ key, path, params }: QueryOptions) => { + const pathPrefix = useAppSelector((state) => state.settings.pathPrefix); + + return useSuspenseQuery>({ queryKey: key ? [key] : [path, params], retry: false, refetchOnWindowFocus: false, gcTime: 0, - queryFn: createQueryFn({ path, params }), + queryFn: createQueryFn({ pathPrefix, path, params }), }); +}; diff --git a/web/ui/mantine-ui/src/state/settingsSlice.ts b/web/ui/mantine-ui/src/state/settingsSlice.ts new file mode 100644 index 000000000..9c979f558 --- /dev/null +++ b/web/ui/mantine-ui/src/state/settingsSlice.ts @@ -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>) => { + Object.assign(state, payload); + }, + }, +}); + +export const { updateSettings } = settingsSlice.actions; + +export default settingsSlice.reducer; diff --git a/web/ui/mantine-ui/src/state/store.ts b/web/ui/mantine-ui/src/state/store.ts index fe16347ea..d2d2dcb68 100644 --- a/web/ui/mantine-ui/src/state/store.ts +++ b/web/ui/mantine-ui/src/state/store.ts @@ -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, },