mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-12 16:44:05 -08:00
Implement encoding/decoding graph pages to/from URL
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
e87214a6bd
commit
a526a7ae53
|
@ -5,19 +5,37 @@ import {
|
||||||
IconPlus,
|
IconPlus,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
import { addPanel } from "../../state/queryPageSlice";
|
import { addPanel, setPanels } from "../../state/queryPageSlice";
|
||||||
import Panel from "./QueryPanel";
|
import Panel from "./QueryPanel";
|
||||||
import { LabelValuesResult } from "../../api/responseTypes/labelValues";
|
import { LabelValuesResult } from "../../api/responseTypes/labelValues";
|
||||||
import { useAPIQuery } from "../../api/api";
|
import { useAPIQuery } from "../../api/api";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { InstantQueryResult } from "../../api/responseTypes/query";
|
import { InstantQueryResult } from "../../api/responseTypes/query";
|
||||||
import { humanizeDuration } from "../../lib/formatTime";
|
import { humanizeDuration } from "../../lib/formatTime";
|
||||||
|
import { decodePanelOptionsFromURLParams } from "./urlStateEncoding";
|
||||||
|
|
||||||
export default function QueryPage() {
|
export default function QueryPage() {
|
||||||
const panels = useAppSelector((state) => state.queryPage.panels);
|
const panels = useAppSelector((state) => state.queryPage.panels);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [timeDelta, setTimeDelta] = useState(0);
|
const [timeDelta, setTimeDelta] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleURLChange = () => {
|
||||||
|
const panels = decodePanelOptionsFromURLParams(window.location.search);
|
||||||
|
if (panels.length > 0) {
|
||||||
|
dispatch(setPanels(panels));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleURLChange();
|
||||||
|
|
||||||
|
window.addEventListener("popstate", handleURLChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("popstate", handleURLChange);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const { data: metricNamesResult, error: metricNamesError } =
|
const { data: metricNamesResult, error: metricNamesError } =
|
||||||
useAPIQuery<LabelValuesResult>({
|
useAPIQuery<LabelValuesResult>({
|
||||||
path: "/label/__name__/values",
|
path: "/label/__name__/values",
|
||||||
|
|
|
@ -33,6 +33,8 @@ import ExpressionInput from "./ExpressionInput";
|
||||||
import Graph from "./Graph";
|
import Graph from "./Graph";
|
||||||
import {
|
import {
|
||||||
formatPrometheusDuration,
|
formatPrometheusDuration,
|
||||||
|
formatTimestamp,
|
||||||
|
now,
|
||||||
parsePrometheusDuration,
|
parsePrometheusDuration,
|
||||||
} from "../../lib/formatTime";
|
} from "../../lib/formatTime";
|
||||||
|
|
||||||
|
|
|
@ -225,7 +225,6 @@ const autoPadLeft = (
|
||||||
);
|
);
|
||||||
|
|
||||||
if (longestVal != "") {
|
if (longestVal != "") {
|
||||||
console.log("axis.font", axis.font![0]);
|
|
||||||
u.ctx.font = axis.font![0];
|
u.ctx.font = axis.font![0];
|
||||||
axisSize += u.ctx.measureText(longestVal).width / devicePixelRatio;
|
axisSize += u.ctx.measureText(longestVal).width / devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
109
web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts
Normal file
109
web/ui/mantine-ui/src/pages/query/urlStateEncoding.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import {
|
||||||
|
GraphDisplayMode,
|
||||||
|
Panel,
|
||||||
|
newDefaultPanel,
|
||||||
|
} from "../../state/queryPageSlice";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import {
|
||||||
|
formatPrometheusDuration,
|
||||||
|
parsePrometheusDuration,
|
||||||
|
} from "../../lib/formatTime";
|
||||||
|
|
||||||
|
export function parseTime(timeText: string): number {
|
||||||
|
return dayjs.utc(timeText).valueOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodePanelOptionsFromURLParams = (query: string): Panel[] => {
|
||||||
|
const urlParams = new URLSearchParams(query);
|
||||||
|
const panels = [];
|
||||||
|
|
||||||
|
for (let i = 0; ; i++) {
|
||||||
|
if (!urlParams.has(`g${i}.expr`)) {
|
||||||
|
// Every panel should have an expr, so if we don't find one, we're done.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = newDefaultPanel();
|
||||||
|
|
||||||
|
const decodeSetting = (setting: string, fn: (_value: string) => void) => {
|
||||||
|
const param = `g${i}.${setting}`;
|
||||||
|
if (urlParams.has(param)) {
|
||||||
|
fn(urlParams.get(param) as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
decodeSetting("expr", (value) => {
|
||||||
|
panel.expr = value;
|
||||||
|
});
|
||||||
|
decodeSetting("tab", (value) => {
|
||||||
|
panel.visualizer.activeTab = value === "0" ? "graph" : "table";
|
||||||
|
});
|
||||||
|
decodeSetting("display_mode", (value) => {
|
||||||
|
panel.visualizer.displayMode = value as GraphDisplayMode;
|
||||||
|
});
|
||||||
|
decodeSetting("stacked", (value) => {
|
||||||
|
panel.visualizer.displayMode =
|
||||||
|
value === "1" ? GraphDisplayMode.Stacked : GraphDisplayMode.Lines;
|
||||||
|
});
|
||||||
|
decodeSetting("show_exemplars", (value) => {
|
||||||
|
panel.visualizer.showExemplars = value === "1";
|
||||||
|
});
|
||||||
|
decodeSetting("range_input", (value) => {
|
||||||
|
panel.visualizer.range =
|
||||||
|
parsePrometheusDuration(value) || panel.visualizer.range;
|
||||||
|
});
|
||||||
|
decodeSetting("end_input", (value) => {
|
||||||
|
panel.visualizer.endTime = parseTime(value);
|
||||||
|
});
|
||||||
|
decodeSetting("moment_input", (value) => {
|
||||||
|
panel.visualizer.endTime = parseTime(value);
|
||||||
|
});
|
||||||
|
decodeSetting("step_input", (value) => {
|
||||||
|
if (parseInt(value) > 0) {
|
||||||
|
panel.visualizer.resolution = {
|
||||||
|
type: "custom",
|
||||||
|
value: parseInt(value) * 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
panels.push(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return panels;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatTime(time: number): string {
|
||||||
|
return dayjs.utc(time).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const encodePanelOptionsToURLParams = (
|
||||||
|
panels: Panel[]
|
||||||
|
): URLSearchParams => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
const addParam = (idx: number, param: string, value: string) =>
|
||||||
|
params.append(`g${idx}.${param}`, value);
|
||||||
|
|
||||||
|
panels.forEach((p, idx) => {
|
||||||
|
addParam(idx, "expr", p.expr);
|
||||||
|
addParam(idx, "tab", p.visualizer.activeTab === "graph" ? "0" : "1");
|
||||||
|
if (p.visualizer.endTime !== null) {
|
||||||
|
addParam(idx, "end_input", formatTime(p.visualizer.endTime));
|
||||||
|
addParam(idx, "moment_input", formatTime(p.visualizer.endTime));
|
||||||
|
}
|
||||||
|
addParam(idx, "range_input", formatPrometheusDuration(p.visualizer.range));
|
||||||
|
// TODO: Support the other new resolution types.
|
||||||
|
if (p.visualizer.resolution.type === "custom") {
|
||||||
|
addParam(
|
||||||
|
idx,
|
||||||
|
"step_input",
|
||||||
|
(p.visualizer.resolution.value / 1000).toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addParam(idx, "display_mode", p.visualizer.displayMode);
|
||||||
|
addParam(idx, "show_exemplars", p.visualizer.showExemplars ? "1" : "0");
|
||||||
|
});
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
|
@ -1,5 +1,11 @@
|
||||||
import { randomId } from "@mantine/hooks";
|
import { randomId } from "@mantine/hooks";
|
||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import {
|
||||||
|
PayloadAction,
|
||||||
|
createListenerMiddleware,
|
||||||
|
createSlice,
|
||||||
|
} from "@reduxjs/toolkit";
|
||||||
|
import { encodePanelOptionsToURLParams } from "../pages/query/urlStateEncoding";
|
||||||
|
import { update } from "lodash";
|
||||||
|
|
||||||
export enum GraphDisplayMode {
|
export enum GraphDisplayMode {
|
||||||
Lines = "lines",
|
Lines = "lines",
|
||||||
|
@ -69,7 +75,7 @@ interface QueryPageState {
|
||||||
panels: Panel[];
|
panels: Panel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDefaultPanel = (): Panel => ({
|
export const newDefaultPanel = (): Panel => ({
|
||||||
id: randomId(),
|
id: randomId(),
|
||||||
expr: "",
|
expr: "",
|
||||||
exprStale: false,
|
exprStale: false,
|
||||||
|
@ -88,32 +94,44 @@ const initialState: QueryPageState = {
|
||||||
panels: [newDefaultPanel()],
|
panels: [newDefaultPanel()],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateURL = (panels: Panel[]) => {
|
||||||
|
const query = "?" + encodePanelOptionsToURLParams(panels).toString();
|
||||||
|
window.history.pushState({}, "", query);
|
||||||
|
};
|
||||||
|
|
||||||
export const queryPageSlice = createSlice({
|
export const queryPageSlice = createSlice({
|
||||||
name: "queryPage",
|
name: "queryPage",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
setPanels: (state, { payload }: PayloadAction<Panel[]>) => {
|
||||||
|
state.panels = payload;
|
||||||
|
},
|
||||||
addPanel: (state) => {
|
addPanel: (state) => {
|
||||||
state.panels.push(newDefaultPanel());
|
state.panels.push(newDefaultPanel());
|
||||||
|
updateURL(state.panels);
|
||||||
},
|
},
|
||||||
removePanel: (state, { payload }: PayloadAction<number>) => {
|
removePanel: (state, { payload }: PayloadAction<number>) => {
|
||||||
state.panels.splice(payload, 1);
|
state.panels.splice(payload, 1);
|
||||||
|
updateURL(state.panels);
|
||||||
},
|
},
|
||||||
setExpr: (
|
setExpr: (
|
||||||
state,
|
state,
|
||||||
{ payload }: PayloadAction<{ idx: number; expr: string }>
|
{ payload }: PayloadAction<{ idx: number; expr: string }>
|
||||||
) => {
|
) => {
|
||||||
state.panels[payload.idx].expr = payload.expr;
|
state.panels[payload.idx].expr = payload.expr;
|
||||||
|
updateURL(state.panels);
|
||||||
},
|
},
|
||||||
setVisualizer: (
|
setVisualizer: (
|
||||||
state,
|
state,
|
||||||
{ payload }: PayloadAction<{ idx: number; visualizer: Visualizer }>
|
{ payload }: PayloadAction<{ idx: number; visualizer: Visualizer }>
|
||||||
) => {
|
) => {
|
||||||
state.panels[payload.idx].visualizer = payload.visualizer;
|
state.panels[payload.idx].visualizer = payload.visualizer;
|
||||||
|
updateURL(state.panels);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { addPanel, removePanel, setExpr, setVisualizer } =
|
export const { setPanels, addPanel, removePanel, setExpr, setVisualizer } =
|
||||||
queryPageSlice.actions;
|
queryPageSlice.actions;
|
||||||
|
|
||||||
export default queryPageSlice.reducer;
|
export default queryPageSlice.reducer;
|
||||||
|
|
Loading…
Reference in a new issue