diff --git a/web/ui/mantine-ui/src/App.tsx b/web/ui/mantine-ui/src/App.tsx index 54082741b5..78ff386bbf 100644 --- a/web/ui/mantine-ui/src/App.tsx +++ b/web/ui/mantine-ui/src/App.tsx @@ -62,6 +62,7 @@ import { Notifications } from "@mantine/notifications"; import { useAppDispatch } from "./state/hooks"; import { updateSettings, useSettings } from "./state/settingsSlice"; import SettingsMenu from "./components/SettingsMenu"; +import ReadinessWrapper from "./components/ReadinessWrapper"; const queryClient = new QueryClient(); @@ -379,15 +380,42 @@ function App() { } /> {agentMode ? ( - } /> + + + + } + /> ) : ( <> - } /> - } /> + + + + } + /> + + + + } + /> )} {allStatusPages.map((p) => ( - + {p.element} + } + /> ))} diff --git a/web/ui/mantine-ui/src/api/responseTypes/walreplay.ts b/web/ui/mantine-ui/src/api/responseTypes/walreplay.ts new file mode 100644 index 0000000000..b881ba750d --- /dev/null +++ b/web/ui/mantine-ui/src/api/responseTypes/walreplay.ts @@ -0,0 +1,7 @@ +// Result type for /api/v1/status/walreplay endpoint. +// See: https://prometheus.io/docs/prometheus/latest/querying/api/#wal-replay-stats +export interface WALReplayStatus { + min: number; + max: number; + current: number; +} diff --git a/web/ui/mantine-ui/src/components/ReadinessWrapper.tsx b/web/ui/mantine-ui/src/components/ReadinessWrapper.tsx new file mode 100644 index 0000000000..8bd46eaa4f --- /dev/null +++ b/web/ui/mantine-ui/src/components/ReadinessWrapper.tsx @@ -0,0 +1,92 @@ +import { FC, PropsWithChildren, useEffect, useState } from "react"; +import { useAppDispatch } from "../state/hooks"; +import { updateSettings, useSettings } from "../state/settingsSlice"; +import { useSuspenseAPIQuery } from "../api/api"; +import { WALReplayStatus } from "../api/responseTypes/walreplay"; +import { Progress, Stack, Title } from "@mantine/core"; +import { useSuspenseQuery } from "@tanstack/react-query"; + +const ReadinessLoader: FC = () => { + const dispatch = useAppDispatch(); + + // Query key is incremented every second to retrigger the status fetching. + const [queryKey, setQueryKey] = useState(0); + + // Query readiness status. + const { data: ready } = useSuspenseQuery({ + queryKey: [`ready-${queryKey}`], + retry: false, + refetchOnWindowFocus: false, + gcTime: 0, + queryFn: async ({ signal }: { signal: AbortSignal }) => { + try { + const res = await fetch("/-/ready", { + cache: "no-store", + credentials: "same-origin", + signal, + }); + switch (res.status) { + case 200: + return true; + case 503: + return false; + default: + throw new Error(res.statusText); + } + } catch (error) { + throw new Error("Unexpected error while fetching ready status"); + } + }, + }); + + // Query WAL replay status. + const { + data: { + data: { min, max, current }, + }, + } = useSuspenseAPIQuery({ + path: `/status/walreplay`, + key: `walreplay-${queryKey}`, + }); + + useEffect(() => { + if (ready) { + dispatch(updateSettings({ ready: ready })); + } + }, [ready, dispatch]); + + useEffect(() => { + const interval = setInterval(() => setQueryKey((v) => v + 1), 1000); + return () => clearInterval(interval); + }, []); + + return ( + + Starting up... + {max > 0 && ( + <> +

+ Replaying WAL ({current}/{max}) +

+ + + )} +
+ ); +}; + +export const ReadinessWrapper: FC = ({ children }) => { + const { ready } = useSettings(); + + if (ready) { + return <>{children}; + } + + return ; +}; + +export default ReadinessWrapper; diff --git a/web/ui/mantine-ui/vite.config.ts b/web/ui/mantine-ui/vite.config.ts index 0ed5aa400d..ca52ca1672 100644 --- a/web/ui/mantine-ui/vite.config.ts +++ b/web/ui/mantine-ui/vite.config.ts @@ -9,6 +9,9 @@ export default defineConfig({ "/api": { target: "http://localhost:9090", }, + "/-/": { + target: "http://localhost:9090", + }, }, }, });