mirror of
https://github.com/prometheus/prometheus
synced 2026-04-20 22:41:05 +08:00
Merge pull request #18390 from vijaygovindaraja/ui/delete-series-page
ui: add delete series page
This commit is contained in:
@@ -1,11 +1,26 @@
|
||||
import { Table } from "@mantine/core";
|
||||
import { useSuspenseAPIQuery } from "../api/api";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
} from "@mantine/core";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import { IconAlertTriangle, IconCheck, IconTrash } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { useSuspenseAPIQuery, API_PATH } from "../api/api";
|
||||
import { TSDBStatusResult } from "../api/responseTypes/tsdbStatus";
|
||||
import { formatTimestamp } from "../lib/formatTime";
|
||||
import { useSettings } from "../state/settingsSlice";
|
||||
import InfoPageStack from "../components/InfoPageStack";
|
||||
import InfoPageCard from "../components/InfoPageCard";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
export default function TSDBStatusPage() {
|
||||
const {
|
||||
data: {
|
||||
@@ -19,7 +34,7 @@ export default function TSDBStatusPage() {
|
||||
},
|
||||
} = useSuspenseAPIQuery<TSDBStatusResult>({ path: `/status/tsdb` });
|
||||
|
||||
const { useLocalTime } = useSettings();
|
||||
const { useLocalTime, pathPrefix } = useSettings();
|
||||
|
||||
const unixToTime = (unix: number): string => {
|
||||
const formatted = formatTimestamp(unix, useLocalTime);
|
||||
@@ -42,6 +57,105 @@ export default function TSDBStatusPage() {
|
||||
{ name: "Current Max Time", value: `${unixToTime(maxTime / 1000)}` },
|
||||
];
|
||||
|
||||
// Delete series state
|
||||
const [matchers, setMatchers] = useState("");
|
||||
const [startTime, setStartTime] = useState<number | null>(null);
|
||||
const [endTime, setEndTime] = useState<number | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [deleteSuccess, setDeleteSuccess] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// Clean tombstones state
|
||||
const [cleanError, setCleanError] = useState<string | null>(null);
|
||||
const [cleanSuccess, setCleanSuccess] = useState<string | null>(null);
|
||||
const [cleaning, setCleaning] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setDeleteError(null);
|
||||
setDeleteSuccess(null);
|
||||
|
||||
const matchList = matchers
|
||||
.split("\n")
|
||||
.map((m) => m.trim())
|
||||
.filter((m) => m !== "");
|
||||
|
||||
if (matchList.length === 0) {
|
||||
setDeleteError("Provide at least one match[] selector.");
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const m of matchList) {
|
||||
params.append("match[]", m);
|
||||
}
|
||||
if (startTime !== null) {
|
||||
params.append("start", (startTime / 1000).toString());
|
||||
}
|
||||
if (endTime !== null) {
|
||||
params.append("end", (endTime / 1000).toString());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
setDeleteSuccess(
|
||||
`Successfully deleted series matching: ${matchList.join(", ")}`
|
||||
);
|
||||
setMatchers("");
|
||||
setStartTime(null);
|
||||
setEndTime(null);
|
||||
} catch (err) {
|
||||
setDeleteError(err instanceof Error ? err.message : "Unknown error");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCleanTombstones = async () => {
|
||||
setCleanError(null);
|
||||
setCleanSuccess(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);
|
||||
}
|
||||
|
||||
setCleanSuccess("Tombstones cleaned successfully.");
|
||||
} catch (err) {
|
||||
setCleanError(err instanceof Error ? err.message : "Unknown error");
|
||||
} finally {
|
||||
setCleaning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InfoPageStack>
|
||||
{[
|
||||
@@ -97,6 +211,163 @@ export default function TSDBStatusPage() {
|
||||
</Table>
|
||||
</InfoPageCard>
|
||||
))}
|
||||
|
||||
<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>
|
||||
|
||||
{deleteError && (
|
||||
<Alert
|
||||
color="red"
|
||||
title="Error"
|
||||
withCloseButton
|
||||
onClose={() => setDeleteError(null)}
|
||||
>
|
||||
{deleteError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{deleteSuccess && (
|
||||
<Alert
|
||||
icon={<IconCheck size={16} />}
|
||||
color="green"
|
||||
title="Success"
|
||||
withCloseButton
|
||||
onClose={() => setDeleteSuccess(null)}
|
||||
>
|
||||
{deleteSuccess}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
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)}
|
||||
autosize
|
||||
minRows={2}
|
||||
maxRows={6}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<DateTimePicker
|
||||
label="Start time"
|
||||
description="Optional"
|
||||
placeholder="Select start time"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
withSeconds
|
||||
value={
|
||||
startTime !== null
|
||||
? useLocalTime
|
||||
? dayjs(startTime).format()
|
||||
: dayjs(startTime)
|
||||
.subtract(dayjs().utcOffset(), "minutes")
|
||||
.format()
|
||||
: undefined
|
||||
}
|
||||
onChange={(value) =>
|
||||
setStartTime(
|
||||
value
|
||||
? useLocalTime
|
||||
? new Date(value).getTime()
|
||||
: dayjs.utc(value).valueOf()
|
||||
: null
|
||||
)
|
||||
}
|
||||
clearable
|
||||
/>
|
||||
<DateTimePicker
|
||||
label="End time"
|
||||
description="Optional"
|
||||
placeholder="Select end time"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
withSeconds
|
||||
value={
|
||||
endTime !== null
|
||||
? useLocalTime
|
||||
? dayjs(endTime).format()
|
||||
: dayjs(endTime)
|
||||
.subtract(dayjs().utcOffset(), "minutes")
|
||||
.format()
|
||||
: undefined
|
||||
}
|
||||
onChange={(value) =>
|
||||
setEndTime(
|
||||
value
|
||||
? useLocalTime
|
||||
? new Date(value).getTime()
|
||||
: dayjs.utc(value).valueOf()
|
||||
: null
|
||||
)
|
||||
}
|
||||
clearable
|
||||
/>
|
||||
</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">
|
||||
{cleanError && (
|
||||
<Alert
|
||||
color="red"
|
||||
title="Error"
|
||||
withCloseButton
|
||||
onClose={() => setCleanError(null)}
|
||||
>
|
||||
{cleanError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{cleanSuccess && (
|
||||
<Alert
|
||||
icon={<IconCheck size={16} />}
|
||||
color="green"
|
||||
title="Success"
|
||||
withCloseButton
|
||||
onClose={() => setCleanSuccess(null)}
|
||||
>
|
||||
{cleanSuccess}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
39
web/ui/package-lock.json
generated
39
web/ui/package-lock.json
generated
@@ -2442,9 +2442,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2459,9 +2456,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2476,9 +2470,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2493,9 +2484,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2510,9 +2498,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2527,9 +2512,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2544,9 +2526,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2561,9 +2540,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2578,9 +2554,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2595,9 +2568,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2612,9 +2582,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2629,9 +2596,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2646,9 +2610,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
Reference in New Issue
Block a user