mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: use Chart.js as default engine for graph (#1634)
* feat: add Plotly as default engine for graph * fix: remove unused components * feat: use Chart.js as default engine graph * fix: correct styles for loader * feat: add zoom/pan for chart * feat: add height for chart * fix: remove unused code * fix: remove empty units from duration * fix: change debounce for pan to 500ms * fix: add utility for plugins register globally * fix: optimize render graph * feat: add buffer data for zoom * fix: add limits for zoom in/out * fix: change update data while zooming * app/vmselect: `make vmui-update` Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
4b3951fd86
commit
80df31b2ee
29 changed files with 420 additions and 1641 deletions
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.6452b577.chunk.css",
|
||||
"main.js": "./static/js/main.801aa0ec.chunk.js",
|
||||
"runtime-main.js": "./static/js/runtime-main.55798746.js",
|
||||
"static/js/2.fd2c2c30.chunk.js": "./static/js/2.fd2c2c30.chunk.js",
|
||||
"static/js/3.c36fc28c.chunk.js": "./static/js/3.c36fc28c.chunk.js",
|
||||
"main.css": "./static/css/main.0d21f12a.chunk.css",
|
||||
"main.js": "./static/js/main.a0567498.chunk.js",
|
||||
"runtime-main.js": "./static/js/runtime-main.4736dc3a.js",
|
||||
"static/js/2.c663f8e1.chunk.js": "./static/js/2.c663f8e1.chunk.js",
|
||||
"static/js/3.9f518d6d.chunk.js": "./static/js/3.9f518d6d.chunk.js",
|
||||
"index.html": "./index.html",
|
||||
"static/js/2.fd2c2c30.chunk.js.LICENSE.txt": "./static/js/2.fd2c2c30.chunk.js.LICENSE.txt"
|
||||
"static/js/2.c663f8e1.chunk.js.LICENSE.txt": "./static/js/2.c663f8e1.chunk.js.LICENSE.txt"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/runtime-main.55798746.js",
|
||||
"static/js/2.fd2c2c30.chunk.js",
|
||||
"static/css/main.6452b577.chunk.css",
|
||||
"static/js/main.801aa0ec.chunk.js"
|
||||
"static/js/runtime-main.4736dc3a.js",
|
||||
"static/js/2.c663f8e1.chunk.js",
|
||||
"static/css/main.0d21f12a.chunk.css",
|
||||
"static/js/main.a0567498.chunk.js"
|
||||
]
|
||||
}
|
|
@ -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="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><link href="./static/css/main.6452b577.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);p.length;)p.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var c=t[a];0!==o[c]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"c36fc28c"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="./",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpvmui=this.webpackJsonpvmui||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([])</script><script src="./static/js/2.fd2c2c30.chunk.js"></script><script src="./static/js/main.801aa0ec.chunk.js"></script></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="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><link href="./static/css/main.0d21f12a.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);p.length;)p.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var c=t[a];0!==o[c]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"9f518d6d"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="./",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpvmui=this.webpackJsonpvmui||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([])</script><script src="./static/js/2.c663f8e1.chunk.js"></script><script src="./static/js/main.a0567498.chunk.js"></script></body></html>
|
|
@ -1 +1 @@
|
|||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,"Courier New",monospace}.MuiAccordionSummary-content{margin:10px 0!important}.cm-activeLine{background-color:inherit!important}.cm-wrap{border-radius:4px;border:1px solid #b9b9b9;font-size:10px}.one-line-scroll .cm-wrap{height:24px}.cm-content,.cm-gutter{min-height:51px}.one-line-scroll .cm-content,.one-line-scroll .cm-gutter{min-height:auto}.line{fill:none;stroke-width:2}.overlay{fill:none;pointer-events:all}.dot{fill:#621773;stroke:#fff}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,"Courier New",monospace}.MuiAccordionSummary-content{margin:10px 0!important}.cm-activeLine{background-color:inherit!important}.cm-wrap{border-radius:4px;border:1px solid #b9b9b9;font-size:10px}.one-line-scroll .cm-wrap{height:24px}.cm-content,.cm-gutter{min-height:51px}.one-line-scroll .cm-content,.one-line-scroll .cm-gutter{min-height:auto}
|
2
app/vmselect/vmui/static/js/2.c663f8e1.chunk.js
Normal file
2
app/vmselect/vmui/static/js/2.c663f8e1.chunk.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -4,6 +4,20 @@ object-assign
|
|||
@license MIT
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Chart.js v3.5.1
|
||||
* https://www.chartjs.org
|
||||
* (c) 2021 Chart.js Contributors
|
||||
* Released under the MIT License
|
||||
*/
|
||||
|
||||
/*!
|
||||
* chartjs-adapter-date-fns v2.0.0
|
||||
* https://www.chartjs.org
|
||||
* (c) 2021 chartjs-adapter-date-fns Contributors
|
||||
* Released under the MIT license
|
||||
*/
|
||||
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
|
@ -19,6 +33,12 @@ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|||
PERFORMANCE OF THIS SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
/*! Hammer.JS - v2.0.7 - 2016-04-22
|
||||
* http://hammerjs.github.io/
|
||||
*
|
||||
* Copyright (c) 2016 Jorik Tangelder;
|
||||
* Licensed under the MIT license */
|
||||
|
||||
/**
|
||||
* A better abstraction over CSS.
|
||||
*
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
(this.webpackJsonpvmui=this.webpackJsonpvmui||[]).push([[3],{432:function(t,n,e){"use strict";e.r(n),e.d(n,"getCLS",(function(){return l})),e.d(n,"getFCP",(function(){return g})),e.d(n,"getFID",(function(){return h})),e.d(n,"getLCP",(function(){return y})),e.d(n,"getTTFB",(function(){return F}));var i,a,r=function(){return"".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)},o=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;return{name:t,value:n,delta:0,entries:[],id:r(),isFinal:!1}},u=function(t,n){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){var e=new PerformanceObserver((function(t){return t.getEntries().map(n)}));return e.observe({type:t,buffered:!0}),e}}catch(t){}},s=!1,c=!1,d=function(t){s=!t.persisted},f=function(){addEventListener("pagehide",d),addEventListener("beforeunload",(function(){}))},p=function(t){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];c||(f(),c=!0),addEventListener("visibilitychange",(function(n){var e=n.timeStamp;"hidden"===document.visibilityState&&t({timeStamp:e,isUnloading:s})}),{capture:!0,once:n})},v=function(t,n,e,i){var a;return function(){e&&n.isFinal&&e.disconnect(),n.value>=0&&(i||n.isFinal||"hidden"===document.visibilityState)&&(n.delta=n.value-(a||0),(n.delta||n.isFinal||void 0===a)&&(t(n),a=n.value))}},l=function(t){var n,e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=o("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),n())},r=u("layout-shift",a);r&&(n=v(t,i,r,e),p((function(t){var e=t.isUnloading;r.takeRecords().map(a),e&&(i.isFinal=!0),n()})))},m=function(){return void 0===i&&(i="hidden"===document.visibilityState?0:1/0,p((function(t){var n=t.timeStamp;return i=n}),!0)),{get timeStamp(){return i}}},g=function(t){var n,e=o("FCP"),i=m(),a=u("paint",(function(t){"first-contentful-paint"===t.name&&t.startTime<i.timeStamp&&(e.value=t.startTime,e.isFinal=!0,e.entries.push(t),n())}));a&&(n=v(t,e,a))},h=function(t){var n=o("FID"),e=m(),i=function(t){t.startTime<e.timeStamp&&(n.value=t.processingStart-t.startTime,n.entries.push(t),n.isFinal=!0,r())},a=u("first-input",i),r=v(t,n,a);a?p((function(){a.takeRecords().map(i),a.disconnect()}),!0):window.perfMetrics&&window.perfMetrics.onFirstInputDelay&&window.perfMetrics.onFirstInputDelay((function(t,i){i.timeStamp<e.timeStamp&&(n.value=t,n.isFinal=!0,n.entries=[{entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+t}],r())}))},S=function(){return a||(a=new Promise((function(t){return["scroll","keydown","pointerdown"].map((function(n){addEventListener(n,t,{once:!0,passive:!0,capture:!0})}))}))),a},y=function(t){var n,e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=o("LCP"),a=m(),r=function(t){var e=t.startTime;e<a.timeStamp?(i.value=e,i.entries.push(t)):i.isFinal=!0,n()},s=u("largest-contentful-paint",r);if(s){n=v(t,i,s,e);var c=function(){i.isFinal||(s.takeRecords().map(r),i.isFinal=!0,n())};S().then(c),p(c,!0)}},F=function(t){var n,e=o("TTFB");n=function(){try{var n=performance.getEntriesByType("navigation")[0]||function(){var t=performance.timing,n={entryType:"navigation",startTime:0};for(var e in t)"navigationStart"!==e&&"toJSON"!==e&&(n[e]=Math.max(t[e]-t.navigationStart,0));return n}();e.value=e.delta=n.responseStart,e.entries=[n],e.isFinal=!0,t(e)}catch(t){}},"complete"===document.readyState?setTimeout(n,0):addEventListener("pageshow",n)}}}]);
|
||||
(this.webpackJsonpvmui=this.webpackJsonpvmui||[]).push([[3],{423:function(t,n,e){"use strict";e.r(n),e.d(n,"getCLS",(function(){return l})),e.d(n,"getFCP",(function(){return g})),e.d(n,"getFID",(function(){return h})),e.d(n,"getLCP",(function(){return y})),e.d(n,"getTTFB",(function(){return F}));var i,a,r=function(){return"".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)},o=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;return{name:t,value:n,delta:0,entries:[],id:r(),isFinal:!1}},u=function(t,n){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){var e=new PerformanceObserver((function(t){return t.getEntries().map(n)}));return e.observe({type:t,buffered:!0}),e}}catch(t){}},s=!1,c=!1,d=function(t){s=!t.persisted},f=function(){addEventListener("pagehide",d),addEventListener("beforeunload",(function(){}))},p=function(t){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];c||(f(),c=!0),addEventListener("visibilitychange",(function(n){var e=n.timeStamp;"hidden"===document.visibilityState&&t({timeStamp:e,isUnloading:s})}),{capture:!0,once:n})},v=function(t,n,e,i){var a;return function(){e&&n.isFinal&&e.disconnect(),n.value>=0&&(i||n.isFinal||"hidden"===document.visibilityState)&&(n.delta=n.value-(a||0),(n.delta||n.isFinal||void 0===a)&&(t(n),a=n.value))}},l=function(t){var n,e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=o("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),n())},r=u("layout-shift",a);r&&(n=v(t,i,r,e),p((function(t){var e=t.isUnloading;r.takeRecords().map(a),e&&(i.isFinal=!0),n()})))},m=function(){return void 0===i&&(i="hidden"===document.visibilityState?0:1/0,p((function(t){var n=t.timeStamp;return i=n}),!0)),{get timeStamp(){return i}}},g=function(t){var n,e=o("FCP"),i=m(),a=u("paint",(function(t){"first-contentful-paint"===t.name&&t.startTime<i.timeStamp&&(e.value=t.startTime,e.isFinal=!0,e.entries.push(t),n())}));a&&(n=v(t,e,a))},h=function(t){var n=o("FID"),e=m(),i=function(t){t.startTime<e.timeStamp&&(n.value=t.processingStart-t.startTime,n.entries.push(t),n.isFinal=!0,r())},a=u("first-input",i),r=v(t,n,a);a?p((function(){a.takeRecords().map(i),a.disconnect()}),!0):window.perfMetrics&&window.perfMetrics.onFirstInputDelay&&window.perfMetrics.onFirstInputDelay((function(t,i){i.timeStamp<e.timeStamp&&(n.value=t,n.isFinal=!0,n.entries=[{entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+t}],r())}))},S=function(){return a||(a=new Promise((function(t){return["scroll","keydown","pointerdown"].map((function(n){addEventListener(n,t,{once:!0,passive:!0,capture:!0})}))}))),a},y=function(t){var n,e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=o("LCP"),a=m(),r=function(t){var e=t.startTime;e<a.timeStamp?(i.value=e,i.entries.push(t)):i.isFinal=!0,n()},s=u("largest-contentful-paint",r);if(s){n=v(t,i,s,e);var c=function(){i.isFinal||(s.takeRecords().map(r),i.isFinal=!0,n())};S().then(c),p(c,!0)}},F=function(t){var n,e=o("TTFB");n=function(){try{var n=performance.getEntriesByType("navigation")[0]||function(){var t=performance.timing,n={entryType:"navigation",startTime:0};for(var e in t)"navigationStart"!==e&&"toJSON"!==e&&(n[e]=Math.max(t[e]-t.navigationStart,0));return n}();e.value=e.delta=n.responseStart,e.entries=[n],e.isFinal=!0,t(e)}catch(t){}},"complete"===document.readyState?setTimeout(n,0):addEventListener("pageshow",n)}}}]);
|
File diff suppressed because one or more lines are too long
1
app/vmselect/vmui/static/js/main.a0567498.chunk.js
Normal file
1
app/vmselect/vmui/static/js/main.a0567498.chunk.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);p.length;)p.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var c=t[a];0!==o[c]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"c36fc28c"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="./",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpvmui=this.webpackJsonpvmui||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);
|
||||
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);p.length;)p.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var c=t[a];0!==o[c]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"9f518d6d"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="./",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpvmui=this.webpackJsonpvmui||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);
|
1184
app/vmui/packages/vmui/package-lock.json
generated
1184
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,20 +13,26 @@
|
|||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/react": "^11.1.2",
|
||||
"@testing-library/user-event": "^12.2.2",
|
||||
"@types/d3": "^6.1.0",
|
||||
"@types/chart.js": "^2.9.34",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/node": "^12.19.4",
|
||||
"@types/qs": "^6.9.6",
|
||||
"@types/react": "^16.9.56",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react-measure": "^2.0.6",
|
||||
"chart.js": "^3.5.1",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-plugin-zoom": "^1.1.1",
|
||||
"codemirror-promql": "^0.10.2",
|
||||
"d3": "^6.2.0",
|
||||
"date-fns": "^2.23.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"qs": "^6.5.2",
|
||||
"react": "^17.0.1",
|
||||
"react-chartjs-2": "^3.0.5",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-measure": "^2.5.2",
|
||||
"react-scripts": "4.0.0",
|
||||
|
|
|
@ -3,7 +3,7 @@ import {Box, Popover, TextField, Typography} from "@material-ui/core";
|
|||
import { KeyboardDateTimePicker } from "@material-ui/pickers";
|
||||
import {TimeDurationPopover} from "./TimeDurationPopover";
|
||||
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
||||
import {dateFromSeconds, formatDateForNativeInput} from "../../../utils/time";
|
||||
import {checkDurationLimit, dateFromSeconds, formatDateForNativeInput} from "../../../utils/time";
|
||||
import {InlineBtn} from "../../common/InlineBtn";
|
||||
|
||||
interface TimeSelectorProps {
|
||||
|
@ -33,7 +33,9 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!durationStringFocused) {
|
||||
setDuration(durationString);
|
||||
const value = checkDurationLimit(durationString);
|
||||
setDurationString(value);
|
||||
setDuration(value);
|
||||
}
|
||||
}, [durationString, durationStringFocused]);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const useFetchQuery = (): {
|
|||
isLoading: boolean,
|
||||
graphData?: MetricResult[],
|
||||
liveData?: InstantMetricResult[],
|
||||
error?: string
|
||||
error?: string,
|
||||
} => {
|
||||
const {query, displayType, serverUrl, time: {period}} = useAppState();
|
||||
|
||||
|
@ -40,8 +40,10 @@ export const useFetchQuery = (): {
|
|||
return;
|
||||
}
|
||||
if (isValidHttpUrl(serverUrl)) {
|
||||
const duration = (period.end - period.start)/2;
|
||||
const doublePeriod = {...period, start: period.start - duration, end: period.end + duration};
|
||||
return displayType === "chart"
|
||||
? getQueryRangeUrl(serverUrl, query, period)
|
||||
? getQueryRangeUrl(serverUrl, query, doublePeriod)
|
||||
: getQueryUrl(serverUrl, query, period);
|
||||
} else {
|
||||
setError("Please provide a valid URL");
|
||||
|
@ -63,16 +65,21 @@ export const useFetchQuery = (): {
|
|||
headers.set("Authorization", bearerData?.token || "");
|
||||
}
|
||||
setIsLoading(true);
|
||||
const response = await fetch(fetchUrl, {
|
||||
headers
|
||||
});
|
||||
if (response.ok) {
|
||||
saveToStorage("LAST_QUERY", query);
|
||||
const resp = await response.json();
|
||||
setError(undefined);
|
||||
displayType === "chart" ? setGraphData(resp.data.result) : setLiveData(resp.data.result);
|
||||
} else {
|
||||
setError((await response.json())?.error);
|
||||
try {
|
||||
const response = await fetch(fetchUrl, {
|
||||
headers
|
||||
});
|
||||
if (response.ok) {
|
||||
saveToStorage("LAST_QUERY", query);
|
||||
const resp = await response.json();
|
||||
setError(undefined);
|
||||
displayType === "chart" ? setGraphData(resp.data.result) : setLiveData(resp.data.result);
|
||||
} else {
|
||||
setError((await response.json())?.error);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
@ -50,32 +50,32 @@ const HomeLayout: FC = () => {
|
|||
<UrlCopy url={fetchUrl}/>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box display="flex" flexDirection="column" style={{minHeight: "calc(100vh - 64px)"}}>
|
||||
<Box m={2}>
|
||||
<Box p={2} display="grid" gridTemplateRows="auto 1fr" gridGap={"20px"} style={{minHeight: "calc(100vh - 64px)"}}>
|
||||
<Box>
|
||||
<QueryConfigurator/>
|
||||
</Box>
|
||||
<Box flexShrink={1}>
|
||||
<Box height={"100%"}>
|
||||
{isLoading && <Fade in={isLoading} style={{
|
||||
transitionDelay: isLoading ? "300ms" : "0ms",
|
||||
}}>
|
||||
<Box alignItems="center" flexDirection="column" display="flex"
|
||||
<Box alignItems="center" justifyContent="center" flexDirection="column" display="flex"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "calc(100vh - 32px)",
|
||||
maxWidth: "calc(100vw - 32px)",
|
||||
position: "absolute",
|
||||
height: "150px",
|
||||
height: "50%",
|
||||
background: "linear-gradient(rgba(255,255,255,.7), rgba(255,255,255,.7), rgba(255,255,255,0))"
|
||||
}} m={2}>
|
||||
}}>
|
||||
<CircularProgress/>
|
||||
</Box>
|
||||
</Fade>}
|
||||
{<Box p={2}>
|
||||
{<Box height={"100%"} py={3} px={6} bgcolor={"#fff"}>
|
||||
{error &&
|
||||
<Alert color="error" style={{fontSize: "14px"}}>
|
||||
<Alert color="error" severity="error" style={{fontSize: "14px"}}>
|
||||
{error}
|
||||
</Alert>}
|
||||
{graphData && period && (displayType === "chart") &&
|
||||
<GraphView data={graphData} timePresets={period}></GraphView>}
|
||||
<GraphView data={graphData}/>}
|
||||
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
|
||||
{liveData && (displayType === "table") && <TableView data={liveData}/>}
|
||||
</Box>}
|
||||
|
|
|
@ -1,126 +1,16 @@
|
|||
import React, {FC, useEffect, useMemo, useState} from "react";
|
||||
import React, {FC} from "react";
|
||||
import {MetricResult} from "../../../api/types";
|
||||
|
||||
import {schemeCategory10, scaleOrdinal, interpolateRainbow, range as d3Range} from "d3";
|
||||
|
||||
import {LineChart} from "../../LineChart/LineChart";
|
||||
import {DataSeries, TimeParams} from "../../../types";
|
||||
import {getNameForMetric} from "../../../utils/metric";
|
||||
import {Legend, LegendItem} from "../../Legend/Legend";
|
||||
import {useSortedCategories} from "../../../hooks/useSortedCategories";
|
||||
import {InlineBtn} from "../../common/InlineBtn";
|
||||
import LineChart from "../../LineChart/LineChart";
|
||||
import "../../../utils/chartjs-register-plugins";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data: MetricResult[];
|
||||
timePresets: TimeParams
|
||||
data?: MetricResult[];
|
||||
}
|
||||
|
||||
const preDefinedScale = schemeCategory10;
|
||||
|
||||
const initialMaxAmount = 20;
|
||||
const showingIncrement = 20;
|
||||
|
||||
const GraphView: FC<GraphViewProps> = ({data, timePresets}) => {
|
||||
|
||||
const [showN, setShowN] = useState(initialMaxAmount);
|
||||
|
||||
const series: DataSeries[] = useMemo(() => {
|
||||
return data?.map(d => ({
|
||||
metadata: {
|
||||
name: getNameForMetric(d)
|
||||
},
|
||||
metric: d.metric,
|
||||
// VM metrics are tuples - much simpler to work with objects in chart
|
||||
values: d.values.map(v => ({
|
||||
key: v[0],
|
||||
value: +v[1]
|
||||
}))
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
const showingSeries = useMemo(() => series.slice(0 ,showN), [series, showN]);
|
||||
|
||||
const sortedCategories = useSortedCategories(data);
|
||||
|
||||
const seriesNames = useMemo(() => showingSeries.map(s => s.metadata.name), [showingSeries]);
|
||||
|
||||
// should not change as often as array of series names (for instance between executions of same query) to
|
||||
// keep related state (like selection of a labels)
|
||||
const [seriesNamesStable, setSeriesNamesStable] = useState(seriesNames);
|
||||
|
||||
useEffect(() => {
|
||||
// primitive way to check the fact that array contents are identical
|
||||
if (seriesNamesStable.join(",") !== seriesNames.join(",")) {
|
||||
setSeriesNamesStable(seriesNames);
|
||||
}
|
||||
}, [seriesNames, setSeriesNamesStable, seriesNamesStable]);
|
||||
|
||||
const amountOfSeries = useMemo(() => series.length, [series]);
|
||||
|
||||
const color = useMemo(() => {
|
||||
const len = seriesNamesStable.length;
|
||||
const scheme = len <= preDefinedScale.length
|
||||
? preDefinedScale
|
||||
: d3Range(len).map(d => d / len).map(interpolateRainbow); // dynamically generate n colors
|
||||
return scaleOrdinal<string>()
|
||||
.domain(seriesNamesStable) // associate series names with colors
|
||||
.range(scheme);
|
||||
}, [seriesNamesStable]);
|
||||
|
||||
|
||||
// changes only if names of series are different
|
||||
const initLabels = useMemo(() => {
|
||||
return seriesNamesStable.map(name => ({
|
||||
color: color(name),
|
||||
seriesName: name,
|
||||
labelData: showingSeries.find(s => s.metadata.name === name)?.metric, // find is O(n) - can do faster
|
||||
checked: true // init with checked always
|
||||
} as LegendItem));
|
||||
}, [color, seriesNamesStable]);
|
||||
|
||||
const [labels, setLabels] = useState(initLabels);
|
||||
|
||||
useEffect(() => {
|
||||
setLabels(initLabels);
|
||||
}, [initLabels]);
|
||||
|
||||
const visibleNames = useMemo(() => labels.filter(l => l.checked).map(l => l.seriesName), [labels]);
|
||||
|
||||
const visibleSeries = useMemo(() => showingSeries.filter(s => visibleNames.includes(s.metadata.name)), [showingSeries, visibleNames]);
|
||||
|
||||
const onLegendChange = (index: number) => {
|
||||
setLabels(prevState => {
|
||||
if (prevState) {
|
||||
const newState = [...prevState];
|
||||
newState[index] = {...newState[index], checked: !newState[index].checked};
|
||||
return newState;
|
||||
}
|
||||
return prevState;
|
||||
});
|
||||
};
|
||||
|
||||
const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
||||
return <>
|
||||
{(amountOfSeries > 0)
|
||||
? <>
|
||||
{amountOfSeries > initialMaxAmount && <div style={{textAlign: "center"}}>
|
||||
{amountOfSeries > showN
|
||||
? <span style={{fontStyle: "italic"}}>Showing only first {showN} of {amountOfSeries} series.
|
||||
{showN + showingIncrement >= amountOfSeries
|
||||
?
|
||||
<InlineBtn handler={() => setShowN(amountOfSeries)} text="Show all"/>
|
||||
:
|
||||
<>
|
||||
<InlineBtn handler={() => setShowN(prev => Math.min(prev + showingIncrement, amountOfSeries))} text={`Show ${showingIncrement} more`}/>,
|
||||
<InlineBtn handler={() => setShowN(amountOfSeries)} text="show all"/>.
|
||||
</>}
|
||||
</span>
|
||||
: <span style={{fontStyle: "italic"}}>Showing all series.
|
||||
<InlineBtn handler={() => setShowN(initialMaxAmount)} text={`Show only ${initialMaxAmount}`}/>.
|
||||
</span>}
|
||||
</div>}
|
||||
<LineChart height={400} series={visibleSeries} color={color} timePresets={timePresets} categories={sortedCategories}></LineChart>
|
||||
<Legend labels={labels} onChange={onLegendChange} categories={sortedCategories}></Legend>
|
||||
</>
|
||||
{(data.length > 0)
|
||||
? <LineChart data={data} />
|
||||
: <div style={{textAlign: "center"}}>No data to show</div>}
|
||||
</>;
|
||||
};
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import React, {useEffect, useRef} from "react";
|
||||
import {axisBottom, ScaleTime, select as d3Select} from "d3";
|
||||
|
||||
interface AxisBottomI {
|
||||
xScale: ScaleTime<number, number>;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const AxisBottom: React.FC<AxisBottomI> = ({xScale, height}) => {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ref = useRef<SVGSVGElement | any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
d3Select(ref.current)
|
||||
.call(axisBottom<Date>(xScale));
|
||||
}, [xScale]);
|
||||
return <g ref={ref} className="x axis" transform={`translate(0, ${height})`} />;
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
import React, {useEffect, useRef} from "react";
|
||||
import {axisLeft, ScaleLinear, select as d3Select} from "d3";
|
||||
import {format as d3Format} from "d3-format";
|
||||
|
||||
interface AxisLeftI {
|
||||
yScale: ScaleLinear<number, number>;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const yFormatter = (val: number): string => {
|
||||
const v = Math.abs(val); // helps to handle negatives the same way
|
||||
const DECIMAL_THRESHOLD = 0.001;
|
||||
let format = ".2~s"; // 21K tilde means that it won't be 2.0K but just 2K
|
||||
if (v > 0 && v < DECIMAL_THRESHOLD) {
|
||||
format = ".0e"; // 1E-3 for values below DECIMAL_THRESHOLD
|
||||
}
|
||||
if (v >= DECIMAL_THRESHOLD && v < 1) {
|
||||
format = ".3~f"; // just plain 0.932
|
||||
}
|
||||
return d3Format(format)(val);
|
||||
};
|
||||
|
||||
export const AxisLeft: React.FC<AxisLeftI> = ({yScale, label}) => {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ref = useRef<SVGSVGElement | any>(null);
|
||||
useEffect(() => {
|
||||
yScale && d3Select(ref.current).call(axisLeft<number>(yScale).tickFormat(yFormatter));
|
||||
}, [yScale]);
|
||||
return (
|
||||
<>
|
||||
<g className="y axis" ref={ref} />
|
||||
{label && (
|
||||
<text style={{fontSize: "0.6rem"}} transform="translate(0,-2)">
|
||||
{label}
|
||||
</text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
import React from "react";
|
||||
import {Box, makeStyles, Typography} from "@material-ui/core";
|
||||
|
||||
export interface ChartTooltipData {
|
||||
value: number;
|
||||
metrics: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
data: ChartTooltipData;
|
||||
time?: Date;
|
||||
}
|
||||
|
||||
const useStyle = makeStyles(() => ({
|
||||
wrapper: {
|
||||
maxWidth: "40vw"
|
||||
}
|
||||
}));
|
||||
|
||||
export const ChartTooltip: React.FC<ChartTooltipProps> = ({data, time}) => {
|
||||
const classes = useStyle();
|
||||
|
||||
return (
|
||||
<Box px={1} className={classes.wrapper}>
|
||||
<Box fontStyle="italic" mb={.5}>
|
||||
<Typography variant="subtitle1">{`${time?.toLocaleDateString()} ${time?.toLocaleTimeString()}`}</Typography>
|
||||
</Box>
|
||||
<Box mb={.5} my={1}>
|
||||
<Typography variant="subtitle2">{`Value: ${new Intl.NumberFormat(undefined, {
|
||||
maximumFractionDigits: 10
|
||||
}).format(data.value)}`}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2">
|
||||
{data.metrics.map(({key, value}) =>
|
||||
<Box component="span" mb={.25} key={key} display="flex" flexDirection="row" alignItems="center">
|
||||
<span>{key}: </span>
|
||||
<span style={{fontWeight: "bold"}}>{value}</span>
|
||||
</Box>)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -1,124 +0,0 @@
|
|||
/* eslint max-lines: ["error", {"max": 200}] */ // Complex D3 logic here - file can be larger
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {bisector, brushX, pointer as d3Pointer, ScaleLinear, ScaleTime, select as d3Select} from "d3";
|
||||
|
||||
interface LineI {
|
||||
yScale: ScaleLinear<number, number>;
|
||||
xScale: ScaleTime<number, number>;
|
||||
datesInChart: Date[];
|
||||
setSelection: (from: Date, to: Date) => void;
|
||||
onInteraction: (index: number | undefined, y: number | undefined) => void; // key is index. undefined means no interaction
|
||||
}
|
||||
|
||||
export const InteractionArea: React.FC<LineI> = ({yScale, xScale, datesInChart, onInteraction, setSelection}) => {
|
||||
const refBrush = useRef<SVGGElement>(null);
|
||||
|
||||
const [currentActivePoint, setCurrentActivePoint] = useState<number>();
|
||||
const [currentY, setCurrentY] = useState<number>();
|
||||
const [isBrushed, setIsBrushed] = useState(false);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-function-return-type
|
||||
function brushEnded(this: any, event: any) {
|
||||
const selection = event.selection;
|
||||
if (selection) {
|
||||
if (!event.sourceEvent) return; // see comment in brushstarted
|
||||
setIsBrushed(true);
|
||||
const [from, to]: [Date, Date] = selection.map((s: number) => xScale.invert(s));
|
||||
setSelection(from, to);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
d3Select(refBrush.current).call(brush.move as any, null); // clean brush
|
||||
} else {
|
||||
// end event with empty selection means that we're cancelling brush
|
||||
setIsBrushed(false);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const brushStarted = (event: any): void => {
|
||||
// first of all: event is a d3 global value that stores current event (sort of).
|
||||
// This is weird but this is how d3 works with events.
|
||||
//This check is important:
|
||||
// Inside brushended - we have .call(brush.move, ...) in order to snap selected range to dates
|
||||
// that internally calls brushstarted again. But in this case sourceEvent is null, since the call
|
||||
// is programmatic. If we do not need to adjust selected are - no need to have this check (probably)
|
||||
if (event.sourceEvent) {
|
||||
setCurrentActivePoint(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const brush = useMemo(
|
||||
() =>
|
||||
brushX()
|
||||
.extent([
|
||||
[0, 0],
|
||||
[xScale.range()[1], yScale.range()[0]]
|
||||
])
|
||||
.on("end", brushEnded)
|
||||
.on("start", brushStarted),
|
||||
[brushEnded, xScale, yScale]
|
||||
);
|
||||
|
||||
// Needed to clean brush if we need to keep it
|
||||
|
||||
// const resetBrushHandler = useCallback(
|
||||
// (e) => {
|
||||
// const el = e.target as HTMLElement;
|
||||
// if (
|
||||
// el &&
|
||||
// el.tagName !== "rect" &&
|
||||
// e.target.classList.length &&
|
||||
// !e.target.classList.contains("selection")
|
||||
// ) {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// d3Select(refBrush.current).call(brush.move as any, null);
|
||||
// }
|
||||
// },
|
||||
// [brush.move]
|
||||
// );
|
||||
|
||||
// useEffect(() => {
|
||||
// window.addEventListener("click", resetBrushHandler);
|
||||
// return () => {
|
||||
// window.removeEventListener("click", resetBrushHandler);
|
||||
// };
|
||||
// }, [resetBrushHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
const bisect = bisector((d: Date) => d).center;
|
||||
const defineActivePoint = (mx: number): void => {
|
||||
const date = xScale.invert(mx); // date is a Date object
|
||||
const index = bisect(datesInChart, date, 1);
|
||||
setCurrentActivePoint(index);
|
||||
};
|
||||
|
||||
d3Select(refBrush.current)
|
||||
.on("touchmove mousemove", (event) => {
|
||||
const coords: [number, number] = d3Pointer(event);
|
||||
if (!isBrushed) {
|
||||
defineActivePoint(coords[0]);
|
||||
setCurrentY(coords[1]);
|
||||
}
|
||||
})
|
||||
.on("mouseout", () => {
|
||||
if (!isBrushed) {
|
||||
setCurrentActivePoint(undefined);
|
||||
}
|
||||
});
|
||||
}, [xScale, datesInChart, isBrushed]);
|
||||
|
||||
useEffect(() => {
|
||||
onInteraction(currentActivePoint, currentY);
|
||||
}, [currentActivePoint, currentY, onInteraction]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
brush && xScale && d3Select(refBrush.current).call(brush);
|
||||
}, [xScale, brush]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<g ref={refBrush} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
interface LineI {
|
||||
height: number;
|
||||
x: number | undefined;
|
||||
}
|
||||
|
||||
export const InteractionLine: React.FC<LineI> = ({height, x}) => {
|
||||
return <>{x && <line x1={x} y1="0" x2={x} y2={height} stroke="black" strokeDasharray="4" />}</>;
|
||||
};
|
|
@ -1,196 +1,141 @@
|
|||
/* eslint max-lines: ["error", {"max": 300}] */
|
||||
import React, {useCallback, useMemo, useRef, useState} from "react";
|
||||
import {line as d3Line, max as d3Max, min as d3Min, scaleLinear, ScaleOrdinal, scaleTime} from "d3";
|
||||
import "./line-chart.css";
|
||||
import Measure from "react-measure";
|
||||
import {AxisBottom} from "./AxisBottom";
|
||||
import {AxisLeft} from "./AxisLeft";
|
||||
import {DataSeries, DataValue, TimeParams} from "../../types";
|
||||
import {InteractionLine} from "./InteractionLine";
|
||||
import {InteractionArea} from "./InteractionArea";
|
||||
import {Box, Popover} from "@material-ui/core";
|
||||
import {ChartTooltip, ChartTooltipData} from "./ChartTooltip";
|
||||
import {useAppDispatch} from "../../state/common/StateContext";
|
||||
import {dateFromSeconds} from "../../utils/time";
|
||||
import {MetricCategory} from "../../hooks/useSortedCategories";
|
||||
import React, {FC, useEffect, useRef, useState} from "react";
|
||||
import {Line} from "react-chartjs-2";
|
||||
import {Chart, ChartData, ChartOptions, ScatterDataPoint} from "chart.js";
|
||||
import {getNameForMetric} from "../../utils/metric";
|
||||
import "chartjs-adapter-date-fns";
|
||||
import debounce from "lodash.debounce";
|
||||
import {useAppDispatch, useAppState} from "../../state/common/StateContext";
|
||||
import {dateFromSeconds, getTimeperiodForDuration} from "../../utils/time";
|
||||
import {GraphViewProps} from "../Home/Views/GraphView";
|
||||
import {limitsDurations} from "../../utils/time";
|
||||
|
||||
interface LineChartProps {
|
||||
series: DataSeries[];
|
||||
timePresets: TimeParams;
|
||||
height: number;
|
||||
color: ScaleOrdinal<string, string>; // maps name to color hex code
|
||||
categories: MetricCategory[];
|
||||
}
|
||||
|
||||
interface TooltipState {
|
||||
xCoord: number;
|
||||
date: Date;
|
||||
index: number;
|
||||
leftPart: boolean;
|
||||
activeSeries: number;
|
||||
}
|
||||
|
||||
const TOOLTIP_MARGIN = 20;
|
||||
|
||||
export const LineChart: React.FC<LineChartProps> = ({series, timePresets, height, color, categories}) => {
|
||||
const [screenWidth, setScreenWidth] = useState<number>(window.innerWidth);
|
||||
const LineChart: FC<GraphViewProps> = ({data = []}) => {
|
||||
|
||||
const {time: {duration, period}} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
const [series, setSeries] = useState<ChartData<"line", (ScatterDataPoint)[]>>();
|
||||
const refLine = useRef<Chart>(null);
|
||||
|
||||
const margin = {top: 10, right: 20, bottom: 40, left: 50};
|
||||
const svgWidth = useMemo(() => screenWidth - margin.left - margin.right, [screenWidth, margin.left, margin.right]);
|
||||
const svgHeight = useMemo(() => height - margin.top - margin.bottom, [margin.top, margin.bottom]);
|
||||
const xScale = useMemo(() => scaleTime().domain([timePresets.start,timePresets.end].map(dateFromSeconds)).range([0, svgWidth]), [
|
||||
svgWidth,
|
||||
timePresets
|
||||
]);
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const [tooltipState, setTooltipState] = useState<TooltipState>();
|
||||
|
||||
const yAxisLabel = ""; // TODO: label
|
||||
|
||||
const yScale = useMemo(
|
||||
() => {
|
||||
const seriesValues = series.reduce((acc: DataValue[], next: DataSeries) => [...acc, ...next.values], []).map(_ => _.value);
|
||||
const max = d3Max(seriesValues) ?? 1; // || 1 will cause one additional tick if max is 0
|
||||
const min = d3Min(seriesValues) || 0;
|
||||
return scaleLinear()
|
||||
.domain([min > 0 ? 0 : min, max < 0 ? 0 : max]) // input
|
||||
.range([svgHeight, 0])
|
||||
.nice();
|
||||
},
|
||||
[series, svgHeight]
|
||||
);
|
||||
|
||||
const line = useMemo(
|
||||
() =>
|
||||
d3Line<DataValue>()
|
||||
.x((d) => xScale(dateFromSeconds(d.key)))
|
||||
.y((d) => yScale(d.value || 0)),
|
||||
[xScale, yScale]
|
||||
);
|
||||
const getDataLine = (series: DataSeries) => line(series.values);
|
||||
|
||||
const handleChartInteraction = useCallback(
|
||||
async (key: number | undefined, y: number | undefined) => {
|
||||
if (typeof key === "number") {
|
||||
if (y && series && series[0]) {
|
||||
|
||||
// define closest series in chart
|
||||
const hoveringOverValue = yScale.invert(y);
|
||||
const closestPoint = series.map(s => s.values[key]?.value).reduce((acc, nextValue, index) => {
|
||||
const delta = Math.abs(hoveringOverValue - nextValue);
|
||||
if (delta < acc.delta) {
|
||||
acc = {delta, index};
|
||||
}
|
||||
return acc;
|
||||
}, {delta: Infinity, index: 0});
|
||||
|
||||
const date = dateFromSeconds(series[0].values[key].key);
|
||||
// popover orientation should be defined based on the scale domain middle, not data, since
|
||||
// data may not be present for the whole range
|
||||
const leftPart = date.valueOf() < (xScale.domain()[1].valueOf() + xScale.domain()[0].valueOf()) / 2;
|
||||
setTooltipState({
|
||||
date,
|
||||
xCoord: xScale(date),
|
||||
index: key,
|
||||
activeSeries: closestPoint.index,
|
||||
leftPart
|
||||
});
|
||||
setShowTooltip(true);
|
||||
}
|
||||
} else {
|
||||
setShowTooltip(false);
|
||||
setTooltipState(undefined);
|
||||
}
|
||||
},
|
||||
[xScale, yScale, series]
|
||||
);
|
||||
|
||||
const tooltipData: ChartTooltipData | undefined = useMemo(() => {
|
||||
if (tooltipState?.activeSeries) {
|
||||
return {
|
||||
value: series[tooltipState.activeSeries].values[tooltipState.index].value,
|
||||
metrics: categories.map(c => ({ key: c.key, value: series[tooltipState.activeSeries].metric[c.key]}))
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
const getColorByName = (str: string): string => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
}, [tooltipState, series]);
|
||||
|
||||
const tooltipAnchor = useRef<SVGGElement>(null);
|
||||
|
||||
const seriesDates = useMemo(() => {
|
||||
if (series && series[0]) {
|
||||
return series[0].values.map(v => v.key).map(dateFromSeconds);
|
||||
} else {
|
||||
return [];
|
||||
let colour = "#";
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 0xFF;
|
||||
colour += ("00" + value.toString(16)).substr(-2);
|
||||
}
|
||||
}, [series]);
|
||||
|
||||
const setSelection = (from: Date, to: Date) => {
|
||||
dispatch({type: "SET_PERIOD", payload: {from, to}});
|
||||
return colour;
|
||||
};
|
||||
|
||||
return (
|
||||
<Measure bounds onResize={({bounds}) => bounds && setScreenWidth(bounds?.width)}>
|
||||
{({measureRef}) => (
|
||||
<div ref={measureRef} style={{width: "100%"}}>
|
||||
{tooltipAnchor && tooltipData && (
|
||||
<Popover
|
||||
disableScrollLock={true}
|
||||
style={{pointerEvents: "none"}} // IMPORTANT in order to allow interactions through popover's backdrop
|
||||
id="chart-tooltip-popover"
|
||||
open={showTooltip}
|
||||
anchorEl={tooltipAnchor.current}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: tooltipState?.leftPart ? TOOLTIP_MARGIN : -TOOLTIP_MARGIN
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: tooltipState?.leftPart ? "left" : "right"
|
||||
}}
|
||||
disableRestoreFocus>
|
||||
<Box m={1}>
|
||||
<ChartTooltip data={tooltipData} time={tooltipState?.date}/>
|
||||
</Box>
|
||||
</Popover>
|
||||
)}
|
||||
<svg width="100%" height={height}>
|
||||
<g transform={`translate(${margin.left}, ${margin.top})`}>
|
||||
<defs>
|
||||
{/*Clip path helps to clip the line*/}
|
||||
<clipPath id="clip-line">
|
||||
{/*Transforming and adding size to clip-path in order to avoid clipping of chart elements*/}
|
||||
<rect transform={"translate(0, -2)"} width={xScale.range()[1] + 4} height={yScale.range()[0] + 2} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<AxisBottom xScale={xScale} height={svgHeight} />
|
||||
<AxisLeft yScale={yScale} label={yAxisLabel} />
|
||||
{series.map((s, i) =>
|
||||
<path stroke={color(s.metadata.name)}
|
||||
key={i} className="line"
|
||||
style={{opacity: tooltipState?.activeSeries !== undefined ? (i === tooltipState?.activeSeries ? 1 : .2) : 1 }}
|
||||
d={getDataLine(s) as string}
|
||||
clipPath="url(#clip-line)"/>)}
|
||||
<g ref={tooltipAnchor}>
|
||||
<InteractionLine height={svgHeight} x={tooltipState?.xCoord} />
|
||||
</g>
|
||||
{/*NOTE: in SVG last element wins - so since we want mouseover to work in all area this should be last*/}
|
||||
<InteractionArea
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
datesInChart={seriesDates}
|
||||
onInteraction={handleChartInteraction}
|
||||
setSelection={setSelection}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
useEffect(() => {
|
||||
setSeries({
|
||||
datasets: data?.map(d => {
|
||||
const label = getNameForMetric(d);
|
||||
const color = getColorByName(label);
|
||||
return {
|
||||
label,
|
||||
data: d.values.map(v => ({y: +v[1], x: v[0] * 1000})),
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
};
|
||||
})
|
||||
});
|
||||
if (refLine.current) {
|
||||
refLine.current.stop(); // make sure animations are not running
|
||||
refLine.current.update("none");
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onZoomComplete = ({chart}: {chart: Chart}) => {
|
||||
let {min, max} = chart.scales.x;
|
||||
if (!min || !max) return;
|
||||
const duration = max - min;
|
||||
if (duration < limitsDurations.min) max = min + limitsDurations.min;
|
||||
if (duration > limitsDurations.max) min = max - limitsDurations.max;
|
||||
dispatch({type: "SET_PERIOD", payload: {from: new Date(min), to: new Date(max)}});
|
||||
};
|
||||
|
||||
const onPanComplete = ({chart}: {chart: Chart}) => {
|
||||
const {min, max} = chart.scales.x;
|
||||
if (!min || !max) return;
|
||||
const {start, end} = getTimeperiodForDuration(duration, new Date(max));
|
||||
dispatch({type: "SET_PERIOD", payload: {from: dateFromSeconds(start), to: dateFromSeconds(end)}});
|
||||
};
|
||||
|
||||
const options: ChartOptions = {
|
||||
animation: {duration: 0},
|
||||
parsing: false,
|
||||
normalized: true,
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
position: "bottom",
|
||||
min: (period.start * 1000),
|
||||
max: (period.end * 1000),
|
||||
time: {
|
||||
tooltipFormat: "yyyy-MM-dd HH:mm:ss.SSS",
|
||||
displayFormats: {millisecond: ":ss.SSS", second: "HH:mm:ss", minute: "HH:mm", hour: "HH:mm"}
|
||||
},
|
||||
ticks: {
|
||||
source: "auto",
|
||||
autoSkip: true,
|
||||
autoSkipPadding: 105,
|
||||
crossAlign: "center",
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
sampleSize: 1,
|
||||
color: "#000",
|
||||
font: {size: 10}
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
position: "left",
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
color: "#000",
|
||||
font: {size: 10}
|
||||
}
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0,
|
||||
stepped: false,
|
||||
borderDash: [],
|
||||
borderWidth: 1,
|
||||
capBezierPoints: false
|
||||
},
|
||||
point: {radius: 0, hitRadius: 10}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
align: "start",
|
||||
labels: {padding: 20, color: "#000"}
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: "x",
|
||||
onPan: debounce(onPanComplete, 750)
|
||||
},
|
||||
zoom: {
|
||||
pinch: {enabled: true},
|
||||
wheel: {enabled: true, speed: 0.05},
|
||||
mode: "x",
|
||||
onZoom: debounce(onZoomComplete, 250)
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
{series && <Line data={series} options={options} ref={refLine}/>}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default LineChart;
|
|
@ -1,15 +0,0 @@
|
|||
.line {
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
fill: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* Style the dots by assigning a fill and stroke */
|
||||
.dot {
|
||||
fill: #621773;
|
||||
stroke: #fff;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export type AggregatedDataSet = {
|
||||
key: number;
|
||||
value: aggregatedDataValue;
|
||||
};
|
||||
export type aggregatedDataValue = {[key: string]: number};
|
|
@ -33,13 +33,13 @@ export type Action =
|
|||
| { type: "TOGGLE_AUTOREFRESH"}
|
||||
| { type: "TOGGLE_AUTOCOMPLETE"}
|
||||
|
||||
const duration = getQueryStringValue("g0.range_input", "1h");
|
||||
const endInput = getQueryStringValue("g0.end_input", undefined);
|
||||
const duration = getQueryStringValue("g0.range_input", "1h") as string;
|
||||
const endInput = getQueryStringValue("g0.end_input", undefined) as Date | undefined;
|
||||
|
||||
export const initialState: AppState = {
|
||||
serverUrl: getDefaultServer(),
|
||||
displayType: "chart",
|
||||
query: getQueryStringValue("g0.expr", getFromStorage("LAST_QUERY") as string || "\n"), // demo_memory_usage_bytes
|
||||
query: getQueryStringValue("g0.expr", getFromStorage("LAST_QUERY") as string || "\n") as string, // demo_memory_usage_bytes
|
||||
time: {
|
||||
duration,
|
||||
period: getTimeperiodForDuration(duration, endInput && new Date(endInput))
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import {Chart} from "chart.js";
|
||||
import zoomPlugin from "chartjs-plugin-zoom";
|
||||
|
||||
Chart.register(zoomPlugin);
|
|
@ -53,9 +53,9 @@ export const setQueryStringValue = (newValue: Record<string, unknown>): void =>
|
|||
|
||||
export const getQueryStringValue = (
|
||||
key: string,
|
||||
defaultValue?: any,
|
||||
defaultValue?: unknown,
|
||||
queryString = window.location.search
|
||||
) => {
|
||||
): unknown => {
|
||||
const values = qs.parse(queryString, { ignoreQueryPrefix: true });
|
||||
return get(values, key, defaultValue || "");
|
||||
};
|
||||
|
|
|
@ -5,7 +5,10 @@ import duration from "dayjs/plugin/duration";
|
|||
|
||||
dayjs.extend(duration);
|
||||
|
||||
const MAX_ITEMS_PER_CHART = window.screen.availWidth / 2;
|
||||
const MAX_ITEMS_PER_CHART = window.screen.availWidth / 7.68;
|
||||
|
||||
export const limitsDurations = {min: 1000, max: 1.578e+11}; // min: 1 seconds, max: 5 years
|
||||
|
||||
|
||||
export const supportedDurations = [
|
||||
{long: "days", short: "d", possible: "day"},
|
||||
|
@ -63,14 +66,34 @@ export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams =
|
|||
|
||||
export const formatDateForNativeInput = (date: Date): string => dayjs(date).format("YYYY-MM-DD[T]HH:mm:ss");
|
||||
|
||||
export const getDurationFromPeriod = (p: TimePeriod): string => {
|
||||
const dur = dayjs.duration(p.to.valueOf() - p.from.valueOf());
|
||||
const getDurationFromMilliseconds = (ms: number): string => {
|
||||
const seconds = Math.floor((ms / 1000) % 60);
|
||||
const minutes = Math.floor((ms / 1000 / 60) % 60);
|
||||
const hours = Math.floor((ms / 1000 / 3600 ) % 24);
|
||||
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
|
||||
const durs: UnitTypeShort[] = ["d", "h", "m", "s"];
|
||||
return durs
|
||||
.map(d => ({val: dur.get(d), str: d}))
|
||||
.filter(obj => obj.val !== 0)
|
||||
.map(obj => `${obj.val}${obj.str}`)
|
||||
.join(" ");
|
||||
const values = [days, hours, minutes, seconds].map((t, i) => t ? `${t}${durs[i]}` : "");
|
||||
return values.filter(t => t).join(" ");
|
||||
};
|
||||
|
||||
export const getDurationFromPeriod = (p: TimePeriod): string => {
|
||||
const ms = p.to.valueOf() - p.from.valueOf();
|
||||
return getDurationFromMilliseconds(ms);
|
||||
};
|
||||
|
||||
export const checkDurationLimit = (dur: string): string => {
|
||||
const durItems = dur.trim().split(" ");
|
||||
|
||||
const durObject = durItems.reduce((prev, curr) => {
|
||||
const dur = isSupportedDuration(curr);
|
||||
return dur ? {...prev, ...dur} : {...prev};
|
||||
}, {});
|
||||
|
||||
const delta = dayjs.duration(durObject).asMilliseconds();
|
||||
|
||||
if (delta < limitsDurations.min) return getDurationFromMilliseconds(limitsDurations.min);
|
||||
if (delta > limitsDurations.max) return getDurationFromMilliseconds(limitsDurations.max);
|
||||
return dur;
|
||||
};
|
||||
|
||||
export const dateFromSeconds = (epochTimeInSeconds: number): Date =>
|
||||
|
|
|
@ -9,6 +9,7 @@ sort: 15
|
|||
* FEATURE: vmagent [enterprise](https://victoriametrics.com/enterprise.html): add support for data reading from [Apache Kafka](https://kafka.apache.org/).
|
||||
* FEATURE: calculate quantiles in the same way as Prometheus does in such functions as [quantile_over_time](https://docs.victoriametrics.com/MetricsQL.html#quantile_over_time) and [quantile](https://docs.victoriametrics.com/MetricsQL.html#quantile). Previously results from VictoriaMetrics could be slightly different than results from Prometheus. See [this](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1625) and [this](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1612) issues.
|
||||
* FEATURE: add `rollup_scrape_interval(m[d])` function to [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html), which returns `min`, `max` and `avg` values for the interval between samples for `m` on the given lookbehind window `d`.
|
||||
* FEATURE: vmui: add ability to naturally scroll and zoom graphs. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1634).
|
||||
|
||||
* BUGFIX: align behavior of the queries `a or on (labels) b`, `a and on (labels) b` and `a unless on (labels) b` where `b` has multiple time series with the given `labels` to Prometheus behavior. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1643).
|
||||
|
||||
|
|
Loading…
Reference in a new issue