vmui: Add button to prettify query (#4694)

* Add button to prettify query

Just capitalizes query text for now

* Add /prettify-query API handler

* Replace UI pretiffier using prettifier API

* Add showing server errors

Had to pass setQueryErrors from useFetchQuery.ts

* Use serverUrl from global AppState

* Change icon to AutoAwsome icon + added style change color when button is active

* Add sync/await to prettifyQuery function

* Doc public function for lint

* Minor async fix

* Removed extra blank lines

* Extract usePrettifyQuery hook

* Made more generic style for :active button

* Refactor usePrettifyQuery

However, prettify errors don't clean up query errors, but should

* Add prettyQuery functionality to CHANGELOG.md

* Reuse queryErrors

* Unhide errors on start

---------

Co-authored-by: Tamara <toma.vashchuk@gmail.com>
This commit is contained in:
Tamara Vashchuk 2023-08-18 10:12:48 -07:00 committed by GitHub
parent e9d246f367
commit 7349f18c55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 145 additions and 12 deletions

View file

@ -480,6 +480,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
expandWithExprsRequests.Inc()
prometheus.ExpandWithExprs(w, r)
return true
case "/prettify-query":
prettifyQueryRequests.Inc()
prometheus.PrettifyQuery(w, r)
return true
case "/api/v1/rules", "/rules":
rulesRequests.Inc()
if len(*vmalertProxyURL) > 0 {
@ -655,6 +659,7 @@ var (
graphiteFunctionDetailsErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/functions/<func_name>"}`)
expandWithExprsRequests = metrics.NewCounter(`vm_http_requests_total{path="/expand-with-exprs"}`)
prettifyQueryRequests = metrics.NewCounter(`vm_http_requests_total{path="/prettify-query"}`)
vmalertRequests = metrics.NewCounter(`vm_http_requests_total{path="/vmalert"}`)
rulesRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/rules"}`)

View file

@ -3,6 +3,7 @@ package prometheus
import (
"flag"
"fmt"
"github.com/VictoriaMetrics/metricsql"
"math"
"net/http"
"runtime"
@ -75,6 +76,24 @@ func ExpandWithExprs(w http.ResponseWriter, r *http.Request) {
_ = bw.Flush()
}
// PrettifyQuery implements /prettify-query. Takes a MetricsQL query and returns it formatted.
func PrettifyQuery(w http.ResponseWriter, r *http.Request) {
query := r.FormValue("query")
bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw)
w.Header().Set("Content-Type", "application/json")
httpserver.EnableCORS(w, r)
prettyQuery, err := metricsql.Prettify(query)
if err != nil {
fmt.Fprintf(bw, `{"status": "error", "msg": %q}`, err)
} else {
fmt.Fprintf(bw, `{"status": "success", "query": %q}`, prettyQuery)
}
_ = bw.Flush()
}
// FederateHandler implements /federate . See https://prometheus.io/docs/prometheus/latest/federation/
func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
defer federateDuration.UpdateDuration(startTime)

View file

@ -45,6 +45,10 @@ $button-radius: 6px;
transform: translateZ(-1px);
}
&:active:after {
transform: scale(0.9);
}
span {
display: grid;
align-items: center;

View file

@ -291,6 +291,17 @@ export const VisibilityOffIcon = () => (
</svg>
);
export const Prettify = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"
></path>
</svg>
);
export const CopyIcon = () => (
<svg
viewBox="0 0 24 24"

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "preact/compat";
import { StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/compat";
import { getQueryRangeUrl, getQueryUrl } from "../api/query-range";
import { useAppState } from "../state/common/StateContext";
import { InstantMetricResult, MetricBase, MetricResult, QueryStats } from "../api/types";
@ -28,6 +28,7 @@ interface FetchQueryReturn {
liveData?: InstantMetricResult[],
error?: ErrorTypes | string,
queryErrors: (ErrorTypes | string)[],
setQueryErrors: StateUpdater<string[]>,
queryStats: QueryStats[],
warning?: string,
traces?: Trace[],
@ -62,12 +63,13 @@ export const useFetchQuery = ({
const [liveData, setLiveData] = useState<InstantMetricResult[]>();
const [traces, setTraces] = useState<Trace[]>();
const [error, setError] = useState<ErrorTypes | string>();
const [queryErrors, setQueryErrors] = useState<(ErrorTypes | string)[]>([]);
const [queryErrors, setQueryErrors] = useState<string[]>([]);
const [queryStats, setQueryStats] = useState<QueryStats[]>([]);
const [warning, setWarning] = useState<string>();
const [fetchQueue, setFetchQueue] = useState<AbortController[]>([]);
const [isHistogram, setIsHistogram] = useState(false);
const fetchData = async ({
fetchUrl,
fetchQueue,
@ -193,5 +195,5 @@ export const useFetchQuery = ({
setFetchQueue(fetchQueue.filter(f => !f.signal.aborted));
}, [fetchQueue]);
return { fetchUrl, isLoading, graphData, liveData, error, queryErrors, queryStats, warning, traces, isHistogram };
return { fetchUrl, isLoading, graphData, liveData, error, queryErrors, setQueryErrors, queryStats, warning, traces, isHistogram };
};

View file

@ -1,12 +1,18 @@
import React, { FC, useState, useEffect } from "preact/compat";
import React, { FC, StateUpdater, useEffect, useState } from "preact/compat";
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
import AdditionalSettings from "../../../components/Configurators/AdditionalSettings/AdditionalSettings";
import { ErrorTypes } from "../../../types";
import usePrevious from "../../../hooks/usePrevious";
import { MAX_QUERY_FIELDS } from "../../../constants/graph";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import { useTimeDispatch } from "../../../state/time/TimeStateContext";
import { DeleteIcon, PlayIcon, PlusIcon, VisibilityIcon, VisibilityOffIcon } from "../../../components/Main/Icons";
import {
DeleteIcon,
PlayIcon,
PlusIcon,
Prettify,
VisibilityIcon,
VisibilityOffIcon
} from "../../../components/Main/Icons";
import Button from "../../../components/Main/Button/Button";
import "./style.scss";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
@ -15,9 +21,12 @@ import { MouseEvent as ReactMouseEvent } from "react";
import { arrayEquals } from "../../../utils/array";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { QueryStats } from "../../../api/types";
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
export interface QueryConfiguratorProps {
errors: (ErrorTypes | string)[];
queryErrors: string[];
setQueryErrors: StateUpdater<string[]>;
setHideError: StateUpdater<boolean>;
stats: QueryStats[];
queryOptions: string[]
onHideQuery: (queries: number[]) => void
@ -25,12 +34,15 @@ export interface QueryConfiguratorProps {
}
const QueryConfigurator: FC<QueryConfiguratorProps> = ({
errors,
queryErrors,
setQueryErrors,
setHideError,
stats,
queryOptions,
onHideQuery,
onRunQuery
}) => {
const { isMobile } = useDeviceDetect();
const { query, queryHistory, autocomplete } = useQueryState();
@ -41,6 +53,8 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
const [hideQuery, setHideQuery] = useState<number[]>([]);
const prevStateQuery = usePrevious(stateQuery) as (undefined | string[]);
const getPrettifiedQuery = usePrettifyQuery();
const updateHistory = () => {
queryDispatch({
type: "SET_QUERY_HISTORY", payload: stateQuery.map((q, i) => {
@ -106,13 +120,25 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
const createHandlerRemoveQuery = (i: number) => () => {
handleRemoveQuery(i);
setHideQuery(prev => prev.includes(i) ? prev.filter(n => n !== i) : prev.map(n => n > i ? n - 1: n));
setHideQuery(prev => prev.includes(i) ? prev.filter(n => n !== i) : prev.map(n => n > i ? n - 1 : n));
};
const createHandlerHideQuery = (i: number) => (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
handleToggleHideQuery(e, i);
};
const handlePrettifyQuery = async (i:number) => {
const prettyQuery = await getPrettifiedQuery(stateQuery[i]);
setHideError(false);
handleChangeQuery(prettyQuery.query, i);
setQueryErrors((qe) => {
qe[i] = prettyQuery.error;
return [...qe];
});
};
useEffect(() => {
if (prevStateQuery && (stateQuery.length < prevStateQuery.length)) {
handleRunQuery();
@ -144,7 +170,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
value={stateQuery[i]}
autocomplete={autocomplete}
options={queryOptions}
error={errors[i]}
error={queryErrors[i]}
stats={stats[i]}
onArrowUp={createHandlerArrow(-1, i)}
onArrowDown={createHandlerArrow(1, i)}
@ -163,6 +189,19 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
/>
</div>
</Tooltip>
<Tooltip title={"Prettify query"}>
<div className="vm-query-configurator-list-row__button">
<Button
variant={"text"}
color={"gray"}
startIcon={<Prettify/>}
onClick={async () => await handlePrettifyQuery(i)}
className="prettify"
/>
</div>
</Tooltip>
{stateQuery.length > 1 && (
<Tooltip title="Remove Query">
<div className="vm-query-configurator-list-row__button">

View file

@ -0,0 +1,49 @@
import { useAppState } from "../../../../state/common/StateContext";
export interface PrettyQuery {
query: string;
error: string;
}
export const usePrettifyQuery = () => {
const { serverUrl } = useAppState();
const getPrettifiedQuery = async (query: string): Promise<PrettyQuery> => {
try {
const oldQuery = encodeURIComponent(query);
const fetchUrl = `${serverUrl}/prettify-query?query=${oldQuery}`;
// {"status": "success", "query": "metrics"}
// {"status": "error", "msg": "labelFilterExpr: unexpected token ..."}
const response = await fetch(fetchUrl);
if (response.status != 200) {
return {
query: query,
error: "Error requesting /prettify-query, status: " + response.status,
};
}
const data = await response.json();
if (data["status"] != "success") {
return {
query: query,
error: String(data.msg) };
}
return {
query: String(data.query),
error: "" };
} catch (e) {
console.error(e);
if (e instanceof Error && e.name !== "AbortError") {
return { query: query, error: `${e.name}: ${e.message}` };
}
return { query: query, error: String(e) };
}
};
return getPrettifiedQuery;
};

View file

@ -9,7 +9,7 @@
&-row {
display: grid;
grid-template-columns: 1fr auto auto;
grid-template-columns: 1fr auto auto auto;
align-items: center;
gap: $padding-small;

View file

@ -57,6 +57,7 @@ const CustomPanel: FC = () => {
graphData,
error,
queryErrors,
setQueryErrors,
queryStats,
warning,
traces,
@ -128,7 +129,9 @@ const CustomPanel: FC = () => {
})}
>
<QueryConfigurator
errors={!hideError ? queryErrors : []}
queryErrors={!hideError ? queryErrors : []}
setQueryErrors={setQueryErrors}
setHideError={setHideError}
stats={queryStats}
queryOptions={queryOptions}
onHideQuery={handleHideQuery}

View file

@ -60,6 +60,7 @@ The v1.93.x line will be supported for at least 12 months since [v1.93.0](https:
* FEATURE: [Official Grafana dashboards for VictoriaMetrics](https://grafana.com/orgs/victoriametrics): add panels for absolute Mem and CPU usage by vmalert. See related issue [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4627).
* FEATURE: [Official Grafana dashboards for VictoriaMetrics](https://grafana.com/orgs/victoriametrics): correctly calculate `Bytes per point` value for single-server and cluster VM dashboards. Before, the calculation mistakenly accounted for the number of entries in indexdb in denominator, which could have shown lower values than expected.
* FEATURE: [Alerting rules for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts): `ConcurrentFlushesHitTheLimit` alerting rule was moved from [single-server](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts.yml) and [cluster](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-cluster.yml) alerts to the [list of "health" alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-health.yml) as it could be related to many VictoriaMetrics components.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): added Prettify query functionality. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4681)
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): return human readable error if opentelemetry has json encoding. Follow-up after [PR](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2570).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly validate scheme for `proxy_url` field at the scrape config. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4811) for details.