mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-30 15:22:07 +00:00
app/vmui: small usability improvements
- Show in the line tooltip the number of the query which generates the given line. This simplifies comparison of lines generated by multiple queries. - Show metric name as __name__ label in the line tooltip in the same way as other labels are shown there. This makes the label information in the tooltip more consistent. - Properly quote label values with JSON.stringify(). This prevents from improper formatting when label values contain doublequote chars. - Remove double curly braces artifact at graph legend for lines without names and labels. - Properly use modifier for regular expressions across the code.
This commit is contained in:
parent
59e1e84a92
commit
1794f3d46e
15 changed files with 46 additions and 51 deletions
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "./static/css/main.9a291a47.css",
|
"main.css": "./static/css/main.9a291a47.css",
|
||||||
"main.js": "./static/js/main.9d62d7df.js",
|
"main.js": "./static/js/main.e3ded72d.js",
|
||||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||||
"index.html": "./index.html"
|
"index.html": "./index.html"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.9a291a47.css",
|
"static/css/main.9a291a47.css",
|
||||||
"static/js/main.9d62d7df.js"
|
"static/js/main.e3ded72d.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1 +1 @@
|
||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.9d62d7df.js"></script><link href="./static/css/main.9a291a47.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.e3ded72d.js"></script><link href="./static/css/main.9a291a47.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.e3ded72d.js
Normal file
2
app/vmselect/vmui/static/js/main.e3ded72d.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||||
import uPlot, { Series } from "uplot";
|
import uPlot, { Series } from "uplot";
|
||||||
import { MetricResult } from "../../../api/types";
|
import { MetricResult } from "../../../api/types";
|
||||||
import { formatPrettyNumber, getColorLine, getLegendLabel } from "../../../utils/uplot/helpers";
|
import { formatPrettyNumber, getColorLine } from "../../../utils/uplot/helpers";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
|
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
@ -54,14 +54,14 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||||
const color = useMemo(() => getColorLine(series[seriesIdx]?.label || ""), [series, seriesIdx]);
|
const color = useMemo(() => getColorLine(series[seriesIdx]?.label || ""), [series, seriesIdx]);
|
||||||
|
|
||||||
const name = useMemo(() => {
|
const name = useMemo(() => {
|
||||||
const metricName = (series[seriesIdx]?.label || "").replace(/{.+}/gmi, "").trim();
|
const group = metrics[seriesIdx -1]?.group || 0;
|
||||||
return getLegendLabel(metricName);
|
return `Query ${group}`;
|
||||||
}, [series, seriesIdx]);
|
}, [series, seriesIdx]);
|
||||||
|
|
||||||
const fields = useMemo(() => {
|
const fields = useMemo(() => {
|
||||||
const metric = metrics[seriesIdx - 1]?.metric || {};
|
const metric = metrics[seriesIdx - 1]?.metric || {};
|
||||||
const fields = Object.keys(metric).filter(k => k !== "__name__");
|
const fields = Object.keys(metric);
|
||||||
return fields.map(key => `${key}="${metric[key]}"`);
|
return fields.map(key => `${key}=${JSON.stringify(metric[key])}`);
|
||||||
}, [metrics, seriesIdx]);
|
}, [metrics, seriesIdx]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { FC, useState, useMemo } from "preact/compat";
|
import React, { FC, useState, useMemo } from "preact/compat";
|
||||||
import { MouseEvent } from "react";
|
import { MouseEvent } from "react";
|
||||||
import { LegendItemType } from "../../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../utils/uplot/types";
|
||||||
import { getLegendLabel } from "../../../../utils/uplot/helpers";
|
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||||
|
@ -46,27 +45,30 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
||||||
/>
|
/>
|
||||||
<div className="vm-legend-item-info">
|
<div className="vm-legend-item-info">
|
||||||
<span className="vm-legend-item-info__label">
|
<span className="vm-legend-item-info__label">
|
||||||
{getLegendLabel(legend.label)}
|
{legend.freeFormFields["__name__"] || (freeFormFields.length == 0 ? "{}" : "")}
|
||||||
</span>
|
</span>
|
||||||
|
{freeFormFields.length > 0 &&
|
||||||
 {
|
<span>
|
||||||
{freeFormFields.map(f => (
|
{
|
||||||
<Tooltip
|
{freeFormFields.map(f => (
|
||||||
key={f.id}
|
<Tooltip
|
||||||
open={copiedValue === f.id}
|
key={f.id}
|
||||||
title={"Copied!"}
|
open={copiedValue === f.id}
|
||||||
placement="top-center"
|
title={"Copied!"}
|
||||||
>
|
placement="top-center"
|
||||||
<span
|
>
|
||||||
className="vm-legend-item-info__free-fields"
|
<span
|
||||||
key={f.key}
|
className="vm-legend-item-info__free-fields"
|
||||||
onClick={createHandlerCopy(f.freeField, f.id)}
|
key={f.key}
|
||||||
>
|
onClick={createHandlerCopy(f.freeField, f.id)}
|
||||||
{f.freeField}
|
>
|
||||||
</span>
|
{f.freeField}
|
||||||
</Tooltip>
|
</span>
|
||||||
))}
|
</Tooltip>
|
||||||
}
|
))}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ export const getFreeFields = (legend: LegendItemType) => {
|
||||||
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
|
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
|
||||||
|
|
||||||
return keys.map(f => {
|
return keys.map(f => {
|
||||||
const freeField = `${f}="${legend.freeFormFields[f]}"`;
|
const freeField = `${f}=${JSON.stringify(legend.freeFormFields[f])}`;
|
||||||
const id = `${legend.label}.${freeField}`;
|
const id = `${legend.label}.${freeField}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -20,7 +20,7 @@ const TenantsConfiguration: FC = () => {
|
||||||
const tenantId = Number(value);
|
const tenantId = Number(value);
|
||||||
dispatch({ type: "SET_TENANT_ID", payload: tenantId });
|
dispatch({ type: "SET_TENANT_ID", payload: tenantId });
|
||||||
if (serverURL) {
|
if (serverURL) {
|
||||||
const updateServerUrl = serverURL.replace(/(\/select\/)([\d]+)(\/prometheus)/gmi, `$1${tenantId}$3`);
|
const updateServerUrl = serverURL.replace(/(\/select\/)([\d]+)(\/prometheus)/, `$1${tenantId}$3`);
|
||||||
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
|
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
|
||||||
timeDispatch({ type: "RUN_QUERY" });
|
timeDispatch({ type: "RUN_QUERY" });
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
|
||||||
const rows: InstantDataSeries[] = useMemo(() => {
|
const rows: InstantDataSeries[] = useMemo(() => {
|
||||||
const rows = data?.map(d => ({
|
const rows = data?.map(d => ({
|
||||||
metadata: sortedColumns.map(c => (tableCompact
|
metadata: sortedColumns.map(c => (tableCompact
|
||||||
? getNameForMetric(d, undefined, "=", true)
|
? getNameForMetric(d)
|
||||||
: (d.metric[c.key] || "-")
|
: (d.metric[c.key] || "-")
|
||||||
)),
|
)),
|
||||||
value: d.value ? d.value[1] : "-",
|
value: d.value ? d.value[1] : "-",
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { MetricBase } from "../api/types";
|
import { MetricBase } from "../api/types";
|
||||||
|
|
||||||
export const getNameForMetric = (result: MetricBase, alias?: string, connector = ": ", quoteValue = false): string => {
|
export const getNameForMetric = (result: MetricBase, alias?: string): string => {
|
||||||
const { __name__, ...freeFormFields } = result.metric;
|
const { __name__, ...freeFormFields } = result.metric;
|
||||||
const name = alias || __name__ || "";
|
const name = alias || `[Query ${result.group}] ${__name__ || ""}`;
|
||||||
|
if (Object.keys(freeFormFields).length == 0) {
|
||||||
if (Object.keys(result.metric).length === 0) {
|
return name;
|
||||||
return name || `Result ${result.group}`; // a bit better than just {} for case of aggregation functions
|
|
||||||
}
|
}
|
||||||
|
return `${name}{${Object.entries(freeFormFields).map(e =>
|
||||||
return `${name} {${Object.entries(freeFormFields).map(e =>
|
`${e[0]}=${JSON.stringify(e[1])}`
|
||||||
`${e[0]}${connector}${(quoteValue ? `"${e[1]}"` : e[1])}`
|
|
||||||
).join(", ")}}`;
|
).join(", ")}}`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const getQueryStringValue = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQueryArray = (): string[] => {
|
export const getQueryArray = (): string[] => {
|
||||||
const queryLength = window.location.search.match(/g\d+.expr/gmi)?.length || 1;
|
const queryLength = window.location.search.match(/g\d+\.expr/g)?.length || 1;
|
||||||
return new Array(queryLength > MAX_QUERY_FIELDS ? MAX_QUERY_FIELDS : queryLength)
|
return new Array(queryLength > MAX_QUERY_FIELDS ? MAX_QUERY_FIELDS : queryLength)
|
||||||
.fill(1)
|
.fill(1)
|
||||||
.map((q, i) => getQueryStringValue(`g${i}.expr`, "") as string);
|
.map((q, i) => getQueryStringValue(`g${i}.expr`, "") as string);
|
||||||
|
|
|
@ -194,8 +194,8 @@ export const getTimezoneList = (search = "") => {
|
||||||
return supportedTimezones.reduce((acc: {[key: string]: Timezone[]}, region) => {
|
return supportedTimezones.reduce((acc: {[key: string]: Timezone[]}, region) => {
|
||||||
const zone = (region.match(/^(.*?)\//) || [])[1] || "unknown";
|
const zone = (region.match(/^(.*?)\//) || [])[1] || "unknown";
|
||||||
const utc = getUTCByTimezone(region);
|
const utc = getUTCByTimezone(region);
|
||||||
const utcForSearch = utc.replace(/UTC|0/gmi, "");
|
const utcForSearch = utc.replace(/UTC|0/, "");
|
||||||
const regionForSearch = region.replace(/[/_]/gmi, " ");
|
const regionForSearch = region.replace(/[/_]/g, " ");
|
||||||
const item = {
|
const item = {
|
||||||
region,
|
region,
|
||||||
utc,
|
utc,
|
||||||
|
|
|
@ -66,7 +66,3 @@ export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum:
|
||||||
export const getColorLine = (label: string): string => getColorFromString(label);
|
export const getColorLine = (label: string): string => getColorFromString(label);
|
||||||
|
|
||||||
export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];
|
export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];
|
||||||
|
|
||||||
export const getLegendLabel = (label: string): string => {
|
|
||||||
return label.replace(/^\[\d+]/, "").replace(/{.+}/gmi, "");
|
|
||||||
};
|
|
||||||
|
|
|
@ -10,8 +10,7 @@ interface SeriesItem extends Series {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSeriesItem = (d: MetricResult, hideSeries: string[], alias: string[]): SeriesItem => {
|
export const getSeriesItem = (d: MetricResult, hideSeries: string[], alias: string[]): SeriesItem => {
|
||||||
const name = getNameForMetric(d, alias[d.group - 1]);
|
const label = getNameForMetric(d, alias[d.group - 1]);
|
||||||
const label = `[${d.group}]${name}`;
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
freeFormFields: d.metric,
|
freeFormFields: d.metric,
|
||||||
|
|
Loading…
Reference in a new issue