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)}
+ />
+
+
+
+ }
+ onClick={handleDelete}
+ loading={deleting}
+ disabled={!matchers.trim()}
+ >
+ Delete series
+
+
+
+
+
+
+
+
+ After deleting series, tombstones mark the data for deletion but
+ do not free disk space immediately. Use this to remove tombstones
+ and reclaim storage.
+
+
+
+
+
+
+
+ );
+}