vmui: multiple queries (#1916)

* feat: change duration by "enter"

* fix: optimize data processing for chart

* feat: set minimum step to 1ms

* update dependencies

* feat: remove save the last query to local storage

* fix: handle an error in a table with subqueries

* feat: store display type in URL

* Revert "feat: store display type in URL"

This reverts commit ccc242c69a.

* feat: store display type in URL

* refactor: move the time setting to a folder

* refactor: move the query configurator to a folder

* refactor: move the auth settings to a folder

* feat: improve styles

* feat: add multi query

* update package-lock

* feat: add display multiple queries

* feat: add limits for multiple queries

* update dependencies

* feat: add history for multiple queries

* feat: add line type to legend

* feat: change style for switch

* feat: change the logic for axes limits for multiple queries

* update package-lock.json

* update dependencies

* feat: add the filter to legend

* wip

* lib/httpserver: add missing 127.0.0.1 hostname to the logged address for http and pprof server if the address starts with ':'

This allows copy-pasting the url to http server from logs.

* lib/httpserver: add missing 127.0.0.1 hostname to the logged address for http and pprof server if the address starts with ':'

This allows copy-pasting the url to http server from logs.

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2021-12-08 17:40:15 +03:00 committed by GitHub
parent 0288078cfb
commit c1fd93e8a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 940 additions and 854 deletions

View File

@ -1217,7 +1217,8 @@ Consider setting the following command-line flags:
* `-snapshotAuthKey` for protecting `/snapshot*` endpoints. See [how to work with snapshots](#how-to-work-with-snapshots). * `-snapshotAuthKey` for protecting `/snapshot*` endpoints. See [how to work with snapshots](#how-to-work-with-snapshots).
* `-forceMergeAuthKey` for protecting `/internal/force_merge` endpoint. See [force merge docs](#forced-merge). * `-forceMergeAuthKey` for protecting `/internal/force_merge` endpoint. See [force merge docs](#forced-merge).
* `-search.resetCacheAuthKey` for protecting `/internal/resetRollupResultCache` endpoint. See [backfilling](#backfilling) for more details. * `-search.resetCacheAuthKey` for protecting `/internal/resetRollupResultCache` endpoint. See [backfilling](#backfilling) for more details.
* `-configAuthKey` for pretecting `/config` endpoint, since it may contain sensitive information such as passwords. * `-configAuthKey` for protecting `/config` endpoint, since it may contain sensitive information such as passwords.
- `-pprofAuthKey` for protecting `/debug/pprof/*` endpoints, which can be used for [profiling](#profiling).
Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats. Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats.
For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`. For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`.

View File

@ -73,7 +73,6 @@ func main() {
}() }()
} }
logger.Infof("starting http server for exporting metrics at http://%q/metrics", *httpListenAddr)
go httpserver.Serve(*httpListenAddr, nil) go httpserver.Serve(*httpListenAddr, nil)
srcFS, err := newSrcFS() srcFS, err := newSrcFS()

View File

@ -36,7 +36,6 @@ func main() {
buildinfo.Init() buildinfo.Init()
logger.Init() logger.Init()
logger.Infof("starting http server for exporting metrics at http://%q/metrics", *httpListenAddr)
go httpserver.Serve(*httpListenAddr, nil) go httpserver.Serve(*httpListenAddr, nil)
srcFS, err := newSrcFS() srcFS, err := newSrcFS()

View File

@ -1,19 +1,19 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.674f8c98.chunk.css", "main.css": "./static/css/main.1b10b1ed.chunk.css",
"main.js": "./static/js/main.06277491.chunk.js", "main.js": "./static/js/main.9f03a409.chunk.js",
"runtime-main.js": "./static/js/runtime-main.f698388d.js", "runtime-main.js": "./static/js/runtime-main.66a19bd8.js",
"static/css/2.77671664.chunk.css": "./static/css/2.77671664.chunk.css", "static/css/2.77671664.chunk.css": "./static/css/2.77671664.chunk.css",
"static/js/2.e53c287a.chunk.js": "./static/js/2.e53c287a.chunk.js", "static/js/2.bc8706fc.chunk.js": "./static/js/2.bc8706fc.chunk.js",
"static/js/3.e51afffb.chunk.js": "./static/js/3.e51afffb.chunk.js", "static/js/3.7c3472dc.chunk.js": "./static/js/3.7c3472dc.chunk.js",
"index.html": "./index.html", "index.html": "./index.html",
"static/js/2.e53c287a.chunk.js.LICENSE.txt": "./static/js/2.e53c287a.chunk.js.LICENSE.txt" "static/js/2.bc8706fc.chunk.js.LICENSE.txt": "./static/js/2.bc8706fc.chunk.js.LICENSE.txt"
}, },
"entrypoints": [ "entrypoints": [
"static/js/runtime-main.f698388d.js", "static/js/runtime-main.66a19bd8.js",
"static/css/2.77671664.chunk.css", "static/css/2.77671664.chunk.css",
"static/js/2.e53c287a.chunk.js", "static/js/2.bc8706fc.chunk.js",
"static/css/main.674f8c98.chunk.css", "static/css/main.1b10b1ed.chunk.css",
"static/js/main.06277491.chunk.js" "static/js/main.9f03a409.chunk.js"
] ]
} }

View File

@ -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/2.77671664.chunk.css" rel="stylesheet"><link href="./static/css/main.674f8c98.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],f=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(l&&l(r);p.length;)p.shift()();return u.push.apply(u,f||[]),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:"e51afffb"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(f);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 f=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 f=0;f<a.length;f++)r(a[f]);var l=c;t()}([])</script><script src="./static/js/2.e53c287a.chunk.js"></script><script src="./static/js/main.06277491.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/2.77671664.chunk.css" rel="stylesheet"><link href="./static/css/main.1b10b1ed.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:"7c3472dc"}[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.bc8706fc.chunk.js"></script><script src="./static/js/main.9f03a409.chunk.js"></script></body></html>

View File

@ -0,0 +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:0!important}.cm-activeLine{background-color:inherit!important}.cm-editor{border-radius:4px;border:1px solid #b9b9b9;font-size:10px}.one-line-scroll .cm-editor{height:40px}.cm-gutters{border-radius:4px 0 0 4px;height:100%}.multi-line-scroll .cm-content,.multi-line-scroll .cm-gutters{min-height:38px!important}.one-line-scroll .cm-content,.one-line-scroll .cm-gutters{min-height:auto}.u-tooltip{position:absolute;display:none;grid-gap:12px;max-width:300px;padding:8px;border-radius:4px;background:rgba(57,57,57,.9);color:#fff;font-size:10px;line-height:1.4em;font-weight:500;word-wrap:break-word;font-family:monospace;pointer-events:none;z-index:100}.u-tooltip-data{display:flex;flex-wrap:wrap;align-items:center;font-size:11px;line-height:150%}.u-tooltip-data__value{padding:4px;font-weight:700}.u-tooltip__info{display:grid;grid-gap:4px}.u-tooltip__marker{width:12px;height:12px;margin-right:4px}.legendWrapper{display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));grid-gap:20px;margin-top:20px;cursor:default}.legendGroup{margin-bottom:24px}.legendGroupTitle{display:flex;align-items:center;padding:10px 0 5px;font-size:11px}.legendGroupLine{margin:0 10px}.legendItem{display:inline-grid;grid-template-columns:auto auto;grid-gap:6px;align-items:start;justify-content:start;padding:5px 10px;background-color:#fff;cursor:pointer;transition:.2s ease}.legendItemHide{text-decoration:line-through;opacity:.5}.legendItem:hover{background-color:rgba(0,0,0,.1)}.legendMarker{width:12px;height:12px;border-width:2px;border-style:solid;box-sizing:border-box;transition:.2s ease;margin:3px 0}.legendLabel{font-size:11px;font-weight:400}

View File

@ -1 +0,0 @@
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-editor{border-radius:4px;border:1px solid #b9b9b9;font-size:10px}.one-line-scroll .cm-editor{height:24px}.cm-gutters{border-radius:4px 0 0 4px;height:100%}.multi-line-scroll .cm-content,.multi-line-scroll .cm-gutters{min-height:64px!important}.one-line-scroll .cm-content,.one-line-scroll .cm-gutters{min-height:auto}.u-tooltip{position:absolute;display:none;grid-gap:12px;max-width:300px;padding:8px;border-radius:4px;background:rgba(57,57,57,.9);color:#fff;font-size:10px;line-height:1.4em;font-weight:500;word-wrap:break-word;font-family:monospace;pointer-events:none;z-index:100}.u-tooltip-data{display:flex;flex-wrap:wrap;align-items:center;font-size:11px;line-height:150%}.u-tooltip-data__value{padding:4px;font-weight:700}.u-tooltip__info{display:grid;grid-gap:4px}.u-tooltip__marker{width:12px;height:12px;margin-right:4px}.legendWrapper{margin-top:20px}.legendItem{display:inline-grid;grid-template-columns:auto auto;grid-gap:4px;align-items:center;justify-content:start;padding:5px 10px;background-color:#fff;cursor:pointer;transition:.2s ease}.legendItemHide{text-decoration:line-through;opacity:.5}.legendItem:hover{background-color:rgba(0,0,0,.1)}.legendMarker{width:12px;height:12px;border-width:2px;border-style:solid;box-sizing:border-box;transition:.2s ease}.legendLabel{font-size:12px;font-weight:600}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(this.webpackJsonpvmui=this.webpackJsonpvmui||[]).push([[3],{351:function(e,t,n){"use strict";n.r(t),n.d(t,"getCLS",(function(){return y})),n.d(t,"getFCP",(function(){return g})),n.d(t,"getFID",(function(){return C})),n.d(t,"getLCP",(function(){return k})),n.d(t,"getTTFB",(function(){return D}));var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},P={},k=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e)),n()},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){P[r.id]||(o.takeRecords().map(a),o.disconnect(),P[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,P[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("pageshow",t)}}}]); (this.webpackJsonpvmui=this.webpackJsonpvmui||[]).push([[3],{355:function(e,t,n){"use strict";n.r(t),n.d(t,"getCLS",(function(){return y})),n.d(t,"getFCP",(function(){return g})),n.d(t,"getFID",(function(){return C})),n.d(t,"getLCP",(function(){return k})),n.d(t,"getTTFB",(function(){return D}));var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},P={},k=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e)),n()},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){P[r.id]||(o.takeRecords().map(a),o.disconnect(),P[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,P[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("pageshow",t)}}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],f=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(l&&l(r);p.length;)p.shift()();return u.push.apply(u,f||[]),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:"e51afffb"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(f);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 f=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 f=0;f<a.length;f++)r(a[f]);var l=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:"7c3472dc"}[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()}([]);

View File

@ -40,9 +40,10 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"qs": "^6.10.1", "qs": "^6.10.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-measure": "^2.5.2", "react-measure": "^2.5.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "~4.5.2", "typescript": "~4.5.2",
@ -1809,9 +1810,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.15.4", "version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
}, },
@ -3129,17 +3130,6 @@
} }
} }
}, },
"node_modules/@mui/base/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/icons-material": { "node_modules/@mui/icons-material": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.2.0.tgz",
@ -3161,17 +3151,6 @@
} }
} }
}, },
"node_modules/@mui/icons-material/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/lab": { "node_modules/@mui/lab": {
"version": "5.0.0-alpha.58", "version": "5.0.0-alpha.58",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.58.tgz", "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.58.tgz",
@ -3222,17 +3201,6 @@
} }
} }
}, },
"node_modules/@mui/lab/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.2.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.2.tgz",
@ -3277,17 +3245,6 @@
} }
} }
}, },
"node_modules/@mui/material/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/private-theming": { "node_modules/@mui/private-theming": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.2.2.tgz", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.2.2.tgz",
@ -3314,17 +3271,6 @@
} }
} }
}, },
"node_modules/@mui/private-theming/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/styled-engine": { "node_modules/@mui/styled-engine": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.2.0.tgz",
@ -3355,17 +3301,6 @@
} }
} }
}, },
"node_modules/@mui/styled-engine/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/styles": { "node_modules/@mui/styles": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/@mui/styles/-/styles-5.2.2.tgz", "resolved": "https://registry.npmjs.org/@mui/styles/-/styles-5.2.2.tgz",
@ -3406,17 +3341,6 @@
} }
} }
}, },
"node_modules/@mui/styles/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/system": { "node_modules/@mui/system": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.2.2.tgz", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.2.2.tgz",
@ -3456,17 +3380,6 @@
} }
} }
}, },
"node_modules/@mui/system/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mui/types": { "node_modules/@mui/types": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.0.tgz",
@ -3498,17 +3411,6 @@
"react": "^17.0.2" "react": "^17.0.2"
} }
}, },
"node_modules/@mui/utils/node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4305,9 +4207,9 @@
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==" "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ=="
}, },
"node_modules/@types/testing-library__jest-dom": { "node_modules/@types/testing-library__jest-dom": {
"version": "5.14.1", "version": "5.14.2",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz",
"integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==", "integrity": "sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg==",
"dependencies": { "dependencies": {
"@types/jest": "*" "@types/jest": "*"
} }
@ -6052,14 +5954,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bindings": {
"version": "1.5.0",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bluebird": { "node_modules/bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -7893,9 +7787,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"node_modules/diff-sequences": { "node_modules/diff-sequences": {
"version": "27.4.0", "version": "27.0.6",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz",
"integrity": "sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==", "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==",
"engines": { "engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
} }
@ -9745,12 +9639,6 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"node_modules/filesize": { "node_modules/filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@ -12193,29 +12081,14 @@
} }
}, },
"node_modules/jest-diff": { "node_modules/jest-diff": {
"version": "27.4.0", "version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.0.tgz", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.3.1.tgz",
"integrity": "sha512-fdXgpnyQH4LNSnYgRfHN/g413bqbPspWIAZPlXrdNISehDih1VNDtuRvlzGQJ4Go+fur1HKB2IyI25t6cWi5EA==", "integrity": "sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ==",
"dependencies": { "dependencies": {
"chalk": "^4.0.0", "chalk": "^4.0.0",
"diff-sequences": "^27.4.0", "diff-sequences": "^27.0.6",
"jest-get-type": "^27.4.0", "jest-get-type": "^27.3.1",
"pretty-format": "^27.4.0" "pretty-format": "^27.3.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/jest-diff/node_modules/@jest/types": {
"version": "27.4.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.0.tgz",
"integrity": "sha512-jIsLdASXMf8GS7P7oGFGwobNse/6Ewq3GBPHoo0i6XRmja+NrUoDqJm4a1ffF2bHGleKJizxokcp1sCqSktP3g==",
"dependencies": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^16.0.0",
"chalk": "^4.0.0"
}, },
"engines": { "engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
@ -12236,31 +12109,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/jest-diff/node_modules/pretty-format": {
"version": "27.4.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.0.tgz",
"integrity": "sha512-n0QR6hMREfp6nLzfVksXMAfIxk1ffOOfbb/FzKHFmRtn9iJKaZXB8WMzLr8a72IASShEAhqK06nlwp1gVWgqKg==",
"dependencies": {
"@jest/types": "^27.4.0",
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-docblock": { "node_modules/jest-docblock": {
"version": "26.0.0", "version": "26.0.0",
"resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz",
@ -12457,9 +12305,9 @@
} }
}, },
"node_modules/jest-get-type": { "node_modules/jest-get-type": {
"version": "27.4.0", "version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.3.1.tgz",
"integrity": "sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==", "integrity": "sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg==",
"engines": { "engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
} }
@ -14550,11 +14398,6 @@
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
}, },
"node_modules/nan": {
"version": "2.15.0",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"optional": true
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.1.30", "version": "3.1.30",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
@ -16972,9 +16815,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.10.1", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.2.tgz",
"integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", "integrity": "sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
}, },
@ -17382,6 +17225,19 @@
"react": "17.0.2" "react": "17.0.2"
} }
}, },
"node_modules/react-draggable": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.4.tgz",
"integrity": "sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA==",
"dependencies": {
"clsx": "^1.1.1",
"prop-types": "^15.6.0"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/react-error-overlay": { "node_modules/react-error-overlay": {
"version": "6.0.9", "version": "6.0.9",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
@ -23684,9 +23540,9 @@
} }
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.15.4", "version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": { "requires": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
@ -24763,16 +24619,6 @@
"clsx": "^1.1.1", "clsx": "^1.1.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-is": "^17.0.2" "react-is": "^17.0.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/icons-material": { "@mui/icons-material": {
@ -24781,16 +24627,6 @@
"integrity": "sha512-NvyrVaGKpP4R1yFw8BCnE0QcsQ67RtpgxPr4FtH8q60MDYPuPVczLOn5Ash5CFavoDWur/NfM/4DpT54yf3InA==", "integrity": "sha512-NvyrVaGKpP4R1yFw8BCnE0QcsQ67RtpgxPr4FtH8q60MDYPuPVczLOn5Ash5CFavoDWur/NfM/4DpT54yf3InA==",
"requires": { "requires": {
"@babel/runtime": "^7.16.3" "@babel/runtime": "^7.16.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/lab": { "@mui/lab": {
@ -24811,16 +24647,6 @@
"react-is": "^17.0.2", "react-is": "^17.0.2",
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",
"rifm": "^0.12.1" "rifm": "^0.12.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/material": { "@mui/material": {
@ -24840,16 +24666,6 @@
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-is": "^17.0.2", "react-is": "^17.0.2",
"react-transition-group": "^4.4.2" "react-transition-group": "^4.4.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/private-theming": { "@mui/private-theming": {
@ -24860,16 +24676,6 @@
"@babel/runtime": "^7.16.3", "@babel/runtime": "^7.16.3",
"@mui/utils": "^5.2.2", "@mui/utils": "^5.2.2",
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/styled-engine": { "@mui/styled-engine": {
@ -24880,16 +24686,6 @@
"@babel/runtime": "^7.16.3", "@babel/runtime": "^7.16.3",
"@emotion/cache": "^11.6.0", "@emotion/cache": "^11.6.0",
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/styles": { "@mui/styles": {
@ -24914,16 +24710,6 @@
"jss-plugin-rule-value-function": "^10.8.2", "jss-plugin-rule-value-function": "^10.8.2",
"jss-plugin-vendor-prefixer": "^10.8.2", "jss-plugin-vendor-prefixer": "^10.8.2",
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/system": { "@mui/system": {
@ -24939,16 +24725,6 @@
"clsx": "^1.1.1", "clsx": "^1.1.1",
"csstype": "^3.0.10", "csstype": "^3.0.10",
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@mui/types": { "@mui/types": {
@ -24967,16 +24743,6 @@
"@types/react-is": "^16.7.1 || ^17.0.0", "@types/react-is": "^16.7.1 || ^17.0.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-is": "^17.0.2" "react-is": "^17.0.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
@ -25573,9 +25339,9 @@
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==" "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ=="
}, },
"@types/testing-library__jest-dom": { "@types/testing-library__jest-dom": {
"version": "5.14.1", "version": "5.14.2",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz",
"integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==", "integrity": "sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg==",
"requires": { "requires": {
"@types/jest": "*" "@types/jest": "*"
} }
@ -26901,14 +26667,6 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"optional": true "optional": true
}, },
"bindings": {
"version": "1.5.0",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bluebird": { "bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -28384,9 +28142,9 @@
} }
}, },
"diff-sequences": { "diff-sequences": {
"version": "27.4.0", "version": "27.0.6",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz",
"integrity": "sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==" "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ=="
}, },
"diffie-hellman": { "diffie-hellman": {
"version": "5.0.3", "version": "5.0.3",
@ -29796,12 +29554,6 @@
} }
} }
}, },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"filesize": { "filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@ -31689,28 +31441,16 @@
} }
}, },
"jest-diff": { "jest-diff": {
"version": "27.4.0", "version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.0.tgz", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.3.1.tgz",
"integrity": "sha512-fdXgpnyQH4LNSnYgRfHN/g413bqbPspWIAZPlXrdNISehDih1VNDtuRvlzGQJ4Go+fur1HKB2IyI25t6cWi5EA==", "integrity": "sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ==",
"requires": { "requires": {
"chalk": "^4.0.0", "chalk": "^4.0.0",
"diff-sequences": "^27.4.0", "diff-sequences": "^27.0.6",
"jest-get-type": "^27.4.0", "jest-get-type": "^27.3.1",
"pretty-format": "^27.4.0" "pretty-format": "^27.3.1"
}, },
"dependencies": { "dependencies": {
"@jest/types": {
"version": "27.4.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.0.tgz",
"integrity": "sha512-jIsLdASXMf8GS7P7oGFGwobNse/6Ewq3GBPHoo0i6XRmja+NrUoDqJm4a1ffF2bHGleKJizxokcp1sCqSktP3g==",
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^16.0.0",
"chalk": "^4.0.0"
}
},
"chalk": { "chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -31719,24 +31459,6 @@
"ansi-styles": "^4.1.0", "ansi-styles": "^4.1.0",
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
} }
},
"pretty-format": {
"version": "27.4.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.0.tgz",
"integrity": "sha512-n0QR6hMREfp6nLzfVksXMAfIxk1ffOOfbb/FzKHFmRtn9iJKaZXB8WMzLr8a72IASShEAhqK06nlwp1gVWgqKg==",
"requires": {
"@jest/types": "^27.4.0",
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"dependencies": {
"ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
}
}
} }
} }
}, },
@ -31897,9 +31619,9 @@
} }
}, },
"jest-get-type": { "jest-get-type": {
"version": "27.4.0", "version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.3.1.tgz",
"integrity": "sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==" "integrity": "sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg=="
}, },
"jest-haste-map": { "jest-haste-map": {
"version": "26.6.2", "version": "26.6.2",
@ -33528,11 +33250,6 @@
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
}, },
"nan": {
"version": "2.15.0",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"optional": true
},
"nanoid": { "nanoid": {
"version": "3.1.30", "version": "3.1.30",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
@ -35484,9 +35201,9 @@
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
}, },
"qs": { "qs": {
"version": "6.10.1", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.2.tgz",
"integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", "integrity": "sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==",
"requires": { "requires": {
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
} }
@ -35787,6 +35504,15 @@
"scheduler": "^0.20.2" "scheduler": "^0.20.2"
} }
}, },
"react-draggable": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.4.tgz",
"integrity": "sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA==",
"requires": {
"clsx": "^1.1.1",
"prop-types": "^15.6.0"
}
},
"react-error-overlay": { "react-error-overlay": {
"version": "6.0.9", "version": "6.0.9",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",

View File

@ -36,9 +36,10 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"qs": "^6.10.1", "qs": "^6.10.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-measure": "^2.5.2", "react-measure": "^2.5.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "~4.5.2", "typescript": "~4.5.2",

View File

@ -4,44 +4,15 @@ import HomeLayout from "./components/Home/HomeLayout";
import {StateProvider} from "./state/common/StateContext"; import {StateProvider} from "./state/common/StateContext";
import {AuthStateProvider} from "./state/auth/AuthStateContext"; import {AuthStateProvider} from "./state/auth/AuthStateContext";
import {GraphStateProvider} from "./state/graph/GraphStateContext"; import {GraphStateProvider} from "./state/graph/GraphStateContext";
import { ThemeProvider, Theme, StyledEngineProvider, createTheme } from "@mui/material/styles"; import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
import THEME from "./theme/theme";
import CssBaseline from "@mui/material/CssBaseline"; import CssBaseline from "@mui/material/CssBaseline";
import LocalizationProvider from "@mui/lab/LocalizationProvider"; import LocalizationProvider from "@mui/lab/LocalizationProvider";
// pick a date util library
import DayjsUtils from "@date-io/dayjs"; import DayjsUtils from "@date-io/dayjs";
declare module "@mui/styles/defaultTheme" {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface DefaultTheme extends Theme {}
}
const App: FC = () => { const App: FC = () => {
const THEME = createTheme({
palette: {
primary: {
main: "#3F51B5"
},
secondary: {
main: "#F50057"
}
},
components: {
MuiSwitch: {
defaultProps: {
color: "secondary"
}
}
},
typography: {
"fontSize": 10
}
});
return <> return <>
<CssBaseline /> {/* CSS Baseline: kind of normalize.css made by materialUI team - can be scoped */} <CssBaseline /> {/* CSS Baseline: kind of normalize.css made by materialUI team - can be scoped */}
<LocalizationProvider dateAdapter={DayjsUtils}> {/* Allows datepicker to work with DayJS */} <LocalizationProvider dateAdapter={DayjsUtils}> {/* Allows datepicker to work with DayJS */}

View File

@ -1,4 +1,5 @@
export interface MetricBase { export interface MetricBase {
group: number;
metric: { metric: {
[key: string]: string; [key: string]: string;
}; };

View File

@ -26,8 +26,8 @@ import TabPanel from "./AuthTabPanel";
import PersonIcon from "@mui/icons-material/Person"; import PersonIcon from "@mui/icons-material/Person";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import {useAuthDispatch, useAuthState} from "../../../state/auth/AuthStateContext"; import {useAuthDispatch, useAuthState} from "../../../../state/auth/AuthStateContext";
import {AUTH_METHOD, WithCheckbox} from "../../../state/auth/reducer"; import {AUTH_METHOD, WithCheckbox} from "../../../../state/auth/reducer";
// TODO: make generic when creating second dialog // TODO: make generic when creating second dialog
export interface DialogProps { export interface DialogProps {

View File

@ -0,0 +1,42 @@
import React, {FC, useCallback, useMemo} from "react";
import {Box, FormControlLabel, TextField} from "@mui/material";
import {useGraphDispatch, useGraphState} from "../../../../state/graph/GraphStateContext";
import debounce from "lodash.debounce";
import BasicSwitch from "../../../../theme/switch";
const AxesLimitsConfigurator: FC = () => {
const { yaxis } = useGraphState();
const graphDispatch = useGraphDispatch();
const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]);
const onChangeYaxisLimits = () => { graphDispatch({type: "TOGGLE_ENABLE_YAXIS_LIMITS"}); };
const onChangeLimit = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, axis: string, index: number) => {
const newLimits = yaxis.limits.range;
newLimits[axis][index] = +e.target.value;
graphDispatch({type: "SET_YAXIS_LIMITS", payload: newLimits});
};
const debouncedOnChangeLimit = useCallback(debounce(onChangeLimit, 500), [yaxis.limits.range]);
return <Box display="grid" alignItems="center" gap={2}>
<FormControlLabel
control={<BasicSwitch checked={yaxis.limits.enable} onChange={onChangeYaxisLimits}/>}
label="Fix the limits for y-axis"
/>
<Box display="grid" alignItems="center" gap={4}>
{axes.map(axis => <Box display="grid" gridTemplateColumns="120px 120px" gap={1} key={axis}>
<TextField label={`Min ${axis}`} type="number" size="small" variant="outlined"
disabled={!yaxis.limits.enable}
defaultValue={yaxis.limits.range[axis][0]}
onChange={(e) => debouncedOnChangeLimit(e, axis, 0)}/>
<TextField label={`Max ${axis}`} type="number" size="small" variant="outlined"
disabled={!yaxis.limits.enable}
defaultValue={yaxis.limits.range[axis][1]}
onChange={(e) => debouncedOnChangeLimit(e, axis, 1)} />
</Box>)}
</Box>
</Box>;
};
export default AxesLimitsConfigurator;

View File

@ -0,0 +1,62 @@
import SettingsIcon from "@mui/icons-material/Settings";
import React, {FC, useState, useRef} from "react";
import AxesLimitsConfigurator from "./AxesLimitsConfigurator";
import {Box, Button, IconButton, Paper, Typography} from "@mui/material";
import Draggable from "react-draggable";
import makeStyles from "@mui/styles/makeStyles";
import CloseIcon from "@mui/icons-material/Close";
const useStyles = makeStyles({
popover: {
position: "absolute",
display: "grid",
gridGap: "16px",
padding: "0 0 25px",
zIndex: 2,
},
popoverHeader: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "#3F51B5",
padding: "6px 6px 6px 12px",
borderRadius: "4px 4px 0 0",
color: "#FFF",
cursor: "move",
},
popoverBody: {
padding: "0 14px"
}
});
const GraphSettings: FC = () => {
const [open, setOpen] = useState(false);
const draggableRef = useRef<HTMLDivElement>(null);
const position = { x: 173, y: 0 };
const classes = useStyles();
return <Box display="flex" px={2}>
<Button onClick={() => setOpen((old) => !old)} variant="outlined">
<SettingsIcon sx={{fontSize: 16, marginRight: "4px"}}/>
<span style={{lineHeight: 1, paddingTop: "1px"}}>{open ? "Hide" : "Show"} graph settings</span>
</Button>
{open && (
<Draggable nodeRef={draggableRef} defaultPosition={position} handle="#handle">
<Paper elevation={3} className={classes.popover} ref={draggableRef}>
<div id="handle" className={classes.popoverHeader}>
<Typography variant="body1"><b>Graph Settings</b></Typography>
<IconButton size="small" onClick={() => setOpen(false)}>
<CloseIcon style={{color: "white"}}/>
</IconButton>
</div>
<Box className={classes.popoverBody}>
<AxesLimitsConfigurator/>
</Box>
</Paper>
</Draggable>
)}
</Box>;
};
export default GraphSettings;

View File

@ -0,0 +1,36 @@
import React, {FC} from "react";
import {Box, FormControlLabel} from "@mui/material";
import {saveToStorage} from "../../../../utils/storage";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import BasicSwitch from "../../../../theme/switch";
const AdditionalSettings: FC = () => {
const {queryControls: {autocomplete, nocache}} = useAppState();
const dispatch = useAppDispatch();
const onChangeAutocomplete = () => {
dispatch({type: "TOGGLE_AUTOCOMPLETE"});
saveToStorage("AUTOCOMPLETE", !autocomplete);
};
const onChangeCache = () => {
dispatch({type: "NO_CACHE"});
saveToStorage("NO_CACHE", !nocache);
};
return <Box display="flex" alignItems="center">
<Box>
<FormControlLabel label="Enable autocomplete"
control={<BasicSwitch checked={autocomplete} onChange={onChangeAutocomplete}/>}
/>
</Box>
<Box ml={2}>
<FormControlLabel label="Enable cache"
control={<BasicSwitch checked={!nocache} onChange={onChangeCache}/>}
/>
</Box>
</Box>;
};
export default AdditionalSettings;

View File

@ -0,0 +1,136 @@
import React, {FC, useRef, useState} from "react";
import {
Accordion, AccordionDetails, AccordionSummary, Box, Grid, IconButton, Typography, Tooltip, Button
} from "@mui/material";
import QueryEditor from "./QueryEditor";
import {TimeSelector} from "../Time/TimeSelector";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import HighlightOffIcon from "@mui/icons-material/HighlightOff";
import AddIcon from "@mui/icons-material/Add";
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
import Portal from "@mui/material/Portal";
import ServerConfigurator from "./ServerConfigurator";
import AdditionalSettings from "./AdditionalSettings";
const QueryConfigurator: FC = () => {
const {serverUrl, query, queryHistory, time: {duration}, queryControls: {autocomplete}} = useAppState();
const dispatch = useAppDispatch();
const [expanded, setExpanded] = useState(true);
const [queryString, _setQueryString] = useState(query);
const queryStringRef = useRef(queryString);
const queryContainer = useRef<HTMLDivElement>(null);
const setQueryString = (data: string[]) => {
queryStringRef.current = data;
_setQueryString(data);
};
const onSetDuration = (dur: string) => dispatch({type: "SET_DURATION", payload: dur});
const onRunQuery = () => {
const history = queryHistory.map((h, i) => {
const lastQueryEqual = queryString[i] === h.values[h.values.length - 1];
return {
index: h.values.length - Number(lastQueryEqual),
values: lastQueryEqual ? h.values : [...h.values, queryString[i]]
};
});
dispatch({type: "RUN_QUERY"});
dispatch({type: "SET_QUERY_HISTORY", payload: history});
dispatch({type: "SET_QUERY", payload: queryStringRef.current});
};
const onAddQuery = () => {
const value = [...queryString, ""];
setQueryString(value);
dispatch({type: "SET_QUERY", payload: value});
};
const onRemoveQuery = (index: number) => {
const value = [...queryString];
value.splice(index, 1);
setQueryString(value);
onRunQuery();
};
const onSetQuery = (value: string, index: number) => {
const newQuery = [...queryStringRef.current];
newQuery[index] = value;
setQueryString(newQuery);
};
const setHistoryIndex = (step: number, indexQuery: number) => {
const {index, values} = queryHistory[indexQuery];
const newIndexHistory = index + step;
if (newIndexHistory < 0 || newIndexHistory >= values.length) return;
const newQuery = values[newIndexHistory] || "";
onSetQuery(newQuery, indexQuery);
dispatch({
type: "SET_QUERY_HISTORY_BY_INDEX",
payload: {value: {values, index: newIndexHistory}, queryNumber: indexQuery}
});
};
return <>
<Accordion expanded={expanded} onChange={() => setExpanded(prev => !prev)}>
<AccordionSummary
expandIcon={<IconButton><ExpandMoreIcon/></IconButton>}
aria-controls="panel1a-content"
id="panel1a-header"
sx={{alignItems: "flex-start", padding: "15px"}}
>
<Box mr={2}>
<Typography variant="h6" component="h2">Query Configuration</Typography>
</Box>
<Box flexGrow={1} onClick={e => e.stopPropagation()} onFocusCapture={e => e.stopPropagation()}>
<Portal disablePortal={!expanded} container={queryContainer.current}>
{query.map((q, i) =>
<Box key={`${i}_${q}`} display="grid" gridTemplateColumns="1fr auto" gap="4px" width="100%"
mb={i === query.length-1 ? 0 : 2}>
<QueryEditor server={serverUrl} query={queryString[i]} index={i} oneLiner={!expanded}
autocomplete={autocomplete}
queryHistory={queryHistory[i]} setHistoryIndex={setHistoryIndex}
runQuery={onRunQuery}
setQuery={onSetQuery}/>
{i === 0 && <Tooltip title="Execute Query">
<IconButton onClick={onRunQuery}>
<PlayCircleOutlineIcon/>
</IconButton>
</Tooltip>}
{i > 0 && <Tooltip title="Remove Query">
<IconButton onClick={() => onRemoveQuery(i)}>
<HighlightOffIcon/>
</IconButton>
</Tooltip>}
</Box>)}
</Portal>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container columnSpacing={2}>
<Grid item xs={6} minWidth={400}>
<ServerConfigurator/>
{/* for portal QueryEditor */}
<div ref={queryContainer}/>
{query.length < 2 && <Box display="inline-block" minHeight="40px" mt={2}>
<Button onClick={onAddQuery} variant="outlined">
<AddIcon sx={{fontSize: 16, marginRight: "4px"}}/>
<span style={{lineHeight: 1, paddingTop: "1px"}}>Query</span>
</Button>
</Box>}
</Grid>
<Grid item xs>
<TimeSelector setDuration={onSetDuration} duration={duration}/>
</Grid>
<Grid item xs={12} pt={1}>
<AdditionalSettings/>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</>;
};
export default QueryConfigurator;

View File

@ -4,13 +4,14 @@ import {defaultKeymap} from "@codemirror/commands";
import React, {FC, useEffect, useRef, useState} from "react"; import React, {FC, useEffect, useRef, useState} from "react";
import { PromQLExtension } from "codemirror-promql"; import { PromQLExtension } from "codemirror-promql";
import { basicSetup } from "@codemirror/basic-setup"; import { basicSetup } from "@codemirror/basic-setup";
import {QueryHistory} from "../../../state/common/reducer"; import {QueryHistory} from "../../../../state/common/reducer";
export interface QueryEditorProps { export interface QueryEditorProps {
setHistoryIndex: (step: number) => void; setHistoryIndex: (step: number, index: number) => void;
setQuery: (query: string) => void; setQuery: (query: string, index: number) => void;
runQuery: () => void; runQuery: () => void;
query: string; query: string,
index: number;
queryHistory: QueryHistory; queryHistory: QueryHistory;
server: string; server: string;
oneLiner?: boolean; oneLiner?: boolean;
@ -18,7 +19,15 @@ export interface QueryEditorProps {
} }
const QueryEditor: FC<QueryEditorProps> = ({ const QueryEditor: FC<QueryEditorProps> = ({
query, queryHistory, setHistoryIndex, setQuery, runQuery, server, oneLiner = false, autocomplete index,
query,
queryHistory,
setHistoryIndex,
setQuery,
runQuery,
server,
oneLiner = false,
autocomplete
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -45,7 +54,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
const listenerExtension = EditorView.updateListener.of(editorUpdate => { const listenerExtension = EditorView.updateListener.of(editorUpdate => {
if (editorUpdate.docChanged) { if (editorUpdate.docChanged) {
setQuery(editorUpdate.state.doc.toJSON().map(el => el.trim()).join("")); setQuery(editorUpdate.state.doc.toJSON().map(el => el.trim()).join(""), index);
} }
}); });
@ -66,9 +75,9 @@ const QueryEditor: FC<QueryEditorProps> = ({
if (key === "Enter" && ctrlMetaKey) { if (key === "Enter" && ctrlMetaKey) {
runQuery(); runQuery();
} else if (key === "ArrowUp" && ctrlMetaKey) { } else if (key === "ArrowUp" && ctrlMetaKey) {
setHistoryIndex(-1); setHistoryIndex(-1, index);
} else if (key === "ArrowDown" && ctrlMetaKey) { } else if (key === "ArrowDown" && ctrlMetaKey) {
setHistoryIndex(1); setHistoryIndex(1, index);
} }
}; };

View File

@ -0,0 +1,35 @@
import React, {FC, useState} from "react";
import {Box, TextField, Tooltip, IconButton} from "@mui/material";
import SecurityIcon from "@mui/icons-material/Security";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {AuthDialog} from "../Auth/AuthDialog";
const ServerConfigurator: FC = () => {
const {serverUrl} = useAppState();
const dispatch = useAppDispatch();
const onSetServer = ({target: {value}}: {target: {value: string}}) => {
dispatch({type: "SET_SERVER", payload: value});
};
const [dialogOpen, setDialogOpen] = useState(false);
return <>
<Box display="grid" gridTemplateColumns="1fr auto" gap="4px" alignItems="center" width="100%" mb={2} minHeight={50}>
<TextField variant="outlined" fullWidth label="Server URL" value={serverUrl}
inputProps={{style: {fontFamily: "Monospace"}}}
onChange={onSetServer}/>
<Box>
<Tooltip title="Request Auth Settings">
<IconButton onClick={() => setDialogOpen(true)}>
<SecurityIcon/>
</IconButton>
</Tooltip>
</Box>
</Box>
<AuthDialog open={dialogOpen} onClose={() => setDialogOpen(false)}/>
</>;
};
export default ServerConfigurator;

View File

@ -1,13 +1,13 @@
import {useEffect, useMemo, useState} from "react"; import {useEffect, useMemo, useState} from "react";
import {getQueryRangeUrl, getQueryUrl} from "../../../api/query-range"; import {getQueryRangeUrl, getQueryUrl} from "../../../../api/query-range";
import {useAppState} from "../../../state/common/StateContext"; import {useAppState} from "../../../../state/common/StateContext";
import {InstantMetricResult, MetricResult} from "../../../api/types"; import {InstantMetricResult, MetricBase, MetricResult} from "../../../../api/types";
import {isValidHttpUrl} from "../../../utils/url"; import {isValidHttpUrl} from "../../../../utils/url";
import {useAuthState} from "../../../state/auth/AuthStateContext"; import {useAuthState} from "../../../../state/auth/AuthStateContext";
import {TimeParams} from "../../../types"; import {TimeParams} from "../../../../types";
export const useFetchQuery = (): { export const useFetchQuery = (): {
fetchUrl?: string, fetchUrl?: string[],
isLoading: boolean, isLoading: boolean,
graphData?: MetricResult[], graphData?: MetricResult[],
liveData?: InstantMetricResult[], liveData?: InstantMetricResult[],
@ -40,7 +40,7 @@ export const useFetchQuery = (): {
}, [period]); }, [period]);
const fetchData = async () => { const fetchData = async () => {
if (!fetchUrl) return; if (!fetchUrl?.length) return;
setIsLoading(true); setIsLoading(true);
setPrevPeriod(period); setPrevPeriod(period);
@ -53,14 +53,23 @@ export const useFetchQuery = (): {
} }
try { try {
const response = await fetch(fetchUrl, { headers }); const responses = await Promise.all(fetchUrl.map(url => fetch(url, {headers})));
if (response.ok) { const tempData = [];
const resp = await response.json(); let counter = 1;
setError(undefined); for await (const response of responses) {
displayType === "chart" ? setGraphData(resp.data.result) : setLiveData(resp.data.result); if (response.ok) {
} else { const resp = await response.json();
setError((await response.json())?.error); setError(undefined);
tempData.push(...resp.data.result.map((d: MetricBase) => {
d.group = counter;
return d;
}));
counter++;
} else {
setError((await response.json())?.error);
}
} }
displayType === "chart" ? setGraphData(tempData) : setLiveData(tempData);
} catch (e) { } catch (e) {
if (e instanceof Error) setError(e.message); if (e instanceof Error) setError(e.message);
} }
@ -72,14 +81,14 @@ export const useFetchQuery = (): {
if (!period) return; if (!period) return;
if (!serverUrl) { if (!serverUrl) {
setError("Please enter Server URL"); setError("Please enter Server URL");
} else if (!query.trim()) { } else if (query.every(q => !q.trim())) {
setError("Please enter a valid Query and execute it"); setError("Please enter a valid Query and execute it");
} else if (isValidHttpUrl(serverUrl)) { } else if (isValidHttpUrl(serverUrl)) {
const duration = (period.end - period.start) / 2; const duration = (period.end - period.start) / 2;
const bufferPeriod = {...period, start: period.start - duration, end: period.end + duration}; const bufferPeriod = {...period, start: period.start - duration, end: period.end + duration};
return displayType === "chart" return query.filter(q => q.trim()).map(q => displayType === "chart"
? getQueryRangeUrl(serverUrl, query, bufferPeriod, nocache) ? getQueryRangeUrl(serverUrl, q, bufferPeriod, nocache)
: getQueryUrl(serverUrl, query, period); : getQueryUrl(serverUrl, q, period));
} else { } else {
setError("Please provide a valid URL"); setError("Please provide a valid URL");
} }
@ -88,6 +97,7 @@ export const useFetchQuery = (): {
useEffect(() => { useEffect(() => {
setPrevPeriod(undefined); setPrevPeriod(undefined);
fetchData();
}, [query]); }, [query]);
// TODO: this should depend on query as well, but need to decide when to do the request. // TODO: this should depend on query as well, but need to decide when to do the request.

View File

@ -1,148 +0,0 @@
import React, {FC, useRef, useState} from "react";
import { Accordion, AccordionDetails, AccordionSummary, Box, Grid, IconButton, TextField, Typography, FormControlLabel,
Tooltip, Switch } from "@mui/material";
import QueryEditor from "./QueryEditor";
import {TimeSelector} from "./TimeSelector";
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import SecurityIcon from "@mui/icons-material/Security";
import {AuthDialog} from "./AuthDialog";
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
import Portal from "@mui/material/Portal";
import {saveToStorage} from "../../../utils/storage";
import {useGraphDispatch, useGraphState} from "../../../state/graph/GraphStateContext";
import debounce from "lodash.debounce";
const QueryConfigurator: FC = () => {
const {serverUrl, query, queryHistory, time: {duration}, queryControls: {autocomplete, nocache}} = useAppState();
const dispatch = useAppDispatch();
const onChangeAutocomplete = () => {
dispatch({type: "TOGGLE_AUTOCOMPLETE"});
saveToStorage("AUTOCOMPLETE", !autocomplete);
};
const onChangeCache = () => {
dispatch({type: "NO_CACHE"});
saveToStorage("NO_CACHE", !nocache);
};
const { yaxis } = useGraphState();
const graphDispatch = useGraphDispatch();
const onChangeYaxisLimits = () => { graphDispatch({type: "TOGGLE_ENABLE_YAXIS_LIMITS"}); };
const setMinLimit = ({target: {value}}: {target: {value: string}}) => {
graphDispatch({type: "SET_YAXIS_LIMITS", payload: [+value, yaxis.limits.range[1]]});
};
const setMaxLimit = ({target: {value}}: {target: {value: string}}) => {
graphDispatch({type: "SET_YAXIS_LIMITS", payload: [yaxis.limits.range[0], +value]});
};
const [dialogOpen, setDialogOpen] = useState(false);
const [expanded, setExpanded] = useState(true);
const queryContainer = useRef<HTMLDivElement>(null);
const onSetDuration = (dur: string) => dispatch({type: "SET_DURATION", payload: dur});
const onRunQuery = () => {
const { values } = queryHistory;
dispatch({type: "RUN_QUERY"});
if (query === values[values.length - 1]) return;
dispatch({type: "SET_QUERY_HISTORY_INDEX", payload: values.length});
dispatch({type: "SET_QUERY_HISTORY_VALUES", payload: [...values, query]});
};
const onSetQuery = (newQuery: string) => {
if (query === newQuery) return;
dispatch({type: "SET_QUERY", payload: newQuery});
};
const setHistoryIndex = (step: number) => {
const index = queryHistory.index + step;
if (index < -1 || index > queryHistory.values.length) return;
dispatch({type: "SET_QUERY_HISTORY_INDEX", payload: index});
onSetQuery(queryHistory.values[index] || "");
};
const onSetServer = ({target: {value}}: {target: {value: string}}) => {
dispatch({type: "SET_SERVER", payload: value});
};
return <>
<Accordion expanded={expanded} onChange={() => setExpanded(prev => !prev)}>
<AccordionSummary
expandIcon={<ExpandMoreIcon/>}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Box display="flex" alignItems="center" mr={2}><Typography variant="h6" component="h2">Query Configuration</Typography></Box>
<Box flexGrow={1} onClick={e => e.stopPropagation()} onFocusCapture={e => e.stopPropagation()}>
<Portal disablePortal={!expanded} container={queryContainer.current}>
<Box display="flex" alignItems="center">
<Box width="100%">
<QueryEditor server={serverUrl} query={query} oneLiner={!expanded} autocomplete={autocomplete}
queryHistory={queryHistory} setHistoryIndex={setHistoryIndex} runQuery={onRunQuery} setQuery={onSetQuery}/>
</Box>
<Tooltip title="Execute Query">
<IconButton onClick={onRunQuery} size="large"><PlayCircleOutlineIcon /></IconButton>
</Tooltip>
</Box>
</Portal>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box display="grid" gap={2} gridTemplateRows="auto 1fr">
<Box display="flex" alignItems="center">
<TextField variant="outlined" fullWidth label="Server URL" value={serverUrl}
inputProps={{style: {fontFamily: "Monospace"}}}
onChange={onSetServer}/>
<Box>
<Tooltip title="Request Auth Settings">
<IconButton onClick={() => setDialogOpen(true)} size="large"><SecurityIcon/></IconButton>
</Tooltip>
</Box>
</Box>
<Box flexGrow={1} ><div ref={queryContainer} />{/* for portal QueryEditor */}</Box>
</Box>
</Grid>
<Grid item xs={8} md={6} >
<Box style={{
minHeight: "128px",
padding: "10px 0",
borderRadius: "4px",
borderColor: "#b9b9b9",
borderStyle: "solid",
borderWidth: "1px"}}>
<TimeSelector setDuration={onSetDuration} duration={duration}/>
</Box>
</Grid>
<Grid item xs={12}>
<Box px={1} display="flex" alignItems="center" minHeight={52}>
<Box><FormControlLabel label="Enable autocomplete"
control={<Switch size="small" checked={autocomplete} onChange={onChangeAutocomplete}/>}
/></Box>
<Box ml={2}><FormControlLabel label="Enable cache"
control={<Switch size="small" checked={!nocache} onChange={onChangeCache}/>}
/></Box>
<Box ml={2} display="flex" alignItems="center">
<FormControlLabel
control={<Switch size="small" checked={yaxis.limits.enable} onChange={onChangeYaxisLimits}/>}
label="Fix the limits for y-axis"
/>
{yaxis.limits.enable && <Box display="grid" gridTemplateColumns="120px 120px" gap={1}>
<TextField label="Min" type="number" size="small" variant="outlined"
defaultValue={yaxis.limits.range[0]} onChange={debounce(setMinLimit, 750)}/>
<TextField label="Max" type="number" size="small" variant="outlined"
defaultValue={yaxis.limits.range[1]} onChange={debounce(setMaxLimit, 750)}/>
</Box>}
</Box>
</Box>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
<AuthDialog open={dialogOpen} onClose={() => setDialogOpen(false)}/>
</>;
};
export default QueryConfigurator;

View File

@ -1,9 +1,10 @@
import React, {FC, useEffect, useState} from "react"; import React, {FC, useEffect, useState} from "react";
import {Box, FormControlLabel, IconButton, Switch, Tooltip} from "@mui/material"; import {Box, FormControlLabel, IconButton, Tooltip} from "@mui/material";
import EqualizerIcon from "@mui/icons-material/Equalizer"; import EqualizerIcon from "@mui/icons-material/Equalizer";
import {useAppDispatch, useAppState} from "../../../state/common/StateContext"; import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import CircularProgressWithLabel from "../../common/CircularProgressWithLabel"; import CircularProgressWithLabel from "../../../common/CircularProgressWithLabel";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import BasicSwitch from "../../../../theme/switch";
const useStyles = makeStyles({ const useStyles = makeStyles({
colorizing: { colorizing: {
@ -69,7 +70,7 @@ export const ExecutionControls: FC = () => {
return <Box display="flex" alignItems="center"> return <Box display="flex" alignItems="center">
{<FormControlLabel {<FormControlLabel
control={<Switch size="small" className={classes.colorizing} checked={autoRefresh} onChange={handleChange} />} control={<BasicSwitch className={classes.colorizing} checked={autoRefresh} onChange={handleChange} />}
label="Auto-refresh" label="Auto-refresh"
/>} />}
@ -78,7 +79,9 @@ export const ExecutionControls: FC = () => {
onClick={() => {iterateDelays();}} /> onClick={() => {iterateDelays();}} />
<Tooltip title="Change delay refresh"> <Tooltip title="Change delay refresh">
<Box ml={1}> <Box ml={1}>
<IconButton onClick={() => {iterateDelays();}} size="large"><EqualizerIcon style={{color: "white"}} /></IconButton> <IconButton onClick={() => {iterateDelays();}}>
<EqualizerIcon style={{color: "white"}} />
</IconButton>
</Box> </Box>
</Tooltip> </Tooltip>
</>} </>}

View File

@ -1,6 +1,6 @@
import React, {FC} from "react"; import React, {FC} from "react";
import {Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material"; import {Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material";
import {supportedDurations} from "../../../utils/time"; import {supportedDurations} from "../../../../utils/time";
export const TimeDurationPopover: FC = () => { export const TimeDurationPopover: FC = () => {

View File

@ -2,17 +2,33 @@ import React, {FC, useEffect, useState} from "react";
import {Box, Popover, TextField, Typography} from "@mui/material"; import {Box, Popover, TextField, Typography} from "@mui/material";
import DateTimePicker from "@mui/lab/DateTimePicker"; import DateTimePicker from "@mui/lab/DateTimePicker";
import {TimeDurationPopover} from "./TimeDurationPopover"; import {TimeDurationPopover} from "./TimeDurationPopover";
import {useAppDispatch, useAppState} from "../../../state/common/StateContext"; import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {checkDurationLimit, dateFromSeconds, formatDateForNativeInput} from "../../../utils/time"; import {checkDurationLimit, dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time";
import {InlineBtn} from "../../common/InlineBtn"; import {InlineBtn} from "../../../common/InlineBtn";
import makeStyles from "@mui/styles/makeStyles";
interface TimeSelectorProps { interface TimeSelectorProps {
setDuration: (str: string) => void; setDuration: (str: string) => void;
duration: string; duration: string;
} }
const useStyles = makeStyles({
container: {
display: "grid",
gridTemplateColumns: "auto auto",
height: "100%",
padding: "18px 14px",
borderRadius: "4px",
borderColor: "#b9b9b9",
borderStyle: "solid",
borderWidth: "1px"
}
});
export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => { export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
const classes = useStyles();
const [durationStringFocused, setFocused] = useState(false); const [durationStringFocused, setFocused] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<Element | null>(null); const [anchorEl, setAnchorEl] = React.useState<Element | null>(null);
const [until, setUntil] = useState<string>(); const [until, setUntil] = useState<string>();
@ -60,7 +76,7 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
return <Box m={1} flexDirection="row" display="flex"> return <Box className={classes.container}>
{/*setup duration*/} {/*setup duration*/}
<Box px={1}> <Box px={1}>
<Box> <Box>
@ -72,7 +88,7 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
onFocus={() => {setFocused(true);}} onFocus={() => {setFocused(true);}}
/> />
</Box> </Box>
<Box my={2}> <Box mt={2}>
<Typography variant="body2"> <Typography variant="body2">
<span aria-owns={open ? "mouse-over-popover" : undefined} <span aria-owns={open ? "mouse-over-popover" : undefined}
aria-haspopup="true" aria-haspopup="true"
@ -119,7 +135,7 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
/> />
</Box> </Box>
<Box my={2}> <Box mt={2}>
<Typography variant="body2"> <Typography variant="body2">
Will be changed to current time for auto-refresh mode.&nbsp; Will be changed to current time for auto-refresh mode.&nbsp;
<InlineBtn handler={() => dispatch({type: "RUN_QUERY_TO_NOW"})} text="Switch to now"/> <InlineBtn handler={() => dispatch({type: "RUN_QUERY_TO_NOW"})} text="Switch to now"/>

View File

@ -1,20 +1,19 @@
import React, {FC} from "react"; import React, {FC} from "react";
import {Alert, AppBar, Box, CircularProgress, Fade, Link, Toolbar, Typography} from "@mui/material"; import {Alert, AppBar, Box, CircularProgress, Fade, Link, Toolbar, Typography} from "@mui/material";
import {ExecutionControls} from "./Configurator/ExecutionControls"; import {ExecutionControls} from "./Configurator/Time/ExecutionControls";
import {DisplayTypeSwitch} from "./Configurator/DisplayTypeSwitch"; import {DisplayTypeSwitch} from "./Configurator/DisplayTypeSwitch";
import GraphView from "./Views/GraphView"; import GraphView from "./Views/GraphView";
import TableView from "./Views/TableView"; import TableView from "./Views/TableView";
import {useAppState} from "../../state/common/StateContext"; import {useAppState} from "../../state/common/StateContext";
import QueryConfigurator from "./Configurator/QueryConfigurator"; import QueryConfigurator from "./Configurator/Query/QueryConfigurator";
import {useFetchQuery} from "./Configurator/useFetchQuery"; import {useFetchQuery} from "./Configurator/Query/useFetchQuery";
import JsonView from "./Views/JsonView"; import JsonView from "./Views/JsonView";
import {UrlCopy} from "./UrlCopy";
const HomeLayout: FC = () => { const HomeLayout: FC = () => {
const {displayType, time: {period}} = useAppState(); const {displayType, time: {period}} = useAppState();
const {fetchUrl, isLoading, liveData, graphData, error} = useFetchQuery(); const {isLoading, liveData, graphData, error} = useFetchQuery();
return ( return (
<> <>
@ -46,10 +45,9 @@ const HomeLayout: FC = () => {
<ExecutionControls/> <ExecutionControls/>
</Box> </Box>
<DisplayTypeSwitch/> <DisplayTypeSwitch/>
<UrlCopy url={fetchUrl}/>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Box p={2} display="grid" gridTemplateRows="auto 1fr" gap={"20px"} style={{minHeight: "calc(100vh - 64px)"}}> <Box p={4} display="grid" gridTemplateRows="auto 1fr" gap={"20px"} style={{minHeight: "calc(100vh - 64px)"}}>
<Box> <Box>
<QueryConfigurator/> <QueryConfigurator/>
</Box> </Box>
@ -68,7 +66,7 @@ const HomeLayout: FC = () => {
<CircularProgress/> <CircularProgress/>
</Box> </Box>
</Fade>} </Fade>}
{<Box height={"100%"} p={3} bgcolor={"#fff"}> {<Box height={"100%"} bgcolor={"#fff"}>
{error && {error &&
<Alert color="error" severity="error" style={{fontSize: "14px"}}> <Alert color="error" severity="error" style={{fontSize: "14px"}}>
{error} {error}

View File

@ -2,50 +2,51 @@ import React, {FC, useEffect, useState} from "react";
import {MetricResult} from "../../../api/types"; import {MetricResult} from "../../../api/types";
import LineChart from "../../LineChart/LineChart"; import LineChart from "../../LineChart/LineChart";
import {AlignedData as uPlotData, Series as uPlotSeries} from "uplot"; import {AlignedData as uPlotData, Series as uPlotSeries} from "uplot";
import {Legend, LegendItem} from "../../Legend/Legend"; import Legend from "../../Legend/Legend";
import {useGraphDispatch, useGraphState} from "../../../state/graph/GraphStateContext"; import {useGraphDispatch} from "../../../state/graph/GraphStateContext";
import {getHideSeries, getLegendItem, getLimitsYAxis, getSeriesItem, getTimeSeries} from "../../../utils/uPlot"; import {getHideSeries, getLegendItem, getSeriesItem} from "../../../utils/uplot/series";
import {getLimitsYAxis, getTimeSeries} from "../../../utils/uplot/axes";
import {LegendItem} from "../../../utils/uplot/types";
import {AxisRange} from "../../../state/graph/reducer";
import GraphSettings from "../Configurator/Graph/GraphSettings";
export interface GraphViewProps { export interface GraphViewProps {
data?: MetricResult[]; data?: MetricResult[];
} }
const GraphView: FC<GraphViewProps> = ({data = []}) => { const GraphView: FC<GraphViewProps> = ({data = []}) => {
const { yaxis } = useGraphState();
const graphDispatch = useGraphDispatch(); const graphDispatch = useGraphDispatch();
const [dataChart, setDataChart] = useState<uPlotData>([[]]); const [dataChart, setDataChart] = useState<uPlotData>([[]]);
const [series, setSeries] = useState<uPlotSeries[]>([]); const [series, setSeries] = useState<uPlotSeries[]>([]);
const [legend, setLegend] = useState<LegendItem[]>([]); const [legend, setLegend] = useState<LegendItem[]>([]);
const [hideSeries, setHideSeries] = useState<string[]>([]); const [hideSeries, setHideSeries] = useState<string[]>([]);
const [valuesLimit, setValuesLimit] = useState<[number, number]>([0, 1]); const [valuesLimit, setValuesLimit] = useState<AxisRange>({"1": [0, 1]});
const setLimitsYaxis = (values: number[]) => { const setLimitsYaxis = (values: {[key: string]: number[]}) => {
if (!yaxis.limits.enable || (yaxis.limits.range.every(item => !item))) { const limits = getLimitsYAxis(values);
const limits = getLimitsYAxis(values); setValuesLimit(limits);
setValuesLimit(limits); graphDispatch({type: "SET_YAXIS_LIMITS", payload: limits});
graphDispatch({type: "SET_YAXIS_LIMITS", payload: limits});
}
}; };
const onChangeLegend = (label: string, metaKey: boolean) => { const onChangeLegend = (legend: LegendItem, metaKey: boolean) => {
setHideSeries(getHideSeries({hideSeries, label, metaKey, series})); setHideSeries(getHideSeries({hideSeries, legend, metaKey, series}));
}; };
useEffect(() => { useEffect(() => {
const tempTimes: number[] = []; const tempTimes: number[] = [];
const tempValues: number[] = []; const tempValues: {[key: string]: number[]} = {};
const tempLegend: LegendItem[] = []; const tempLegend: LegendItem[] = [];
const tempSeries: uPlotSeries[] = []; const tempSeries: uPlotSeries[] = [];
data?.forEach(d => { data?.forEach((d) => {
const seriesItem = getSeriesItem(d, hideSeries); const seriesItem = getSeriesItem(d, hideSeries);
tempSeries.push(seriesItem); tempSeries.push(seriesItem);
tempLegend.push(getLegendItem(seriesItem)); tempLegend.push(getLegendItem(seriesItem, d.group));
d.values.forEach(v => { d.values.forEach(v => {
tempTimes.push(v[0]); tempTimes.push(v[0]);
tempValues.push(+v[1]); tempValues[d.group] ? tempValues[d.group].push(+v[1]) : tempValues[d.group] = [+v[1]];
}); });
}); });
@ -68,7 +69,7 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
data?.forEach(d => { data?.forEach(d => {
const seriesItem = getSeriesItem(d, hideSeries); const seriesItem = getSeriesItem(d, hideSeries);
tempSeries.push(seriesItem); tempSeries.push(seriesItem);
tempLegend.push(getLegendItem(seriesItem)); tempLegend.push(getLegendItem(seriesItem, d.group));
}); });
setSeries([{}, ...tempSeries]); setSeries([{}, ...tempSeries]);
setLegend(tempLegend); setLegend(tempLegend);
@ -77,6 +78,7 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
return <> return <>
{(data.length > 0) {(data.length > 0)
? <div> ? <div>
<GraphSettings/>
<LineChart data={dataChart} series={series} metrics={data} limits={valuesLimit}/> <LineChart data={dataChart} series={series} metrics={data} limits={valuesLimit}/>
<Legend labels={legend} onChange={onChangeLegend}/> <Legend labels={legend} onChange={onChangeLegend}/>
</div> </div>

View File

@ -1,31 +1,48 @@
import React, {FC} from "react"; import React, {FC, useMemo} from "react";
import {hexToRGB} from "../../utils/color"; import {hexToRGB} from "../../utils/color";
import {useAppState} from "../../state/common/StateContext";
import {LegendItem} from "../../utils/uplot/types";
import "./legend.css"; import "./legend.css";
import {getDashLine} from "../../utils/uplot/helpers";
export interface LegendItem {
label: string;
color: string;
checked: boolean;
}
export interface LegendProps { export interface LegendProps {
labels: LegendItem[]; labels: LegendItem[];
onChange: (legend: string, metaKey: boolean) => void; onChange: (item: LegendItem, metaKey: boolean) => void;
} }
export const Legend: FC<LegendProps> = ({labels, onChange}) => { const Legend: FC<LegendProps> = ({labels, onChange}) => {
const {query} = useAppState();
const groups = useMemo(() => {
return Array.from(new Set(labels.map(l => l.group)));
}, [labels]);
return <div className="legendWrapper"> return <div className="legendWrapper">
{labels.map((legendItem: LegendItem) => {groups.map((group) => <div className="legendGroup" key={group}>
<div className={legendItem.checked ? "legendItem" : "legendItem legendItemHide"} <div className="legendGroupTitle">
key={legendItem.label} <svg className="legendGroupLine" width="33" height="3" version="1.1" xmlns="http://www.w3.org/2000/svg">
onClick={(e) => onChange(legendItem.label, e.ctrlKey || e.metaKey)}> <line strokeWidth="3" x1="0" y1="0" x2="33" y2="0" stroke="#363636"
<div className="legendMarker" strokeDasharray={getDashLine(group).join(",")}
style={{ />
borderColor: legendItem.color, </svg>
backgroundColor: `rgba(${hexToRGB(legendItem.color)}, 0.1)` <b>&quot;{query[group - 1]}&quot;</b>:
}}/>
<div className="legendLabel">{legendItem.checked} {legendItem.label}</div>
</div> </div>
)} <div>
{labels.filter(l => l.group === group).map((legendItem: LegendItem) =>
<div className={legendItem.checked ? "legendItem" : "legendItem legendItemHide"}
key={`${legendItem.group}.${legendItem.label}`}
onClick={(e) => onChange(legendItem, e.ctrlKey || e.metaKey)}>
<div className="legendMarker"
style={{
borderColor: legendItem.color,
backgroundColor: `rgba(${hexToRGB(legendItem.color)}, 0.1)`
}}/>
<div className="legendLabel">{legendItem.label}</div>
</div>
)}
</div>
</div>)}
</div>; </div>;
}; };
export default Legend;

View File

@ -1,12 +1,31 @@
.legendWrapper { .legendWrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-gap: 20px;
margin-top: 20px; margin-top: 20px;
cursor: default;
}
.legendGroup {
margin-bottom: 24px;
}
.legendGroupTitle {
display: flex;
align-items: center;
padding: 10px 0 5px;
font-size: 11px;
}
.legendGroupLine {
margin: 0 10px;
} }
.legendItem { .legendItem {
display: inline-grid; display: inline-grid;
grid-template-columns: auto auto; grid-template-columns: auto auto;
grid-gap: 4px; grid-gap: 6px;
align-items: center; align-items: start;
justify-content: start; justify-content: start;
padding: 5px 10px; padding: 5px 10px;
background-color: #FFF; background-color: #FFF;
@ -30,9 +49,10 @@
border-style: solid; border-style: solid;
box-sizing: border-box; box-sizing: border-box;
transition: 0.2s ease; transition: 0.2s ease;
margin: 3px 0;
} }
.legendLabel { .legendLabel {
font-size: 12px; font-size: 11px;
font-weight: 600; font-weight: normal;
} }

View File

@ -1,45 +1,47 @@
import React, {FC, useCallback, useEffect, useRef, useState} from "react"; import React, {FC, useCallback, useEffect, useRef, useState} from "react";
import {useAppDispatch, useAppState} from "../../state/common/StateContext"; import {useAppDispatch, useAppState} from "../../state/common/StateContext";
import uPlot, {AlignedData as uPlotData, Options as uPlotOptions, Series as uPlotSeries, Range} from "uplot"; import uPlot, {AlignedData as uPlotData, Options as uPlotOptions, Series as uPlotSeries, Range, Scales, Scale} from "uplot";
import {useGraphState} from "../../state/graph/GraphStateContext"; import {useGraphState} from "../../state/graph/GraphStateContext";
import {defaultOptions, dragChart, setTooltip} from "../../utils/uPlot"; import {defaultOptions} from "../../utils/uplot/helpers";
import {dragChart} from "../../utils/uplot/events";
import {getAxes} from "../../utils/uplot/axes";
import {setTooltip} from "../../utils/uplot/tooltip";
import {MetricResult} from "../../api/types"; import {MetricResult} from "../../api/types";
import {limitsDurations} from "../../utils/time"; import {limitsDurations} from "../../utils/time";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import "uplot/dist/uPlot.min.css"; import "uplot/dist/uPlot.min.css";
import "./tooltip.css"; import "./tooltip.css";
import {AxisRange} from "../../state/graph/reducer";
export interface LineChartProps { export interface LineChartProps {
metrics: MetricResult[] metrics: MetricResult[];
data: uPlotData; data: uPlotData;
series: uPlotSeries[], series: uPlotSeries[];
limits: [number, number] limits: AxisRange;
} }
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
enum typeChartUpdate { xRange = "xRange", yRange = "yRange", data = "data" }
const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) => { const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const {time: {period}} = useAppState(); const {time: {period}} = useAppState();
const { yaxis } = useGraphState(); const {yaxis} = useGraphState();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const uPlotRef = useRef<HTMLDivElement>(null); const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning, setPanning] = useState(false); const [isPanning, setPanning] = useState(false);
const [zoomPos, setZoomPos] = useState(0); const [zoomPos, setZoomPos] = useState(0);
const [xRange, setXRange] = useState({ min: period.start, max: period.end }); const [xRange, setXRange] = useState({min: period.start, max: period.end});
const [uPlotInst, setUPlotInst] = useState<uPlot>(); const [uPlotInst, setUPlotInst] = useState<uPlot>();
const tooltip = document.createElement("div"); const tooltip = document.createElement("div");
tooltip.className = "u-tooltip"; tooltip.className = "u-tooltip";
const tooltipIdx = { seriesIdx: 1, dataIdx: 0 }; const tooltipIdx = {seriesIdx: 1, dataIdx: 0};
const tooltipOffset = { left: 0, top: 0 }; const tooltipOffset = {left: 0, top: 0};
const setScale = ({min, max}: {min: number, max: number}): void => { const setScale = ({min, max}: { min: number, max: number }): void => {
dispatch({type: "SET_PERIOD", payload: {from: new Date(min * 1000), to: new Date(max * 1000)}}); dispatch({type: "SET_PERIOD", payload: {from: new Date(min * 1000), to: new Date(max * 1000)}});
}; };
const throttledSetScale = useCallback(throttle(setScale, 500), []); const throttledSetScale = useCallback(throttle(setScale, 500), []);
const setPlotScale = ({u, min, max}: { u: uPlot, min: number, max: number }) => {
const setPlotScale = ({u, min, max}: {u: uPlot, min: number, max: number}) => {
const delta = (max - min) * 1000; const delta = (max - min) * 1000;
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return; if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
u.setScale("x", {min, max}); u.setScale("x", {min, max});
@ -52,12 +54,8 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
tooltipOffset.left = parseFloat(u.over.style.left); tooltipOffset.left = parseFloat(u.over.style.left);
tooltipOffset.top = parseFloat(u.over.style.top); tooltipOffset.top = parseFloat(u.over.style.top);
u.root.querySelector(".u-wrap")?.appendChild(tooltip); u.root.querySelector(".u-wrap")?.appendChild(tooltip);
// wheel drag pan // wheel drag pan
u.over.addEventListener("mousedown", e => { u.over.addEventListener("mousedown", e => dragChart({u, e, setPanning, setPlotScale, factor}));
dragChart({u, e, setPanning, setPlotScale, factor});
});
// wheel scroll zoom // wheel scroll zoom
u.over.addEventListener("wheel", e => { u.over.addEventListener("wheel", e => {
if (!e.ctrlKey && !e.metaKey) return; if (!e.ctrlKey && !e.metaKey) return;
@ -67,7 +65,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
const xVal = u.posToVal(zoomPos, "x"); const xVal = u.posToVal(zoomPos, "x");
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0); const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor; const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
const min = xVal - (zoomPos/width) * nxRange; const min = xVal - (zoomPos / width) * nxRange;
const max = min + nxRange; const max = min + nxRange;
u.batch(() => setPlotScale({u, min, max})); u.batch(() => setPlotScale({u, min, max}));
}); });
@ -88,25 +86,27 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
? setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset}) ? setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset})
: tooltip.style.display = "none"; : tooltip.style.display = "none";
}; };
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
const getRangeY = (u: uPlot, min = 0, max = 1): Range.MinMax => { const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
if (yaxis.limits.enable) return yaxis.limits.range; if (yaxis.limits.enable) return yaxis.limits.range[axis];
return min && max ? [min - (min * 0.05), max + (max * 0.05)] : limits; return min && max ? [min - (min * 0.05), max + (max * 0.05)] : limits[axis];
}; };
const getRangeX = (): Range.MinMax => { const getScales = (): Scales => {
return [xRange.min, xRange.max]; const scales: { [key: string]: { range: Scale.Range } } = {x: {range: getRangeX}};
Object.keys(yaxis.limits.range).forEach(axis => {
scales[axis] = {range: (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis)};
});
return scales;
}; };
const options: uPlotOptions = { const options: uPlotOptions = {
...defaultOptions, ...defaultOptions,
width: containerRef.current ? containerRef.current.offsetWidth : 400,
series, series,
plugins: [{ hooks: { ready: onReadyChart, setCursor, setSeries: seriesFocus }}], axes: getAxes(series),
scales: { scales: {...getScales()},
x: { range: getRangeX }, width: containerRef.current ? containerRef.current.offsetWidth : 400,
y: { range: getRangeY } plugins: [{hooks: {ready: onReadyChart, setCursor, setSeries: seriesFocus}}],
}
}; };
const updateChart = (type: typeChartUpdate): void => { const updateChart = (type: typeChartUpdate): void => {
@ -116,7 +116,10 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
uPlotInst.scales.x.range = getRangeX; uPlotInst.scales.x.range = getRangeX;
break; break;
case typeChartUpdate.yRange: case typeChartUpdate.yRange:
uPlotInst.scales.y.range = getRangeY; Object.keys(yaxis.limits.range).forEach(axis => {
if (!uPlotInst.scales[axis]) return;
uPlotInst.scales[axis].range = (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis);
});
break; break;
case typeChartUpdate.data: case typeChartUpdate.data:
uPlotInst.setData(data); uPlotInst.setData(data);
@ -125,13 +128,13 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
uPlotInst.redraw(); uPlotInst.redraw();
}; };
useEffect(() => setXRange({ min: period.start, max: period.end }), [period]); useEffect(() => setXRange({min: period.start, max: period.end}), [period]);
useEffect(() => { useEffect(() => {
if (!uPlotRef.current) return; if (!uPlotRef.current) return;
const u = new uPlot(options, data, uPlotRef.current); const u = new uPlot(options, data, uPlotRef.current);
setUPlotInst(u); setUPlotInst(u);
setXRange({ min: period.start, max: period.end }); setXRange({min: period.start, max: period.end});
return u.destroy; return u.destroy;
}, [uPlotRef.current, series]); }, [uPlotRef.current, series]);

View File

@ -14,7 +14,7 @@ code {
/*Material UI global classes*/ /*Material UI global classes*/
.MuiAccordionSummary-content { .MuiAccordionSummary-content {
margin: 10px 0 !important; margin: 0 !important;
} }
/* TODO: find better way to override codemirror styles */ /* TODO: find better way to override codemirror styles */
@ -30,7 +30,7 @@ code {
} }
.one-line-scroll .cm-editor { .one-line-scroll .cm-editor {
height: 24px; height: 40px;
} }
.cm-gutters { .cm-gutters {
@ -40,7 +40,7 @@ code {
.multi-line-scroll .cm-content, .multi-line-scroll .cm-content,
.multi-line-scroll .cm-gutters { .multi-line-scroll .cm-gutters {
min-height: 64px !important; min-height: 38px !important;
} }
.one-line-scroll .cm-content, .one-line-scroll .cm-content,

View File

@ -4,7 +4,7 @@ import {TimeParams, TimePeriod} from "../../types";
import {dateFromSeconds, formatDateToLocal, getDateNowUTC, getDurationFromPeriod, getTimeperiodForDuration} from "../../utils/time"; import {dateFromSeconds, formatDateToLocal, getDateNowUTC, getDurationFromPeriod, getTimeperiodForDuration} from "../../utils/time";
import {getFromStorage} from "../../utils/storage"; import {getFromStorage} from "../../utils/storage";
import {getDefaultServer} from "../../utils/default-server-url"; import {getDefaultServer} from "../../utils/default-server-url";
import {getQueryStringValue} from "../../utils/query-string"; import {getQueryArray, getQueryStringValue} from "../../utils/query-string";
export interface TimeState { export interface TimeState {
duration: string; duration: string;
@ -19,9 +19,9 @@ export interface QueryHistory {
export interface AppState { export interface AppState {
serverUrl: string; serverUrl: string;
displayType: DisplayType; displayType: DisplayType;
query: string; query: string[];
time: TimeState; time: TimeState;
queryHistory: QueryHistory, queryHistory: QueryHistory[],
queryControls: { queryControls: {
autoRefresh: boolean; autoRefresh: boolean;
autocomplete: boolean, autocomplete: boolean,
@ -32,9 +32,9 @@ export interface AppState {
export type Action = export type Action =
| { type: "SET_DISPLAY_TYPE", payload: DisplayType } | { type: "SET_DISPLAY_TYPE", payload: DisplayType }
| { type: "SET_SERVER", payload: string } | { type: "SET_SERVER", payload: string }
| { type: "SET_QUERY", payload: string } | { type: "SET_QUERY", payload: string[] }
| { type: "SET_QUERY_HISTORY_INDEX", payload: number } | { type: "SET_QUERY_HISTORY_BY_INDEX", payload: {value: QueryHistory, queryNumber: number} }
| { type: "SET_QUERY_HISTORY_VALUES", payload: string[] } | { type: "SET_QUERY_HISTORY", payload: QueryHistory[] }
| { type: "SET_DURATION", payload: string } | { type: "SET_DURATION", payload: string }
| { type: "SET_UNTIL", payload: Date } | { type: "SET_UNTIL", payload: Date }
| { type: "SET_PERIOD", payload: TimePeriod } | { type: "SET_PERIOD", payload: TimePeriod }
@ -46,13 +46,13 @@ export type Action =
const duration = getQueryStringValue("g0.range_input", "1h") as string; const duration = getQueryStringValue("g0.range_input", "1h") as string;
const endInput = formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as Date); const endInput = formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as Date);
const query = getQueryStringValue("g0.expr", "") as string; const query = getQueryArray();
export const initialState: AppState = { export const initialState: AppState = {
serverUrl: getDefaultServer(), serverUrl: getDefaultServer(),
displayType: getQueryStringValue("tab", "chart") as DisplayType, displayType: getQueryStringValue("tab", "chart") as DisplayType,
query: query, // demo_memory_usage_bytes query: query, // demo_memory_usage_bytes
queryHistory: { index: 0, values: [query] }, queryHistory: query.map(q => ({index: 0, values: [q]})),
time: { time: {
duration, duration,
period: getTimeperiodForDuration(duration, new Date(endInput)) period: getTimeperiodForDuration(duration, new Date(endInput))
@ -81,21 +81,15 @@ export function reducer(state: AppState, action: Action): AppState {
...state, ...state,
query: action.payload query: action.payload
}; };
case "SET_QUERY_HISTORY_INDEX": case "SET_QUERY_HISTORY":
return { return {
...state, ...state,
queryHistory: { queryHistory: action.payload
...state.queryHistory,
index: action.payload
}
}; };
case "SET_QUERY_HISTORY_VALUES": case "SET_QUERY_HISTORY_BY_INDEX":
return { return {
...state, ...state,
queryHistory: { queryHistory: state.queryHistory.splice(action.payload.queryNumber, 1, action.payload.value)
...state.queryHistory,
values: action.payload
}
}; };
case "SET_DURATION": case "SET_DURATION":
return { return {

View File

@ -1,7 +1,11 @@
export interface AxisRange {
[key: string]: [number, number]
}
export interface YaxisState { export interface YaxisState {
limits: { limits: {
enable: boolean, enable: boolean,
range: [number, number] range: AxisRange
} }
} }
@ -11,11 +15,11 @@ export interface GraphState {
export type GraphAction = export type GraphAction =
| { type: "TOGGLE_ENABLE_YAXIS_LIMITS" } | { type: "TOGGLE_ENABLE_YAXIS_LIMITS" }
| { type: "SET_YAXIS_LIMITS", payload: [number, number] } | { type: "SET_YAXIS_LIMITS", payload: {[key: string]: [number, number]} }
export const initialGraphState: GraphState = { export const initialGraphState: GraphState = {
yaxis: { yaxis: {
limits: {enable: false, range: [0, 0]} limits: {enable: false, range: {"1": [0, 0]}}
} }
}; };

View File

@ -0,0 +1,25 @@
import {styled} from "@mui/material/styles";
import Switch from "@mui/material/Switch";
const BasicSwitch = styled(Switch)(() => ({
padding: 10,
"& .MuiSwitch-track": {
borderRadius: 14,
"&:before, &:after": {
content: "\"\"",
position: "absolute",
top: "50%",
transform: "translateY(-50%)",
width: 14,
height: 14,
},
},
"& .MuiSwitch-thumb": {
boxShadow: "none",
width: 12,
height: 12,
margin: 4,
},
}));
export default BasicSwitch;

View File

@ -0,0 +1,56 @@
import {createTheme} from "@mui/material/styles";
const THEME = createTheme({
palette: {
primary: {
main: "#3F51B5"
},
secondary: {
main: "#F50057"
}
},
components: {
MuiSwitch: {
defaultProps: {
color: "secondary"
}
},
MuiAccordion: {
styleOverrides: {
root: {
boxShadow: "rgba(0, 0, 0, 0.16) 0px 1px 4px;"
},
},
},
MuiPaper: {
styleOverrides: {
elevation3: {
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 8px;"
},
},
},
MuiIconButton: {
defaultProps: {
size: "large",
},
styleOverrides: {
sizeLarge: {
borderRadius: "20%",
height: "40px",
width: "41px"
},
sizeMedium: {
borderRadius: "20%",
},
sizeSmall: {
borderRadius: "20%",
}
}
}
},
typography: {
"fontSize": 10
}
});
export default THEME;

View File

@ -2,10 +2,9 @@ import qs from "qs";
import get from "lodash.get"; import get from "lodash.get";
const stateToUrlParams = { const stateToUrlParams = {
"query": "g0.expr", "time.duration": "range_input",
"time.duration": "g0.range_input", "time.period.date": "end_input",
"time.period.date": "g0.end_input", "time.period.step": "step_input",
"time.period.step": "g0.step_input",
"displayType": "tab" "displayType": "tab"
}; };
@ -39,15 +38,19 @@ export const setQueryStringWithoutPageReload = (qsValue: string): void => {
export const setQueryStringValue = (newValue: Record<string, unknown>): void => { export const setQueryStringValue = (newValue: Record<string, unknown>): void => {
const queryMap = new Map(Object.entries(stateToUrlParams)); const queryMap = new Map(Object.entries(stateToUrlParams));
const query = get(newValue, "query", "") as string[];
const newQsValue: string[] = []; const newQsValue: string[] = [];
queryMap.forEach((queryKey, stateKey) => { query.forEach((q, i) => {
// const queryKeyEncoded = encodeURIComponent(queryKey); queryMap.forEach((queryKey, stateKey) => {
const value = get(newValue, stateKey, "") as string; const value = get(newValue, stateKey, "") as string;
if (value) { if (value) {
const valueEncoded = encodeURIComponent(value); const valueEncoded = encodeURIComponent(value);
newQsValue.push(`${queryKey}=${valueEncoded}`); newQsValue.push(`g${i}.${queryKey}=${valueEncoded}`);
} }
});
newQsValue.push(`g${i}.expr=${q}`);
}); });
setQueryStringWithoutPageReload(newQsValue.join("&")); setQueryStringWithoutPageReload(newQsValue.join("&"));
}; };
@ -59,3 +62,10 @@ export const getQueryStringValue = (
const values = qs.parse(queryString, { ignoreQueryPrefix: true }); const values = qs.parse(queryString, { ignoreQueryPrefix: true });
return get(values, key, defaultValue || ""); return get(values, key, defaultValue || "");
}; };
export const getQueryArray = (): string[] => {
const queryLength = window.location.search.match(/g\d+.expr/gmi)?.length || 1;
return new Array(queryLength).fill(1).map((q, i) => {
return getQueryStringValue(`g${i}.expr`, "") as string;
});
};

View File

@ -1,147 +0,0 @@
import uPlot, {Series as uPlotSeries, Series} from "uplot";
import {getColorFromString} from "./color";
import dayjs from "dayjs";
import {MetricResult} from "../api/types";
import {LegendItem} from "../components/Legend/Legend";
import {getNameForMetric} from "./metric";
import {getMaxFromArray, getMinFromArray} from "./math";
import {roundTimeSeconds} from "./time";
import numeral from "numeral";
interface SetupTooltip {
u: uPlot,
metrics: MetricResult[],
series: Series[],
tooltip: HTMLDivElement,
tooltipOffset: {left: number, top: number},
tooltipIdx: {seriesIdx: number, dataIdx: number}
}
interface HideSeriesArgs {
hideSeries: string[],
label: string,
metaKey: boolean,
series: Series[]
}
interface DragArgs {
e: MouseEvent,
u: uPlot,
factor: number,
setPanning: (enable: boolean) => void,
setPlotScale: ({u, min, max}: {u: uPlot, min: number, max: number}) => void
}
const stub = (): null => null;
export const defaultOptions = {
height: 500,
legend: { show: false },
axes: [
{ space: 80 },
{
show: true,
font: "10px Arial",
values: (self: uPlot, ticks: number[]): (string | number)[] => ticks.map(n => n > 1000 ? numeral(n).format("0.0a") : n)
}
],
cursor: {
drag: { x: false, y: false },
focus: { prox: 30 },
bind: { mouseup: stub, mousedown: stub, click: stub, dblclick: stub, mouseenter: stub }
},
};
export const setTooltip = ({ u, tooltipIdx, metrics, series, tooltip, tooltipOffset }: SetupTooltip) : void => {
const {seriesIdx, dataIdx} = tooltipIdx;
const dataSeries = u.data[seriesIdx][dataIdx];
const dataTime = u.data[0][dataIdx];
const metric = metrics[seriesIdx - 1]?.metric || {};
const color = getColorFromString(series[seriesIdx].label || "");
const {width, height} = u.over.getBoundingClientRect();
const top = u.valToPos((dataSeries || 0), "y");
const lft = u.valToPos(dataTime, "x");
const {width: tooltipWidth, height: tooltipHeight} = tooltip.getBoundingClientRect();
const overflowX = lft + tooltipWidth >= width;
const overflowY = top + tooltipHeight >= height;
tooltip.style.display = "grid";
tooltip.style.top = `${tooltipOffset.top + top + 10 - (overflowY ? tooltipHeight + 10 : 0)}px`;
tooltip.style.left = `${tooltipOffset.left + lft + 10 - (overflowX ? tooltipWidth + 20 : 0)}px`;
const date = dayjs(new Date(dataTime * 1000)).format("YYYY-MM-DD HH:mm:ss:SSS (Z)");
const info = Object.keys(metric).filter(k => k !== "__name__").map(k => `<div><b>${k}</b>: ${metric[k]}</div>`).join("");
const marker = `<div class="u-tooltip__marker" style="background: ${color}"></div>`;
tooltip.innerHTML = `<div>${date}</div>
<div class="u-tooltip-data">
${marker}${metric.__name__ || ""}: <b class="u-tooltip-data__value">${dataSeries}</b>
</div>
<div class="u-tooltip__info">${info}</div>`;
};
export const getHideSeries = ({hideSeries, label, metaKey, series}: HideSeriesArgs): string[] => {
const include = hideSeries.includes(label);
const labels = series.map(s => s.label || "").filter(l => l);
if (metaKey && include) {
return [...labels.filter(l => l !== label)];
} else if (metaKey && !include) {
return hideSeries.length === series.length - 2 ? [] : [...labels.filter(l => l !== label)];
}
return include ? hideSeries.filter(l => l !== label) : [...hideSeries, label];
};
export const getTimeSeries = (times: number[]): number[] => {
const allTimes = Array.from(new Set(times)).sort((a,b) => a-b);
const step = getMinFromArray(allTimes.map((t, i) => allTimes[i + 1] - t));
const length = allTimes.length;
const startTime = allTimes[0] || 0;
return new Array(length).fill(startTime).map((d, i) => roundTimeSeconds(d + (step * i)));
};
export const getLimitsYAxis = (values: number[]): [number, number] => {
const min = getMinFromArray(values);
const max = getMaxFromArray(values);
return [min - (min * 0.05), max + (max * 0.05)];
};
export const getSeriesItem = (d: MetricResult, hideSeries: string[]): Series => {
const label = getNameForMetric(d);
return {
label,
width: 1.5,
stroke: getColorFromString(label),
show: !hideSeries.includes(label),
scale: "y"
};
};
export const getLegendItem = (s: uPlotSeries): LegendItem => ({
label: s.label || "",
color: s.stroke as string,
checked: s.show || false
});
export const dragChart = ({e, factor = 0.85, u, setPanning, setPlotScale}: DragArgs): void => {
if (e.button !== 0) return;
e.preventDefault();
setPanning(true);
const leftStart = e.clientX;
const xUnitsPerPx = u.posToVal(1, "x") - u.posToVal(0, "x");
const scXMin = u.scales.x.min || 0;
const scXMax = u.scales.x.max || 0;
const mouseMove = (e: MouseEvent) => {
e.preventDefault();
const dx = xUnitsPerPx * ((e.clientX - leftStart) * factor);
setPlotScale({u, min: scXMin - dx, max: scXMax - dx});
};
const mouseUp = () => {
setPanning(false);
document.removeEventListener("mousemove", mouseMove);
document.removeEventListener("mouseup", mouseUp);
};
document.addEventListener("mousemove", mouseMove);
document.addEventListener("mouseup", mouseUp);
};

View File

@ -0,0 +1,30 @@
import {Axis, Series} from "uplot";
import {getMaxFromArray, getMinFromArray} from "../math";
import {roundTimeSeconds} from "../time";
import {AxisRange} from "../../state/graph/reducer";
import {formatTicks} from "./helpers";
export const getAxes = (series: Series[]): Axis[] => Array.from(new Set(series.map(s => s.scale))).map(a => {
const axis = {scale: a, show: true, font: "10px Arial", values: formatTicks};
if (!a) return {space: 80};
if (!(Number(a) % 2)) return {...axis, side: 1};
return axis;
});
export const getTimeSeries = (times: number[]): number[] => {
const allTimes = Array.from(new Set(times)).sort((a, b) => a - b);
const step = getMinFromArray(allTimes.map((t, i) => allTimes[i + 1] - t));
const startTime = allTimes[0] || 0;
return new Array(allTimes.length).fill(startTime).map((d, i) => roundTimeSeconds(d + (step * i)));
};
export const getLimitsYAxis = (values: { [key: string]: number[] }): AxisRange => {
const result: AxisRange = {};
for (const key in values) {
const numbers = values[key];
const min = getMinFromArray(numbers);
const max = getMaxFromArray(numbers);
result[key] = [min - (min * 0.05), max + (max * 0.05)];
}
return result;
};

View File

@ -0,0 +1,25 @@
import {DragArgs} from "./types";
export const dragChart = ({e, factor = 0.85, u, setPanning, setPlotScale}: DragArgs): void => {
if (e.button !== 0) return;
e.preventDefault();
setPanning(true);
const leftStart = e.clientX;
const xUnitsPerPx = u.posToVal(1, "x") - u.posToVal(0, "x");
const scXMin = u.scales.x.min || 0;
const scXMax = u.scales.x.max || 0;
const mouseMove = (e: MouseEvent) => {
e.preventDefault();
const dx = xUnitsPerPx * ((e.clientX - leftStart) * factor);
setPlotScale({u, min: scXMin - dx, max: scXMax - dx});
};
const mouseUp = () => {
setPanning(false);
document.removeEventListener("mousemove", mouseMove);
document.removeEventListener("mouseup", mouseUp);
};
document.addEventListener("mousemove", mouseMove);
document.addEventListener("mouseup", mouseUp);
};

View File

@ -0,0 +1,34 @@
import uPlot from "uplot";
import numeral from "numeral";
import {getColorFromString} from "../color";
export const defaultOptions = {
height: 500,
legend: {
show: false
},
cursor: {
drag: {
x: false,
y: false
},
focus: {
prox: 30
},
bind: {
mouseup: (): null => null,
mousedown: (): null => null,
click: (): null => null,
dblclick: (): null => null,
mouseenter: (): null => null
}
},
};
export const formatTicks = (u: uPlot, ticks: number[]): (string | number)[] => {
return ticks.map(n => n > 1000 ? numeral(n).format("0.0a") : n);
};
export const getColorLine = (scale: number, label: string): string => getColorFromString(`${scale}${label}`);
export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];

View File

@ -0,0 +1,41 @@
import {MetricResult} from "../../api/types";
import {Series} from "uplot";
import {getNameForMetric} from "../metric";
import {LegendItem} from "./types";
import {getColorLine, getDashLine} from "./helpers";
import {HideSeriesArgs} from "./types";
export const getSeriesItem = (d: MetricResult, hideSeries: string[]): Series => {
const label = getNameForMetric(d);
return {
label,
dash: getDashLine(d.group),
width: 1.5,
stroke: getColorLine(d.group, label),
show: !includesHideSeries(label, d.group, hideSeries),
scale: String(d.group)
};
};
export const getLegendItem = (s: Series, group: number): LegendItem => ({
group,
label: s.label || "",
color: s.stroke as string,
checked: s.show || false
});
export const getHideSeries = ({hideSeries, legend, metaKey, series}: HideSeriesArgs): string[] => {
const label = `${legend.group}.${legend.label}`;
const include = includesHideSeries(legend.label, legend.group, hideSeries);
const labels = series.map(s => `${s.scale}.${s.label}`);
if (metaKey && include) {
return [...labels.filter(l => l !== label)];
} else if (metaKey && !include) {
return hideSeries.length >= series.length - 1 ? [] : [...labels.filter(l => l !== label)];
}
return include ? hideSeries.filter(l => l !== label) : [...hideSeries, label];
};
export const includesHideSeries = (label: string, group: string | number, hideSeries: string[]): boolean => {
return hideSeries.includes(`${group}.${label}`);
};

View File

@ -0,0 +1,30 @@
import dayjs from "dayjs";
import {SetupTooltip} from "./types";
import {getColorLine} from "./helpers";
export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffset}: SetupTooltip): void => {
const {seriesIdx, dataIdx} = tooltipIdx;
const dataSeries = u.data[seriesIdx][dataIdx];
const dataTime = u.data[0][dataIdx];
const metric = metrics[seriesIdx - 1]?.metric || {};
const color = getColorLine(Number(series[seriesIdx].scale || 0), series[seriesIdx].label || "");
const {width, height} = u.over.getBoundingClientRect();
const top = u.valToPos((dataSeries || 0), series[seriesIdx]?.scale || "1");
const lft = u.valToPos(dataTime, "x");
const {width: tooltipWidth, height: tooltipHeight} = tooltip.getBoundingClientRect();
const overflowX = lft + tooltipWidth >= width;
const overflowY = top + tooltipHeight >= height;
tooltip.style.display = "grid";
tooltip.style.top = `${tooltipOffset.top + top + 10 - (overflowY ? tooltipHeight + 10 : 0)}px`;
tooltip.style.left = `${tooltipOffset.left + lft + 10 - (overflowX ? tooltipWidth + 20 : 0)}px`;
const date = dayjs(new Date(dataTime * 1000)).format("YYYY-MM-DD HH:mm:ss:SSS (Z)");
const info = Object.keys(metric).filter(k => k !== "__name__").map(k => `<div><b>${k}</b>: ${metric[k]}</div>`).join("");
const marker = `<div class="u-tooltip__marker" style="background: ${color}"></div>`;
tooltip.innerHTML = `<div>${date}</div>
<div class="u-tooltip-data">
${marker}${metric.__name__ || ""}: <b class="u-tooltip-data__value">${dataSeries}</b>
</div>
<div class="u-tooltip__info">${info}</div>`;
};

View File

@ -0,0 +1,39 @@
import uPlot, {Series} from "uplot";
import {MetricResult} from "../../api/types";
export interface SetupTooltip {
u: uPlot,
metrics: MetricResult[],
series: Series[],
tooltip: HTMLDivElement,
tooltipOffset: {
left: number,
top: number
},
tooltipIdx: {
seriesIdx: number,
dataIdx: number
}
}
export interface HideSeriesArgs {
hideSeries: string[],
legend: LegendItem,
metaKey: boolean,
series: Series[]
}
export interface DragArgs {
e: MouseEvent,
u: uPlot,
factor: number,
setPanning: (enable: boolean) => void,
setPlotScale: ({u, min, max}: { u: uPlot, min: number, max: number }) => void
}
export interface LegendItem {
group: number;
label: string;
color: string;
checked: boolean;
}

View File

@ -1217,7 +1217,8 @@ Consider setting the following command-line flags:
* `-snapshotAuthKey` for protecting `/snapshot*` endpoints. See [how to work with snapshots](#how-to-work-with-snapshots). * `-snapshotAuthKey` for protecting `/snapshot*` endpoints. See [how to work with snapshots](#how-to-work-with-snapshots).
* `-forceMergeAuthKey` for protecting `/internal/force_merge` endpoint. See [force merge docs](#forced-merge). * `-forceMergeAuthKey` for protecting `/internal/force_merge` endpoint. See [force merge docs](#forced-merge).
* `-search.resetCacheAuthKey` for protecting `/internal/resetRollupResultCache` endpoint. See [backfilling](#backfilling) for more details. * `-search.resetCacheAuthKey` for protecting `/internal/resetRollupResultCache` endpoint. See [backfilling](#backfilling) for more details.
* `-configAuthKey` for pretecting `/config` endpoint, since it may contain sensitive information such as passwords. * `-configAuthKey` for protecting `/config` endpoint, since it may contain sensitive information such as passwords.
- `-pprofAuthKey` for protecting `/debug/pprof/*` endpoints, which can be used for [profiling](#profiling).
Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats. Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats.
For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`. For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`.

View File

@ -1221,7 +1221,8 @@ Consider setting the following command-line flags:
* `-snapshotAuthKey` for protecting `/snapshot*` endpoints. See [how to work with snapshots](#how-to-work-with-snapshots). * `-snapshotAuthKey` for protecting `/snapshot*` endpoints. See [how to work with snapshots](#how-to-work-with-snapshots).
* `-forceMergeAuthKey` for protecting `/internal/force_merge` endpoint. See [force merge docs](#forced-merge). * `-forceMergeAuthKey` for protecting `/internal/force_merge` endpoint. See [force merge docs](#forced-merge).
* `-search.resetCacheAuthKey` for protecting `/internal/resetRollupResultCache` endpoint. See [backfilling](#backfilling) for more details. * `-search.resetCacheAuthKey` for protecting `/internal/resetRollupResultCache` endpoint. See [backfilling](#backfilling) for more details.
* `-configAuthKey` for pretecting `/config` endpoint, since it may contain sensitive information such as passwords. * `-configAuthKey` for protecting `/config` endpoint, since it may contain sensitive information such as passwords.
- `-pprofAuthKey` for protecting `/debug/pprof/*` endpoints, which can be used for [profiling](#profiling).
Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats. Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats.
For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`. For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`.

View File

@ -84,8 +84,12 @@ func Serve(addr string, rh RequestHandler) {
if *tlsEnable { if *tlsEnable {
scheme = "https" scheme = "https"
} }
logger.Infof("starting http server at %s://%s/", scheme, addr) hostAddr := addr
logger.Infof("pprof handlers are exposed at %s://%s/debug/pprof/", scheme, addr) if strings.HasPrefix(hostAddr, ":") {
hostAddr = "127.0.0.1" + hostAddr
}
logger.Infof("starting http server at %s://%s/", scheme, hostAddr)
logger.Infof("pprof handlers are exposed at %s://%s/debug/pprof/", scheme, hostAddr)
lnTmp, err := netutil.NewTCPListener(scheme, addr) lnTmp, err := netutil.NewTCPListener(scheme, addr)
if err != nil { if err != nil {
logger.Fatalf("cannot start http server at %s: %s", addr, err) logger.Fatalf("cannot start http server at %s: %s", addr, err)