mirror of
https://github.com/prometheus/prometheus.git
synced 2026-05-05 04:16:15 +02:00
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 <vijay.govindarajan91@gmail.com>
This commit is contained in:
parent
cb3382314d
commit
bb7ff1aa05
@ -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: <AlertmanagerDiscoveryPage />,
|
||||
inAgentMode: false,
|
||||
},
|
||||
{
|
||||
title: "Delete series",
|
||||
path: "/delete-series",
|
||||
icon: <IconTrash style={navIconStyle} />,
|
||||
element: <DeleteSeriesPage />,
|
||||
inAgentMode: false,
|
||||
},
|
||||
];
|
||||
|
||||
const allStatusPages = [...monitoringStatusPages, ...serverStatusPages];
|
||||
|
||||
205
web/ui/mantine-ui/src/pages/DeleteSeriesPage.tsx
Normal file
205
web/ui/mantine-ui/src/pages/DeleteSeriesPage.tsx
Normal file
@ -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<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(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 (
|
||||
<InfoPageStack>
|
||||
<InfoPageCard title="Delete Series" icon={IconTrash}>
|
||||
<Stack gap="md">
|
||||
<Alert
|
||||
icon={<IconAlertTriangle size={16} />}
|
||||
color="yellow"
|
||||
title="Warning"
|
||||
>
|
||||
This operation marks matching series for deletion. Deleted data
|
||||
cannot be recovered. Use Clean Tombstones afterwards to reclaim disk
|
||||
space.
|
||||
</Alert>
|
||||
|
||||
{error && (
|
||||
<Alert color="red" title="Error" withCloseButton onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert
|
||||
icon={<IconCheck size={16} />}
|
||||
color="green"
|
||||
title="Success"
|
||||
withCloseButton
|
||||
onClose={() => setSuccess(null)}
|
||||
>
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
label="Series selector(s)"
|
||||
description="PromQL series selectors, one per line. Example: up{job="prometheus"}"
|
||||
placeholder={'up{job="prometheus"}'}
|
||||
value={matchers}
|
||||
onChange={(e) => setMatchers(e.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<TextInput
|
||||
label="Start time"
|
||||
description="RFC3339 or Unix timestamp (optional)"
|
||||
placeholder="2024-01-01T00:00:00Z"
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="End time"
|
||||
description="RFC3339 or Unix timestamp (optional)"
|
||||
placeholder="2024-12-31T23:59:59Z"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.currentTarget.value)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
color="red"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={handleDelete}
|
||||
loading={deleting}
|
||||
disabled={!matchers.trim()}
|
||||
>
|
||||
Delete series
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</InfoPageCard>
|
||||
|
||||
<InfoPageCard title="Clean Tombstones">
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
After deleting series, tombstones mark the data for deletion but
|
||||
do not free disk space immediately. Use this to remove tombstones
|
||||
and reclaim storage.
|
||||
</Text>
|
||||
<Group>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCleanTombstones}
|
||||
loading={cleaning}
|
||||
>
|
||||
Clean tombstones
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</InfoPageCard>
|
||||
</InfoPageStack>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user