From 7349f18c55642306729366a493d422732d330bfd Mon Sep 17 00:00:00 2001 From: Tamara Vashchuk <98753789+aramattamara@users.noreply.github.com> Date: Fri, 18 Aug 2023 10:12:48 -0700 Subject: [PATCH] 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> --- app/vmselect/main.go | 5 ++ app/vmselect/prometheus/prometheus.go | 19 +++++++ .../src/components/Main/Button/style.scss | 4 ++ .../vmui/src/components/Main/Icons/index.tsx | 11 ++++ .../packages/vmui/src/hooks/useFetchQuery.ts | 8 +-- .../QueryConfigurator/QueryConfigurator.tsx | 53 ++++++++++++++++--- .../hooks/usePrettifyQuery.ts | 49 +++++++++++++++++ .../CustomPanel/QueryConfigurator/style.scss | 2 +- .../vmui/src/pages/CustomPanel/index.tsx | 5 +- docs/CHANGELOG.md | 1 + 10 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/hooks/usePrettifyQuery.ts diff --git a/app/vmselect/main.go b/app/vmselect/main.go index 7f10095ddc..7ee0a6e90e 100644 --- a/app/vmselect/main.go +++ b/app/vmselect/main.go @@ -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"}`) diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go index 4cbed0d3bf..ee11979678 100644 --- a/app/vmselect/prometheus/prometheus.go +++ b/app/vmselect/prometheus/prometheus.go @@ -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) diff --git a/app/vmui/packages/vmui/src/components/Main/Button/style.scss b/app/vmui/packages/vmui/src/components/Main/Button/style.scss index 0a0bc1e2b4..24360b758c 100644 --- a/app/vmui/packages/vmui/src/components/Main/Button/style.scss +++ b/app/vmui/packages/vmui/src/components/Main/Button/style.scss @@ -45,6 +45,10 @@ $button-radius: 6px; transform: translateZ(-1px); } + &:active:after { + transform: scale(0.9); + } + span { display: grid; align-items: center; diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx index 82ef532c2e..6e96b3cee6 100644 --- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx @@ -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" diff --git a/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts b/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts index 7576ecda26..e4f418983e 100644 --- a/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts +++ b/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts @@ -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 }; }; diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx index 23e581a22a..b35cbc5e4b 100644 --- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx +++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx @@ -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"> diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/hooks/usePrettifyQuery.ts b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/hooks/usePrettifyQuery.ts new file mode 100644 index 0000000000..da6589fe77 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/hooks/usePrettifyQuery.ts @@ -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; +}; diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss index fb39cfb0a6..a6a564da45 100644 --- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss +++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss @@ -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; diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx index 4e169c1e80..fb170f9bd7 100644 --- a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx +++ b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx @@ -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} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9703d85939..f59db25ed6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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.