From 68e31a6000cd7dc59a6a5f5851437954f235458f Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Tue, 25 Apr 2023 10:40:01 +0200 Subject: [PATCH] vmui: Integrate WITH template playground (#3831) * feat: add WithTemplate page * app/vmselect/prometheus: enable json mode for expand with expr API * app/vmselect/prometheus: enable CORS and add content type * feat: add api for expand with templates * fix: remove console from useExpandWithExprs * app/vmselect/prometheus: fix escaping * vmui: integrate WITH template * app/vmctl: check content type instead of form param * fix: add content-type for fetch with-exprs * fix: add a header to the server's response that allows the "Content-Type" header * app/vmctl: added comment and cleanup * app/vmctl: use format query param --------- Co-authored-by: dmitryk-dk --- app/vmselect/main.go | 1 + .../prometheus/expand-with-exprs.qtpl | 29 +++ .../prometheus/expand-with-exprs.qtpl.go | 225 +++++++++++++----- app/vmselect/prometheus/prometheus.go | 9 +- app/vmui/packages/vmui/src/App.tsx | 5 + .../vmui/src/api/expand-with-exprs.ts | 2 + .../Layout/Header/HeaderNav/HeaderNav.tsx | 13 +- .../Main/CodeExample/CodeExample.tsx | 45 ++++ .../components/Main/CodeExample/style.scss | 17 ++ .../vmui/src/pages/CardinalityPanel/index.tsx | 4 +- .../vmui/src/pages/TopQueries/index.tsx | 5 +- .../WithTemplateTutorial.tsx | 218 +++++++++++++++++ .../WithTemplateTutorial/style.scss | 22 ++ .../WithTemplate/hooks/useExpandWithExprs.ts | 37 +++ .../vmui/src/pages/WithTemplate/index.tsx | 63 +++++ .../vmui/src/pages/WithTemplate/style.scss | 34 +++ app/vmui/packages/vmui/src/router/index.ts | 7 +- docs/CHANGELOG.md | 1 + 18 files changed, 664 insertions(+), 73 deletions(-) create mode 100644 app/vmui/packages/vmui/src/api/expand-with-exprs.ts create mode 100644 app/vmui/packages/vmui/src/components/Main/CodeExample/CodeExample.tsx create mode 100644 app/vmui/packages/vmui/src/components/Main/CodeExample/style.scss create mode 100644 app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/WithTemplateTutorial.tsx create mode 100644 app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/style.scss create mode 100644 app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts create mode 100644 app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx create mode 100644 app/vmui/packages/vmui/src/pages/WithTemplate/style.scss diff --git a/app/vmselect/main.go b/app/vmselect/main.go index 8f4d43129b..157bb5cb72 100644 --- a/app/vmselect/main.go +++ b/app/vmselect/main.go @@ -469,6 +469,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool { return true case "/expand-with-exprs": expandWithExprsRequests.Inc() + httpserver.EnableCORS(w, r) prometheus.ExpandWithExprs(w, r) return true case "/api/v1/rules", "/rules": diff --git a/app/vmselect/prometheus/expand-with-exprs.qtpl b/app/vmselect/prometheus/expand-with-exprs.qtpl index 4ee21da6d5..5e42d71a57 100644 --- a/app/vmselect/prometheus/expand-with-exprs.qtpl +++ b/app/vmselect/prometheus/expand-with-exprs.qtpl @@ -1,4 +1,5 @@ {% import ( + "fmt" "github.com/VictoriaMetrics/metricsql" ) %} @@ -243,3 +244,31 @@ WITH ( {% endfunc %} + +{% stripspace %} +{% func ExpandWithExprsJSONResponse(q string) %} + {%= expandWithExprsJSON(q) %} +{% endfunc %} + +{% func expandWithExprsJSON(q string) %} + {% if len(q) == 0 %} + { + "status": "error", + "error": "query string cannot be empty" + } + {% return %} + {% endif %} + +{ + {% code expr, err := metricsql.Parse(q) %} + {% if err != nil %} + "status": "error", + "error": {%q= fmt.Sprintf("Cannot parse query: %s", err) %} + {% else %} + {% code expr = metricsql.Optimize(expr) %} + "status": "success", + "expr": {%q= string(expr.AppendString(nil)) %} + {% endif %} +} +{% endfunc %} +{% endstripspace %} diff --git a/app/vmselect/prometheus/expand-with-exprs.qtpl.go b/app/vmselect/prometheus/expand-with-exprs.qtpl.go index 58cf119b96..aa65c7fec0 100644 --- a/app/vmselect/prometheus/expand-with-exprs.qtpl.go +++ b/app/vmselect/prometheus/expand-with-exprs.qtpl.go @@ -6,127 +6,128 @@ package prometheus //line app/vmselect/prometheus/expand-with-exprs.qtpl:1 import ( + "fmt" "github.com/VictoriaMetrics/metricsql" ) // ExpandWithExprsResponse returns a webpage, which expands with templates in q MetricsQL. -//line app/vmselect/prometheus/expand-with-exprs.qtpl:8 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:9 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:8 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:9 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:8 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:9 func StreamExpandWithExprsResponse(qw422016 *qt422016.Writer, q string) { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:8 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:9 qw422016.N().S(`Expand WITH expressions

MetricsQL query with optional WITH expressions:


MetricsQL query after expanding WITH expressions and applying other optimizations:

`) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:36 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:37 streamwithExprsTutorial(qw422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:36 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:37 qw422016.N().S(`
`) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 func WriteExpandWithExprsResponse(qq422016 qtio422016.Writer, q string) { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 StreamExpandWithExprsResponse(qw422016, q) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 qt422016.ReleaseWriter(qw422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 func ExpandWithExprsResponse(q string) string { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 WriteExpandWithExprsResponse(qb422016, q) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 qs422016 := string(qb422016.B) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 return qs422016 -//line app/vmselect/prometheus/expand-with-exprs.qtpl:40 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:41 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:42 -func streamexpandWithExprs(qw422016 *qt422016.Writer, q string) { //line app/vmselect/prometheus/expand-with-exprs.qtpl:43 - if len(q) == 0 { +func streamexpandWithExprs(qw422016 *qt422016.Writer, q string) { //line app/vmselect/prometheus/expand-with-exprs.qtpl:44 - return + if len(q) == 0 { //line app/vmselect/prometheus/expand-with-exprs.qtpl:45 + return +//line app/vmselect/prometheus/expand-with-exprs.qtpl:46 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:47 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:48 expr, err := metricsql.Parse(q) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:48 - if err != nil { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:48 - qw422016.N().S(`Cannot parse query:`) //line app/vmselect/prometheus/expand-with-exprs.qtpl:49 - qw422016.E().V(err) + if err != nil { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:49 + qw422016.N().S(`Cannot parse query:`) //line app/vmselect/prometheus/expand-with-exprs.qtpl:50 - } else { + qw422016.E().V(err) //line app/vmselect/prometheus/expand-with-exprs.qtpl:51 + } else { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:52 expr = metricsql.Optimize(expr) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:52 - qw422016.E().Z(expr.AppendString(nil)) //line app/vmselect/prometheus/expand-with-exprs.qtpl:53 + qw422016.E().Z(expr.AppendString(nil)) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 func writeexpandWithExprs(qq422016 qtio422016.Writer, q string) { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 streamexpandWithExprs(qw422016, q) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 qt422016.ReleaseWriter(qw422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 func expandWithExprs(q string) string { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 writeexpandWithExprs(qb422016, q) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 qs422016 := string(qb422016.B) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 return qs422016 -//line app/vmselect/prometheus/expand-with-exprs.qtpl:54 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:55 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:58 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:59 func streamwithExprsTutorial(qw422016 *qt422016.Writer) { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:58 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:59 qw422016.N().S(`

Tutorial for WITH expressions in MetricsQL

@@ -315,31 +316,127 @@ WITH ( `) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 func writewithExprsTutorial(qq422016 qtio422016.Writer) { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 streamwithExprsTutorial(qw422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 qt422016.ReleaseWriter(qw422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 } -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 func withExprsTutorial() string { -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 writewithExprsTutorial(qb422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 qs422016 := string(qb422016.B) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 return qs422016 -//line app/vmselect/prometheus/expand-with-exprs.qtpl:245 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:246 +} + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:249 +func StreamExpandWithExprsJSONResponse(qw422016 *qt422016.Writer, q string) { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:250 + streamexpandWithExprsJSON(qw422016, q) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 +} + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 +func WriteExpandWithExprsJSONResponse(qq422016 qtio422016.Writer, q string) { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + StreamExpandWithExprsJSONResponse(qw422016, q) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + qt422016.ReleaseWriter(qw422016) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 +} + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 +func ExpandWithExprsJSONResponse(q string) string { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + WriteExpandWithExprsJSONResponse(qb422016, q) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + qs422016 := string(qb422016.B) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 + return qs422016 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:251 +} + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:253 +func streamexpandWithExprsJSON(qw422016 *qt422016.Writer, q string) { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:254 + if len(q) == 0 { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:254 + qw422016.N().S(`{"status": "error","error": "query string cannot be empty"}`) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:259 + return +//line app/vmselect/prometheus/expand-with-exprs.qtpl:260 + } +//line app/vmselect/prometheus/expand-with-exprs.qtpl:260 + qw422016.N().S(`{`) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:263 + expr, err := metricsql.Parse(q) + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:264 + if err != nil { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:264 + qw422016.N().S(`"status": "error","error":`) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:266 + qw422016.N().Q(fmt.Sprintf("Cannot parse query: %s", err)) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:267 + } else { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:268 + expr = metricsql.Optimize(expr) + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:268 + qw422016.N().S(`"status": "success","expr":`) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:270 + qw422016.N().Q(string(expr.AppendString(nil))) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:271 + } +//line app/vmselect/prometheus/expand-with-exprs.qtpl:271 + qw422016.N().S(`}`) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 +} + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 +func writeexpandWithExprsJSON(qq422016 qtio422016.Writer, q string) { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + streamexpandWithExprsJSON(qw422016, q) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + qt422016.ReleaseWriter(qw422016) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 +} + +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 +func expandWithExprsJSON(q string) string { +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + writeexpandWithExprsJSON(qb422016, q) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + qs422016 := string(qb422016.B) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 + return qs422016 +//line app/vmselect/prometheus/expand-with-exprs.qtpl:273 } diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go index da71e71ce6..d74bbb9b64 100644 --- a/app/vmselect/prometheus/prometheus.go +++ b/app/vmselect/prometheus/prometheus.go @@ -60,10 +60,17 @@ const defaultStep = 5 * 60 * 1000 // ExpandWithExprs handles the request to /expand-with-exprs func ExpandWithExprs(w http.ResponseWriter, r *http.Request) { + query := r.FormValue("query") + format := r.FormValue("format") bw := bufferedwriter.Get(w) defer bufferedwriter.Put(bw) - WriteExpandWithExprsResponse(bw, query) + if format == "json" { + w.Header().Set("Content-Type", "application/json") + WriteExpandWithExprsJSONResponse(bw, query) + } else { + WriteExpandWithExprsResponse(bw, query) + } _ = bw.Flush() } diff --git a/app/vmui/packages/vmui/src/App.tsx b/app/vmui/packages/vmui/src/App.tsx index 331fb267cc..7d8926d0eb 100644 --- a/app/vmui/packages/vmui/src/App.tsx +++ b/app/vmui/packages/vmui/src/App.tsx @@ -11,6 +11,7 @@ import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider"; import TracePage from "./pages/TracePage"; import ExploreMetrics from "./pages/ExploreMetrics"; import PreviewIcons from "./components/Main/Icons/PreviewIcons"; +import WithTemplate from "./pages/WithTemplate"; const App: FC = () => { @@ -51,6 +52,10 @@ const App: FC = () => { path={router.dashboards} element={} /> + } + /> } diff --git a/app/vmui/packages/vmui/src/api/expand-with-exprs.ts b/app/vmui/packages/vmui/src/api/expand-with-exprs.ts new file mode 100644 index 0000000000..8b0da74976 --- /dev/null +++ b/app/vmui/packages/vmui/src/api/expand-with-exprs.ts @@ -0,0 +1,2 @@ +export const getExpandWithExprUrl = (server: string, query: string): string => + `${server}/expand-with-exprs?query=${query}&format=json`; diff --git a/app/vmui/packages/vmui/src/components/Layout/Header/HeaderNav/HeaderNav.tsx b/app/vmui/packages/vmui/src/components/Layout/Header/HeaderNav/HeaderNav.tsx index 1dbe8fb48c..d737339ede 100644 --- a/app/vmui/packages/vmui/src/components/Layout/Header/HeaderNav/HeaderNav.tsx +++ b/app/vmui/packages/vmui/src/components/Layout/Header/HeaderNav/HeaderNav.tsx @@ -45,8 +45,17 @@ const HeaderNav: FC = ({ color, background, direction }) => { ] }, { - label: routerOptions[router.trace].title, - value: router.trace, + label: "Tools", + submenu: [ + { + label: routerOptions[router.trace].title, + value: router.trace, + }, + { + label: routerOptions[router.withTemplate].title, + value: router.withTemplate, + }, + ] }, { label: routerOptions[router.dashboards].title, diff --git a/app/vmui/packages/vmui/src/components/Main/CodeExample/CodeExample.tsx b/app/vmui/packages/vmui/src/components/Main/CodeExample/CodeExample.tsx new file mode 100644 index 0000000000..6e0257641f --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Main/CodeExample/CodeExample.tsx @@ -0,0 +1,45 @@ +import React, { FC, useEffect } from "preact/compat"; +import "./style.scss"; +import { useState } from "react"; +import Tooltip from "../Tooltip/Tooltip"; +import Button from "../Button/Button"; +import { CopyIcon } from "../Icons"; + +enum CopyState { copy = "Copy", copied = "Copied" } + +const CodeExample: FC<{code: string}> = ({ code }) => { + const [tooltip, setTooltip] = useState(CopyState.copy); + const handlerCopy = () => { + navigator.clipboard.writeText(code); + setTooltip(CopyState.copied); + }; + + useEffect(() => { + let timeout: NodeJS.Timeout | null = null; + if (tooltip === CopyState.copied) { + timeout = setTimeout(() => setTooltip(CopyState.copy), 1000); + } + + return () => { + timeout && clearTimeout(timeout); + }; + }, [tooltip]); + + return ( + + {code} +
+ +
+
+ ); +}; + +export default CodeExample; diff --git a/app/vmui/packages/vmui/src/components/Main/CodeExample/style.scss b/app/vmui/packages/vmui/src/components/Main/CodeExample/style.scss new file mode 100644 index 0000000000..17cb7b7ce1 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Main/CodeExample/style.scss @@ -0,0 +1,17 @@ +@use "src/styles/variables" as *; + +.vm-code-example { + position: relative; + display: block; + padding: $padding-global; + white-space: pre-wrap; + border-radius: $border-radius-small; + background-color: rgba($color-black, 0.05); + overflow: auto; + + &__copy { + position: absolute; + right: 10px; + top: 10px; + } +} diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx index c333f0d393..b005d9f5da 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx @@ -21,7 +21,7 @@ import { const spinnerMessage = `Please wait while cardinality stats is calculated. This may take some time if the db contains big number of time series.`; -const Index: FC = () => { +const CardinalityPanel: FC = () => { const { isMobile } = useDeviceDetect(); const [searchParams, setSearchParams] = useSearchParams(); @@ -88,4 +88,4 @@ const Index: FC = () => { ); }; -export default Index; +export default CardinalityPanel; diff --git a/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx b/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx index 31eaec8c31..c34a87067f 100644 --- a/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx +++ b/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx @@ -19,9 +19,8 @@ import classNames from "classnames"; const exampleDuration = "30ms, 15s, 3d4h, 1y2w"; -const Index: FC = () => { +const TopQueries: FC = () => { const { isMobile } = useDeviceDetect(); - const { data, error, loading } = useFetchTopQueries(); const { topN, maxLifetime } = useTopQueriesState(); const topQueriesDispatch = useTopQueriesDispatch(); @@ -180,4 +179,4 @@ const Index: FC = () => { ); }; -export default Index; +export default TopQueries; diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/WithTemplateTutorial.tsx b/app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/WithTemplateTutorial.tsx new file mode 100644 index 0000000000..b6e4b698a1 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/WithTemplateTutorial.tsx @@ -0,0 +1,218 @@ +import React, { FC } from "preact/compat"; +import "./style.scss"; +import CodeExample from "../../../components/Main/CodeExample/CodeExample"; + +const MetricsQL = () => ( + + MetricsQL + +); + +const NodeExporterFull = () => ( + + Node Exporter Full + +); + +const WithTemplateTutorial: FC = () => ( +
+

+ Tutorial for WITH expressions in +

+
+

Let's look at the following real query from dashboard:

+ +

+ It is clear the query calculates the percentage of used memory for the given $node, $port and $job. + Isn't it? :) +

+
+
+

+ What's wrong with this query? + Copy-pasted label filters for distinct timeseries which makes it easy + to mistype these filters during modification. Let's simplify the query with WITH expressions: +

+ +
+
+

+ Now label filters are located in a single place instead of three distinct places. + The query mentions node_memory_MemTotal_bytes metric twice and {"{commonFilters}"} three times. + WITH expressions may improve this: +

+ +

+ Now the template function my_resource_utilization() may be used + for monitoring arbitrary resources - memory, CPU, network, storage, you name it. +

+
+
+

+ Let's take another nice query from dashboard: +

+ +

+ Do you understand what does this mess do? Is it manageable? :) + WITH expressions are happy to help in a few iterations. +

+
+ +
+

+ 1. Extract common filters used in multiple places into a commonFilters variable: +

+ +
+
+

+ 2. Extract "count(count(...) by (cpu))" into cpuCount variable: +

+ +
+
+

+ 3. Extract rate(...) part into cpuIdle variable, + since it is clear now that this part calculates the number of idle CPUs: +

+ +
+
+

+ 4. Put node_cpu_seconds_total{"{commonFilters}"} into its own varialbe with the name cpuSeconds: +

+ +

+ Now the query became more clear comparing to the initial query. +

+
+
+

+ WITH expressions may be nested and may be put anywhere. Try expanding the following query: +

+ +
+
+); + +export default WithTemplateTutorial; diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/style.scss b/app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/style.scss new file mode 100644 index 0000000000..7933bfffbf --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/WithTemplateTutorial/style.scss @@ -0,0 +1,22 @@ +@use "src/styles/variables" as *; + +.vm-with-template-tutorial { + display: grid; + gap: $padding-large; + + &__title { + font-size: $font-size-large; + font-weight: bold; + } + + &-section { + display: grid; + gap: $padding-global; + + &__text { + font-size: $font-size-medium; + line-height: 130%; + max-width: 720px; + } + } +} diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts b/app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts new file mode 100644 index 0000000000..28e13db947 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts @@ -0,0 +1,37 @@ +import { useAppState } from "../../../state/common/StateContext"; +import { useState } from "react"; +import { ErrorTypes } from "../../../types"; +import { getExpandWithExprUrl } from "../../../api/expand-with-exprs"; + +export const useExpandWithExprs = () => { + const { serverUrl } = useAppState(); + + const [data, setData] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + + const fetchData = async (query: string) => { + const fetchUrl = getExpandWithExprUrl(serverUrl, query); + setLoading(true); + try { + const response = await fetch(fetchUrl); + + const resp = await response.json(); + + setData(resp?.expr || ""); + setError(String(resp.error || "")); + } catch (e) { + if (e instanceof Error && e.name !== "AbortError") { + setError(`${e.name}: ${e.message}`); + } + } + setLoading(false); + }; + + return { + data, + error, + loading, + expand: fetchData + }; +}; diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx b/app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx new file mode 100644 index 0000000000..27097a1744 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx @@ -0,0 +1,63 @@ +import React, { FC } from "preact/compat"; +import "./style.scss"; +import TextField from "../../components/Main/TextField/TextField"; +import { useState } from "react"; +import Button from "../../components/Main/Button/Button"; +import { PlayIcon } from "../../components/Main/Icons"; +import WithTemplateTutorial from "./WithTemplateTutorial/WithTemplateTutorial"; +import { useExpandWithExprs } from "./hooks/useExpandWithExprs"; +import Spinner from "../../components/Main/Spinner/Spinner"; + +const WithTemplate: FC = () => { + const { data, loading, error, expand } = useExpandWithExprs(); + const [expr, setExpr] = useState(""); + + const handleChangeInput = (val: string) => { + setExpr(val); + }; + + const handleRunQuery = () => { + expand(expr); + }; + + return ( +
+ {loading && } + +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ ); +}; + +export default WithTemplate; diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/style.scss b/app/vmui/packages/vmui/src/pages/WithTemplate/style.scss new file mode 100644 index 0000000000..71a79e7853 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/style.scss @@ -0,0 +1,34 @@ +@use "src/styles/variables" as *; + +.vm-with-template { + display: grid; + gap: $padding-medium; + + &-body { + display: grid; + gap: $padding-global; + align-items: flex-start; + width: 100%; + + &-top { + display: flex; + gap: $padding-small; + align-items: center; + justify-content: flex-end; + } + + &__expr textarea { + min-height: 200px; + } + + &__result textarea { + min-height: 60px; + } + + textarea { + overflow: auto; + width: 100%; + height: 100%; + } + } +} diff --git a/app/vmui/packages/vmui/src/router/index.ts b/app/vmui/packages/vmui/src/router/index.ts index b68ace2da9..ae28d03d70 100644 --- a/app/vmui/packages/vmui/src/router/index.ts +++ b/app/vmui/packages/vmui/src/router/index.ts @@ -5,7 +5,8 @@ const router = { cardinality: "/cardinality", topQueries: "/top-queries", trace: "/trace", - icons: "/icons" + withTemplate: "/expand-with-exprs", + icons: "/icons" // for dev }; export interface RouterOptionsHeader { @@ -65,6 +66,10 @@ export const routerOptions: {[key: string]: RouterOptions} = { title: "Dashboards", ...routerOptionsDefault, }, + [router.withTemplate]: { + title: "WITH templates", + header: {} + }, [router.icons]: { title: "Icons", header: {} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1cd915daed..e25d5ac643 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -160,6 +160,7 @@ Released at 2023-02-24 * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): show `median` instead of `avg` in graph tooltip and line legend, since `median` is more tolerant against spikes. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3706). * FEATURE: add `-search.maxSeriesPerAggrFunc` command-line flag, which can be used for limiting the number of time series [MetricsQL aggregate functions](https://docs.victoriametrics.com/MetricsQL.html#aggregate-functions) can return in a single query. This flag can be useful for preventing OOMs when [count_values](https://docs.victoriametrics.com/MetricsQL.html#count_values) function is improperly used. * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): small UX improvements for mobile view. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3707) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3848). +* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): integrate WITH template playground. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3811). * FEATURE: add `-search.logQueryMemoryUsage` command-line flag for logging queries, which need more memory than specified by this command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3553). Thanks to @michal-kralik for the idea and the intial implementation. * FEATURE: allow setting zero value for `-search.latencyOffset` command-line flag. This may be needed in [some cases](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836). Previously the minimum supported value for `-search.latencyOffset` command-line flag was `1s`.