From bb7ff1aa05867fcbe878a5e310b90d72b2cf2ece Mon Sep 17 00:00:00 2001 From: Vijay Govindarajan Date: Sat, 28 Mar 2026 16:21:06 -0700 Subject: [PATCH] ui: add delete series page Adds a web UI page for the delete_series and clean_tombstones admin APIs, making it easier to manage time series data without using curl commands directly. The page provides: - A form to specify PromQL series selectors for deletion - Optional start/end time range filters - A clean tombstones button to reclaim disk space after deletion - Warning and confirmation feedback Fixes #17010 Signed-off-by: Vijay Govindarajan --- web/ui/mantine-ui/src/App.tsx | 9 + .../mantine-ui/src/pages/DeleteSeriesPage.tsx | 205 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 web/ui/mantine-ui/src/pages/DeleteSeriesPage.tsx diff --git a/web/ui/mantine-ui/src/App.tsx b/web/ui/mantine-ui/src/App.tsx index 44e4224d10..98d5dfd308 100644 --- a/web/ui/mantine-ui/src/App.tsx +++ b/web/ui/mantine-ui/src/App.tsx @@ -36,6 +36,7 @@ import { IconSearch, IconServer, IconServerCog, + IconTrash, } from "@tabler/icons-react"; import { BrowserRouter, @@ -56,6 +57,7 @@ import TSDBStatusPage from "./pages/TSDBStatusPage"; import FlagsPage from "./pages/FlagsPage"; import ConfigPage from "./pages/ConfigPage"; import AgentPage from "./pages/AgentPage"; +import DeleteSeriesPage from "./pages/DeleteSeriesPage"; import { Suspense } from "react"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeSelector } from "./components/ThemeSelector"; @@ -162,6 +164,13 @@ const serverStatusPages = [ element: , inAgentMode: false, }, + { + title: "Delete series", + path: "/delete-series", + icon: , + element: , + inAgentMode: false, + }, ]; const allStatusPages = [...monitoringStatusPages, ...serverStatusPages]; diff --git a/web/ui/mantine-ui/src/pages/DeleteSeriesPage.tsx b/web/ui/mantine-ui/src/pages/DeleteSeriesPage.tsx new file mode 100644 index 0000000000..49556056ae --- /dev/null +++ b/web/ui/mantine-ui/src/pages/DeleteSeriesPage.tsx @@ -0,0 +1,205 @@ +import { useState } from "react"; +import { + TextInput, + Button, + Group, + Alert, + Text, + Stack, +} from "@mantine/core"; +import { IconAlertTriangle, IconCheck, IconTrash } from "@tabler/icons-react"; +import { useSettings } from "../state/settingsSlice"; +import { API_PATH } from "../api/api"; +import InfoPageStack from "../components/InfoPageStack"; +import InfoPageCard from "../components/InfoPageCard"; + +export default function DeleteSeriesPage() { + const { pathPrefix } = useSettings(); + + const [matchers, setMatchers] = useState(""); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [deleting, setDeleting] = useState(false); + const [cleaning, setCleaning] = useState(false); + + const handleDelete = async () => { + setError(null); + setSuccess(null); + + const matchList = matchers + .split("\n") + .map((m) => m.trim()) + .filter((m) => m !== ""); + + if (matchList.length === 0) { + setError("Provide at least one match[] selector."); + return; + } + + const params = new URLSearchParams(); + for (const m of matchList) { + params.append("match[]", m); + } + if (startTime) { + params.append("start", startTime); + } + if (endTime) { + params.append("end", endTime); + } + + setDeleting(true); + try { + const res = await fetch( + `${pathPrefix}/${API_PATH}/admin/tsdb/delete_series?${params.toString()}`, + { + method: "POST", + credentials: "same-origin", + } + ); + + if (!res.ok) { + if (res.headers.get("content-type")?.startsWith("application/json")) { + const body = await res.json(); + throw new Error(body.error || res.statusText); + } + throw new Error(res.statusText); + } + + setSuccess( + `Successfully deleted series matching: ${matchList.join(", ")}` + ); + setMatchers(""); + setStartTime(""); + setEndTime(""); + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setDeleting(false); + } + }; + + const handleCleanTombstones = async () => { + setError(null); + setSuccess(null); + setCleaning(true); + + try { + const res = await fetch( + `${pathPrefix}/${API_PATH}/admin/tsdb/clean_tombstones`, + { + method: "POST", + credentials: "same-origin", + } + ); + + if (!res.ok) { + if (res.headers.get("content-type")?.startsWith("application/json")) { + const body = await res.json(); + throw new Error(body.error || res.statusText); + } + throw new Error(res.statusText); + } + + setSuccess("Tombstones cleaned successfully."); + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setCleaning(false); + } + }; + + return ( + + + + } + color="yellow" + title="Warning" + > + This operation marks matching series for deletion. Deleted data + cannot be recovered. Use Clean Tombstones afterwards to reclaim disk + space. + + + {error && ( + setError(null)}> + {error} + + )} + + {success && ( + } + color="green" + title="Success" + withCloseButton + onClose={() => setSuccess(null)} + > + {success} + + )} + + setMatchers(e.currentTarget.value)} + required + /> + + + setStartTime(e.currentTarget.value)} + /> + setEndTime(e.currentTarget.value)} + /> + + + + + + + + + + + + After deleting series, tombstones mark the data for deletion but + do not free disk space immediately. Use this to remove tombstones + and reclaim storage. + + + + + + + + ); +}