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:
Aliaksandr Valialkin 2022-12-29 14:52:48 -08:00
parent c03adc2bce
commit d794e971fc
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
15 changed files with 46 additions and 51 deletions

View file

@ -1,12 +1,12 @@
{
"files": {
"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",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.9a291a47.css",
"static/js/main.9d62d7df.js"
"static/js/main.e3ded72d.js"
]
}

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot, { Series } from "uplot";
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 { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
import ReactDOM from "react-dom";
@ -54,14 +54,14 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
const color = useMemo(() => getColorLine(series[seriesIdx]?.label || ""), [series, seriesIdx]);
const name = useMemo(() => {
const metricName = (series[seriesIdx]?.label || "").replace(/{.+}/gmi, "").trim();
return getLegendLabel(metricName);
const group = metrics[seriesIdx -1]?.group || 0;
return `Query ${group}`;
}, [series, seriesIdx]);
const fields = useMemo(() => {
const metric = metrics[seriesIdx - 1]?.metric || {};
const fields = Object.keys(metric).filter(k => k !== "__name__");
return fields.map(key => `${key}="${metric[key]}"`);
const fields = Object.keys(metric);
return fields.map(key => `${key}=${JSON.stringify(metric[key])}`);
}, [metrics, seriesIdx]);
const handleClose = () => {

View file

@ -1,7 +1,6 @@
import React, { FC, useState, useMemo } from "preact/compat";
import { MouseEvent } from "react";
import { LegendItemType } from "../../../../utils/uplot/types";
import { getLegendLabel } from "../../../../utils/uplot/helpers";
import "./style.scss";
import classNames from "classnames";
import Tooltip from "../../../Main/Tooltip/Tooltip";
@ -46,27 +45,30 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
/>
<div className="vm-legend-item-info">
<span className="vm-legend-item-info__label">
{getLegendLabel(legend.label)}
{legend.freeFormFields["__name__"] || (freeFormFields.length == 0 ? "{}" : "")}
</span>
&#160;&#123;
{freeFormFields.map(f => (
<Tooltip
key={f.id}
open={copiedValue === f.id}
title={"Copied!"}
placement="top-center"
>
<span
className="vm-legend-item-info__free-fields"
key={f.key}
onClick={createHandlerCopy(f.freeField, f.id)}
>
{f.freeField}
</span>
</Tooltip>
))}
&#125;
{freeFormFields.length > 0 &&
<span>
&#123;
{freeFormFields.map(f => (
<Tooltip
key={f.id}
open={copiedValue === f.id}
title={"Copied!"}
placement="top-center"
>
<span
className="vm-legend-item-info__free-fields"
key={f.key}
onClick={createHandlerCopy(f.freeField, f.id)}
>
{f.freeField}
</span>
</Tooltip>
))}
&#125;
</span>
}
</div>
</div>
);

View file

@ -4,7 +4,7 @@ export const getFreeFields = (legend: LegendItemType) => {
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
return keys.map(f => {
const freeField = `${f}="${legend.freeFormFields[f]}"`;
const freeField = `${f}=${JSON.stringify(legend.freeFormFields[f])}`;
const id = `${legend.label}.${freeField}`;
return {

View file

@ -20,7 +20,7 @@ const TenantsConfiguration: FC = () => {
const tenantId = Number(value);
dispatch({ type: "SET_TENANT_ID", payload: tenantId });
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 });
timeDispatch({ type: "RUN_QUERY" });
}

View file

@ -44,7 +44,7 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
const rows: InstantDataSeries[] = useMemo(() => {
const rows = data?.map(d => ({
metadata: sortedColumns.map(c => (tableCompact
? getNameForMetric(d, undefined, "=", true)
? getNameForMetric(d)
: (d.metric[c.key] || "-")
)),
value: d.value ? d.value[1] : "-",

View file

@ -1,14 +1,12 @@
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 = alias || __name__ || "";
if (Object.keys(result.metric).length === 0) {
return name || `Result ${result.group}`; // a bit better than just {} for case of aggregation functions
const name = alias || `[Query ${result.group}] ${__name__ || ""}`;
if (Object.keys(freeFormFields).length == 0) {
return name;
}
return `${name} {${Object.entries(freeFormFields).map(e =>
`${e[0]}${connector}${(quoteValue ? `"${e[1]}"` : e[1])}`
return `${name}{${Object.entries(freeFormFields).map(e =>
`${e[0]}=${JSON.stringify(e[1])}`
).join(", ")}}`;
};

View file

@ -22,7 +22,7 @@ export const getQueryStringValue = (
};
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)
.fill(1)
.map((q, i) => getQueryStringValue(`g${i}.expr`, "") as string);

View file

@ -194,8 +194,8 @@ export const getTimezoneList = (search = "") => {
return supportedTimezones.reduce((acc: {[key: string]: Timezone[]}, region) => {
const zone = (region.match(/^(.*?)\//) || [])[1] || "unknown";
const utc = getUTCByTimezone(region);
const utcForSearch = utc.replace(/UTC|0/gmi, "");
const regionForSearch = region.replace(/[/_]/gmi, " ");
const utcForSearch = utc.replace(/UTC|0/, "");
const regionForSearch = region.replace(/[/_]/g, " ");
const item = {
region,
utc,

View file

@ -66,7 +66,3 @@ export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum:
export const getColorLine = (label: string): string => getColorFromString(label);
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, "");
};

View file

@ -10,8 +10,7 @@ interface SeriesItem extends Series {
}
export const getSeriesItem = (d: MetricResult, hideSeries: string[], alias: string[]): SeriesItem => {
const name = getNameForMetric(d, alias[d.group - 1]);
const label = `[${d.group}]${name}`;
const label = getNameForMetric(d, alias[d.group - 1]);
return {
label,
freeFormFields: d.metric,