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": {
|
"files": {
|
||||||
"main.css": "./static/css/main.6452b577.chunk.css",
|
"main.css": "./static/css/main.0d21f12a.chunk.css",
|
||||||
"main.js": "./static/js/main.801aa0ec.chunk.js",
|
"main.js": "./static/js/main.a0567498.chunk.js",
|
||||||
"runtime-main.js": "./static/js/runtime-main.55798746.js",
|
"runtime-main.js": "./static/js/runtime-main.4736dc3a.js",
|
||||||
"static/js/2.fd2c2c30.chunk.js": "./static/js/2.fd2c2c30.chunk.js",
|
"static/js/2.c663f8e1.chunk.js": "./static/js/2.c663f8e1.chunk.js",
|
||||||
"static/js/3.c36fc28c.chunk.js": "./static/js/3.c36fc28c.chunk.js",
|
"static/js/3.9f518d6d.chunk.js": "./static/js/3.9f518d6d.chunk.js",
|
||||||
"index.html": "./index.html",
|
"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": [
|
"entrypoints": [
|
||||||
"static/js/runtime-main.55798746.js",
|
"static/js/runtime-main.4736dc3a.js",
|
||||||
"static/js/2.fd2c2c30.chunk.js",
|
"static/js/2.c663f8e1.chunk.js",
|
||||||
"static/css/main.6452b577.chunk.css",
|
"static/css/main.0d21f12a.chunk.css",
|
||||||
"static/js/main.801aa0ec.chunk.js"
|
"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
|
@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.
|
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.
|
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.
|
* 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/jest-dom": "^5.11.6",
|
||||||
"@testing-library/react": "^11.1.2",
|
"@testing-library/react": "^11.1.2",
|
||||||
"@testing-library/user-event": "^12.2.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/jest": "^26.0.15",
|
||||||
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/lodash.get": "^4.4.6",
|
"@types/lodash.get": "^4.4.6",
|
||||||
"@types/node": "^12.19.4",
|
"@types/node": "^12.19.4",
|
||||||
"@types/qs": "^6.9.6",
|
"@types/qs": "^6.9.6",
|
||||||
"@types/react": "^16.9.56",
|
"@types/react": "^16.9.56",
|
||||||
"@types/react-dom": "^16.9.9",
|
"@types/react-dom": "^16.9.9",
|
||||||
"@types/react-measure": "^2.0.6",
|
"@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",
|
"codemirror-promql": "^0.10.2",
|
||||||
"d3": "^6.2.0",
|
"date-fns": "^2.23.0",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"qs": "^6.5.2",
|
"qs": "^6.5.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
|
"react-chartjs-2": "^3.0.5",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-measure": "^2.5.2",
|
"react-measure": "^2.5.2",
|
||||||
"react-scripts": "4.0.0",
|
"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 { KeyboardDateTimePicker } from "@material-ui/pickers";
|
||||||
import {TimeDurationPopover} from "./TimeDurationPopover";
|
import {TimeDurationPopover} from "./TimeDurationPopover";
|
||||||
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
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";
|
import {InlineBtn} from "../../common/InlineBtn";
|
||||||
|
|
||||||
interface TimeSelectorProps {
|
interface TimeSelectorProps {
|
||||||
|
@ -33,7 +33,9 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!durationStringFocused) {
|
if (!durationStringFocused) {
|
||||||
setDuration(durationString);
|
const value = checkDurationLimit(durationString);
|
||||||
|
setDurationString(value);
|
||||||
|
setDuration(value);
|
||||||
}
|
}
|
||||||
}, [durationString, durationStringFocused]);
|
}, [durationString, durationStringFocused]);
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const useFetchQuery = (): {
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
graphData?: MetricResult[],
|
graphData?: MetricResult[],
|
||||||
liveData?: InstantMetricResult[],
|
liveData?: InstantMetricResult[],
|
||||||
error?: string
|
error?: string,
|
||||||
} => {
|
} => {
|
||||||
const {query, displayType, serverUrl, time: {period}} = useAppState();
|
const {query, displayType, serverUrl, time: {period}} = useAppState();
|
||||||
|
|
||||||
|
@ -40,8 +40,10 @@ export const useFetchQuery = (): {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isValidHttpUrl(serverUrl)) {
|
if (isValidHttpUrl(serverUrl)) {
|
||||||
|
const duration = (period.end - period.start)/2;
|
||||||
|
const doublePeriod = {...period, start: period.start - duration, end: period.end + duration};
|
||||||
return displayType === "chart"
|
return displayType === "chart"
|
||||||
? getQueryRangeUrl(serverUrl, query, period)
|
? getQueryRangeUrl(serverUrl, query, doublePeriod)
|
||||||
: getQueryUrl(serverUrl, query, period);
|
: getQueryUrl(serverUrl, query, period);
|
||||||
} else {
|
} else {
|
||||||
setError("Please provide a valid URL");
|
setError("Please provide a valid URL");
|
||||||
|
@ -63,6 +65,7 @@ export const useFetchQuery = (): {
|
||||||
headers.set("Authorization", bearerData?.token || "");
|
headers.set("Authorization", bearerData?.token || "");
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
const response = await fetch(fetchUrl, {
|
const response = await fetch(fetchUrl, {
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
@ -74,6 +77,10 @@ export const useFetchQuery = (): {
|
||||||
} else {
|
} else {
|
||||||
setError((await response.json())?.error);
|
setError((await response.json())?.error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
setError(e.message);
|
||||||
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -50,32 +50,32 @@ const HomeLayout: FC = () => {
|
||||||
<UrlCopy url={fetchUrl}/>
|
<UrlCopy url={fetchUrl}/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Box display="flex" flexDirection="column" style={{minHeight: "calc(100vh - 64px)"}}>
|
<Box p={2} display="grid" gridTemplateRows="auto 1fr" gridGap={"20px"} style={{minHeight: "calc(100vh - 64px)"}}>
|
||||||
<Box m={2}>
|
<Box>
|
||||||
<QueryConfigurator/>
|
<QueryConfigurator/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexShrink={1}>
|
<Box height={"100%"}>
|
||||||
{isLoading && <Fade in={isLoading} style={{
|
{isLoading && <Fade in={isLoading} style={{
|
||||||
transitionDelay: isLoading ? "300ms" : "0ms",
|
transitionDelay: isLoading ? "300ms" : "0ms",
|
||||||
}}>
|
}}>
|
||||||
<Box alignItems="center" flexDirection="column" display="flex"
|
<Box alignItems="center" justifyContent="center" flexDirection="column" display="flex"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "calc(100vh - 32px)",
|
maxWidth: "calc(100vw - 32px)",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
height: "150px",
|
height: "50%",
|
||||||
background: "linear-gradient(rgba(255,255,255,.7), rgba(255,255,255,.7), rgba(255,255,255,0))"
|
background: "linear-gradient(rgba(255,255,255,.7), rgba(255,255,255,.7), rgba(255,255,255,0))"
|
||||||
}} m={2}>
|
}}>
|
||||||
<CircularProgress/>
|
<CircularProgress/>
|
||||||
</Box>
|
</Box>
|
||||||
</Fade>}
|
</Fade>}
|
||||||
{<Box p={2}>
|
{<Box height={"100%"} py={3} px={6} bgcolor={"#fff"}>
|
||||||
{error &&
|
{error &&
|
||||||
<Alert color="error" style={{fontSize: "14px"}}>
|
<Alert color="error" severity="error" style={{fontSize: "14px"}}>
|
||||||
{error}
|
{error}
|
||||||
</Alert>}
|
</Alert>}
|
||||||
{graphData && period && (displayType === "chart") &&
|
{graphData && period && (displayType === "chart") &&
|
||||||
<GraphView data={graphData} timePresets={period}></GraphView>}
|
<GraphView data={graphData}/>}
|
||||||
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
|
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
|
||||||
{liveData && (displayType === "table") && <TableView data={liveData}/>}
|
{liveData && (displayType === "table") && <TableView data={liveData}/>}
|
||||||
</Box>}
|
</Box>}
|
||||||
|
|
|
@ -1,126 +1,16 @@
|
||||||
import React, {FC, useEffect, useMemo, useState} from "react";
|
import React, {FC} from "react";
|
||||||
import {MetricResult} from "../../../api/types";
|
import {MetricResult} from "../../../api/types";
|
||||||
|
import LineChart from "../../LineChart/LineChart";
|
||||||
import {schemeCategory10, scaleOrdinal, interpolateRainbow, range as d3Range} from "d3";
|
import "../../../utils/chartjs-register-plugins";
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
export interface GraphViewProps {
|
export interface GraphViewProps {
|
||||||
data: MetricResult[];
|
data?: MetricResult[];
|
||||||
timePresets: TimeParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const preDefinedScale = schemeCategory10;
|
const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{(amountOfSeries > 0)
|
{(data.length > 0)
|
||||||
? <>
|
? <LineChart data={data} />
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
: <div style={{textAlign: "center"}}>No data to show</div>}
|
: <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, {FC, useEffect, useRef, useState} from "react";
|
||||||
import React, {useCallback, useMemo, useRef, useState} from "react";
|
import {Line} from "react-chartjs-2";
|
||||||
import {line as d3Line, max as d3Max, min as d3Min, scaleLinear, ScaleOrdinal, scaleTime} from "d3";
|
import {Chart, ChartData, ChartOptions, ScatterDataPoint} from "chart.js";
|
||||||
import "./line-chart.css";
|
import {getNameForMetric} from "../../utils/metric";
|
||||||
import Measure from "react-measure";
|
import "chartjs-adapter-date-fns";
|
||||||
import {AxisBottom} from "./AxisBottom";
|
import debounce from "lodash.debounce";
|
||||||
import {AxisLeft} from "./AxisLeft";
|
import {useAppDispatch, useAppState} from "../../state/common/StateContext";
|
||||||
import {DataSeries, DataValue, TimeParams} from "../../types";
|
import {dateFromSeconds, getTimeperiodForDuration} from "../../utils/time";
|
||||||
import {InteractionLine} from "./InteractionLine";
|
import {GraphViewProps} from "../Home/Views/GraphView";
|
||||||
import {InteractionArea} from "./InteractionArea";
|
import {limitsDurations} from "../../utils/time";
|
||||||
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";
|
|
||||||
|
|
||||||
interface LineChartProps {
|
const LineChart: FC<GraphViewProps> = ({data = []}) => {
|
||||||
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 {time: {duration, period}} = useAppState();
|
||||||
const dispatch = useAppDispatch();
|
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 getColorByName = (str: string): string => {
|
||||||
const svgWidth = useMemo(() => screenWidth - margin.left - margin.right, [screenWidth, margin.left, margin.right]);
|
let hash = 0;
|
||||||
const svgHeight = useMemo(() => height - margin.top - margin.bottom, [margin.top, margin.bottom]);
|
for (let i = 0; i < str.length; i++) {
|
||||||
const xScale = useMemo(() => scaleTime().domain([timePresets.start,timePresets.end].map(dateFromSeconds)).range([0, svgWidth]), [
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
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;
|
let colour = "#";
|
||||||
}, {delta: Infinity, index: 0});
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xFF;
|
||||||
const date = dateFromSeconds(series[0].values[key].key);
|
colour += ("00" + value.toString(16)).substr(-2);
|
||||||
// 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 {
|
return colour;
|
||||||
setShowTooltip(false);
|
};
|
||||||
setTooltipState(undefined);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[xScale, yScale, series]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tooltipData: ChartTooltipData | undefined = useMemo(() => {
|
useEffect(() => {
|
||||||
if (tooltipState?.activeSeries) {
|
setSeries({
|
||||||
|
datasets: data?.map(d => {
|
||||||
|
const label = getNameForMetric(d);
|
||||||
|
const color = getColorByName(label);
|
||||||
return {
|
return {
|
||||||
value: series[tooltipState.activeSeries].values[tooltipState.index].value,
|
label,
|
||||||
metrics: categories.map(c => ({ key: c.key, value: series[tooltipState.activeSeries].metric[c.key]}))
|
data: d.values.map(v => ({y: +v[1], x: v[0] * 1000})),
|
||||||
|
borderColor: color,
|
||||||
|
backgroundColor: color,
|
||||||
};
|
};
|
||||||
} else {
|
})
|
||||||
return undefined;
|
});
|
||||||
|
if (refLine.current) {
|
||||||
|
refLine.current.stop(); // make sure animations are not running
|
||||||
|
refLine.current.update("none");
|
||||||
}
|
}
|
||||||
}, [tooltipState, series]);
|
}, [data]);
|
||||||
|
|
||||||
const tooltipAnchor = useRef<SVGGElement>(null);
|
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 seriesDates = useMemo(() => {
|
const onPanComplete = ({chart}: {chart: Chart}) => {
|
||||||
if (series && series[0]) {
|
const {min, max} = chart.scales.x;
|
||||||
return series[0].values.map(v => v.key).map(dateFromSeconds);
|
if (!min || !max) return;
|
||||||
} else {
|
const {start, end} = getTimeperiodForDuration(duration, new Date(max));
|
||||||
return [];
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}, [series]);
|
|
||||||
|
|
||||||
const setSelection = (from: Date, to: Date) => {
|
|
||||||
dispatch({type: "SET_PERIOD", payload: {from, to}});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<Measure bounds onResize={({bounds}) => bounds && setScreenWidth(bounds?.width)}>
|
{series && <Line data={series} options={options} ref={refLine}/>}
|
||||||
{({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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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_AUTOREFRESH"}
|
||||||
| { type: "TOGGLE_AUTOCOMPLETE"}
|
| { type: "TOGGLE_AUTOCOMPLETE"}
|
||||||
|
|
||||||
const duration = getQueryStringValue("g0.range_input", "1h");
|
const duration = getQueryStringValue("g0.range_input", "1h") as string;
|
||||||
const endInput = getQueryStringValue("g0.end_input", undefined);
|
const endInput = getQueryStringValue("g0.end_input", undefined) as Date | undefined;
|
||||||
|
|
||||||
export const initialState: AppState = {
|
export const initialState: AppState = {
|
||||||
serverUrl: getDefaultServer(),
|
serverUrl: getDefaultServer(),
|
||||||
displayType: "chart",
|
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: {
|
time: {
|
||||||
duration,
|
duration,
|
||||||
period: getTimeperiodForDuration(duration, endInput && new Date(endInput))
|
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 = (
|
export const getQueryStringValue = (
|
||||||
key: string,
|
key: string,
|
||||||
defaultValue?: any,
|
defaultValue?: unknown,
|
||||||
queryString = window.location.search
|
queryString = window.location.search
|
||||||
) => {
|
): unknown => {
|
||||||
const values = qs.parse(queryString, { ignoreQueryPrefix: true });
|
const values = qs.parse(queryString, { ignoreQueryPrefix: true });
|
||||||
return get(values, key, defaultValue || "");
|
return get(values, key, defaultValue || "");
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,10 @@ import duration from "dayjs/plugin/duration";
|
||||||
|
|
||||||
dayjs.extend(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 = [
|
export const supportedDurations = [
|
||||||
{long: "days", short: "d", possible: "day"},
|
{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 formatDateForNativeInput = (date: Date): string => dayjs(date).format("YYYY-MM-DD[T]HH:mm:ss");
|
||||||
|
|
||||||
export const getDurationFromPeriod = (p: TimePeriod): string => {
|
const getDurationFromMilliseconds = (ms: number): string => {
|
||||||
const dur = dayjs.duration(p.to.valueOf() - p.from.valueOf());
|
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"];
|
const durs: UnitTypeShort[] = ["d", "h", "m", "s"];
|
||||||
return durs
|
const values = [days, hours, minutes, seconds].map((t, i) => t ? `${t}${durs[i]}` : "");
|
||||||
.map(d => ({val: dur.get(d), str: d}))
|
return values.filter(t => t).join(" ");
|
||||||
.filter(obj => obj.val !== 0)
|
};
|
||||||
.map(obj => `${obj.val}${obj.str}`)
|
|
||||||
.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 =>
|
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: 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: 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: 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).
|
* 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