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 59e1e84a92
commit 1794f3d46e
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": { "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"
] ]
} }

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 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 = () => {

View file

@ -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 &&
&#160;&#123; <span>
{freeFormFields.map(f => ( &#123;
<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>
&#125; ))}
&#125;
</span>
}
</div> </div>
</div> </div>
); );

View file

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

View file

@ -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" });
} }

View file

@ -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] : "-",

View file

@ -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(", ")}}`;
}; };

View file

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

View file

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

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 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, "");
};

View file

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