diff --git a/plist b/plist index 0fe694e2c..32862cd9a 100644 --- a/plist +++ b/plist @@ -2054,6 +2054,8 @@ /usr/local/opnsense/www/js/chartjs-plugin-colorschemes.min.js /usr/local/opnsense/www/js/chartjs-plugin-matrix.min.js /usr/local/opnsense/www/js/chartjs-plugin-streaming.js +/usr/local/opnsense/www/js/chartjs-plugin-zoom.min.js +/usr/local/opnsense/www/js/chartjs-scale-timestack.min.js /usr/local/opnsense/www/js/d3.min.js /usr/local/opnsense/www/js/d3.min.js.LICENSE /usr/local/opnsense/www/js/gridstack-all.min.js @@ -2063,12 +2065,14 @@ /usr/local/opnsense/www/js/jquery.bootgrid.js /usr/local/opnsense/www/js/jquery.qrcode.js /usr/local/opnsense/www/js/jquery.qrcode.js.LICENSE +/usr/local/opnsense/www/js/luxon.min.js /usr/local/opnsense/www/js/moment-with-locales.min.js /usr/local/opnsense/www/js/nv.d3.min.js /usr/local/opnsense/www/js/nv.d3.min.js.LICENSE.md /usr/local/opnsense/www/js/opnsense-treeview.js /usr/local/opnsense/www/js/opnsense.js /usr/local/opnsense/www/js/opnsense_bootgrid_plugin.js +/usr/local/opnsense/www/js/opnsense_health.js /usr/local/opnsense/www/js/opnsense_status.js /usr/local/opnsense/www/js/opnsense_theme.js /usr/local/opnsense/www/js/opnsense_ui.js diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/SystemhealthController.php b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/SystemhealthController.php index b33a73a4b..87be1214f 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/SystemhealthController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/SystemhealthController.php @@ -114,6 +114,9 @@ class SystemhealthController extends ApiControllerBase /** * retrieve SystemHealth Data (previously called RRD Graphs) + * + * XXX: $inverse to be removed for 25.7 + * * @param string $rrd * @param bool $inverse * @param int $detail @@ -131,18 +134,10 @@ class SystemhealthController extends ApiControllerBase $response['set']['count'] = count($records); $response['set']['step_size'] = $response['sets'][$detail]['step_size']; foreach ($records as $key => $record) { - $record['area'] = true; - if ($inverse && $key % 2 != 0) { - foreach ($record['values'] as &$value) { - $value[1] = $value[1] * -1; - } - } $response['set']['data'][] = $record; } } - for ($i = 0; $i < count($response['sets']); $i++) { - unset($response['sets'][$detail]['ds']); - } + unset($response['sets']); if (!empty($rrd_details["title"])) { $response['title'] = $rrd_details["title"] . " | " . ucfirst($rrd_details['itemName']); } else { @@ -151,7 +146,7 @@ class SystemhealthController extends ApiControllerBase $response['y-axis_label'] = $rrd_details["y-axis_label"]; return $response; } else { - return ["sets" => [], "set" => [], "title" => "error", "y-axis_label" => ""]; + return ["set" => [], "title" => "error", "y-axis_label" => ""]; } } @@ -171,4 +166,31 @@ class SystemhealthController extends ApiControllerBase } return $intfmap; } + + public function exportAsCSVAction($rrd = "", $detail = -1) + { + $data = $this->getSystemHealthAction($rrd, 0, $detail); + if (empty($data['set']['data'])) { + return; + } + + $parsed = []; + $numKeys = count($data['set']['data']); + $length = count($data['set']['data'][0]['values']); + + for ($i = 0; $i < $length; $i++) { + $timestamp = $data['set']['data'][0]['values'][$i][0] / 1000; + $part = [ + "iso_time" => date("c", $timestamp) + ]; + $values = []; + for ($j = 0; $j < $numKeys; $j++) { + $values[$data['set']['data'][$j]['key']] = $data['set']['data'][$j]['values'][$i][1]; + } + + $parsed[] = array_merge($part, $values); + } + + $this->exportCsv($parsed); + } } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/SystemhealthController.php b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/SystemhealthController.php index 93ce6a58d..a93451186 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/SystemhealthController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/SystemhealthController.php @@ -2,6 +2,7 @@ /* * Copyright (C) 2015 Jos Schellevis + * Copyright (C) 2025 Deciso B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/opnsense/mvc/app/views/OPNsense/Diagnostics/health.volt b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/health.volt index 68678d3f6..f46febaeb 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Diagnostics/health.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/health.volt @@ -1,623 +1,187 @@ - +{# - +OPNsense® is Copyright © 2025 by Deciso B.V. +All rights reserved. - - +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: - - +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. - - +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. - - +#} + + + + + + + + -
-
-
-
-

- {{ lang._('Information') }} -

-
-
- {{ lang._('Local data collection is not enabled at the moment') }} - {{ lang._('Go to reporting settings') }} -
-
-
-
-
-
- - {{ lang._('Options') }} -
-
-
- -
-
-
- {{ lang._('Granularity') }}: -
- -
-
-
- {{ lang._('Inverse') }}: -
- - -
-
-
-
-
- {{ lang._('Show Tables') }}: -
- - -
-
-
-
-
+ + +
+ + +
+ + +
+ + +
+
+ +
- -
-
-

- - - {{ lang._('Please wait while loading data...') }} - - - - - ({{ lang._('Current detail is showing') }} {{ lang._('averages') }}.) - - -

-
-
-
- -
-
+
+ +
- - +
- +
diff --git a/src/opnsense/www/js/chartjs-plugin-zoom.min.js b/src/opnsense/www/js/chartjs-plugin-zoom.min.js new file mode 100644 index 000000000..0c7b6b45a --- /dev/null +++ b/src/opnsense/www/js/chartjs-plugin-zoom.min.js @@ -0,0 +1,7 @@ +/*! +* chartjs-plugin-zoom v2.2.0 +* https://www.chartjs.org/chartjs-plugin-zoom/2.2.0/ + * (c) 2016-2024 chartjs-plugin-zoom Contributors + * Released under the MIT License + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n(require("chart.js"),require("hammerjs"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","hammerjs","chart.js/helpers"],n):(t="undefined"!=typeof globalThis?globalThis:t||self).ChartZoom=n(t.Chart,t.Hammer,t.Chart.helpers)}(this,(function(t,n,e){"use strict";const o=t=>t&&t.enabled&&t.modifierKey,a=(t,n)=>t&&n[t+"Key"],i=(t,n)=>t&&!n[t+"Key"];function r(t,n,e){return void 0===t||("string"==typeof t?-1!==t.indexOf(n):"function"==typeof t&&-1!==t({chart:e}).indexOf(n))}function c(t,n){return"function"==typeof t&&(t=t({chart:n})),"string"==typeof t?{x:-1!==t.indexOf("x"),y:-1!==t.indexOf("y")}:{x:!1,y:!1}}function s(t,n,o){const{mode:a="xy",scaleMode:i,overScaleMode:r}=t||{},s=function({x:t,y:n},e){const o=e.scales,a=Object.keys(o);for(let e=0;e=i.top&&n<=i.bottom&&t>=i.left&&t<=i.right)return i}return null}(n,o),l=c(a,o),m=c(i,o);if(r){const t=c(r,o);for(const n of["x","y"])t[n]&&(m[n]=l[n],l[n]=!1)}if(s&&m[s.axis])return[s];const u=[];return e.each(o.scales,(function(t){l[t.axis]&&u.push(t)})),u}const l=new WeakMap;function m(t){let n=l.get(t);return n||(n={originalScaleLimits:{},updatedScaleLimits:{},handlers:{},panDelta:{},dragging:!1,panning:!1},l.set(t,n)),n}function u(t,n,e,o){const a=Math.max(0,Math.min(1,(t-n)/e||0));return{min:o*a,max:o*(1-a)}}function d(t,n){const e=t.isHorizontal()?n.x:n.y;return t.getValueForPixel(e)}function f(t,n,e){const o=t.max-t.min,a=o*(n-1);return u(d(t,e),t.min,o,a)}function p(t,n,o,a,i){let r=o[a];if("original"===r){const o=t.originalScaleLimits[n.id][a];r=e.valueOrDefault(o.options,o.scale)}return e.valueOrDefault(r,i)}function h(t,{min:n,max:o},a,i=!1){const r=m(t.chart),{options:c}=t,s=function(t,n){return n&&(n[t.id]||n[t.axis])||{}}(t,a),{minRange:l=0}=s,u=p(r,t,s,"min",-1/0),d=p(r,t,s,"max",1/0);if("pan"===i&&(nd))return!0;const f=t.max-t.min,h=i?Math.max(o-n,l):f;if(i&&h===l&&f<=l)return!0;const g=function(t,{min:n,max:o,minLimit:a,maxLimit:i},r){const c=(t-o+n)/2;n-=c,o+=c;const s=r.min.options??r.min.scale,l=r.max.options??r.max.scale,m=t/1e6;return e.almostEquals(n,s,m)&&(n=s),e.almostEquals(o,l,m)&&(o=l),ni&&(o=i,n=Math.max(i-t,a)),{min:n,max:o}}(h,{min:n,max:o,minLimit:u,maxLimit:d},r.originalScaleLimits[t.id]);return c.min=g.min,c.max=g.max,r.updatedScaleLimits[t.id]=g,t.parse(g.min)!==t.min||t.parse(g.max)!==t.max}const g=t=>0===t||isNaN(t)?0:t<0?Math.min(Math.round(t),-1):Math.max(Math.round(t),1);const x={second:500,minute:3e4,hour:18e5,day:432e5,week:3024e5,month:1296e6,quarter:5184e6,year:157248e5};function b(t,n,e,o=!1){const{min:a,max:i,options:r}=t,c=r.time&&r.time.round,s=x[c]||0,l=t.getValueForPixel(t.getPixelForValue(a+s)-n),m=t.getValueForPixel(t.getPixelForValue(i+s)-n);return!(!isNaN(l)&&!isNaN(m))||h(t,{min:l,max:m},e,!!o&&"pan")}function y(t,n,e){return b(t,n,e,!0)}const v={category:function(t,n,e,o){const a=f(t,n,e);return t.min===t.max&&n<1&&function(t){const n=t.getLabels().length-1;t.min>0&&(t.min-=1),t.maxc&&(a=Math.max(0,a-s),i=1===r?a:a+r,l=0===a),h(t,{min:a,max:i},e)||l},default:b,logarithmic:y,timeseries:y};function M(t,n){e.each(t,((e,o)=>{n[o]||delete t[o]}))}function k(t,n){const{scales:o}=t,{originalScaleLimits:a,updatedScaleLimits:i}=n;return e.each(o,(function(t){(function(t,n,e){const{id:o,options:{min:a,max:i}}=t;if(!n[o]||!e[o])return!0;const r=e[o];return r.min!==a||r.max!==i})(t,a,i)&&(a[t.id]={min:{scale:t.min,options:t.options.min},max:{scale:t.max,options:t.options.max}})})),M(a,o),M(i,o),a}function S(t,n,o,a){const i=v[t.type]||v.default;e.callback(i,[t,n,o,a])}function P(t,n,o,a){const i=w[t.type]||w.default;e.callback(i,[t,n,o,a])}function D(t){const n=t.chartArea;return{x:(n.left+n.right)/2,y:(n.top+n.bottom)/2}}function C(t,n,o="none",a="api"){const{x:i=1,y:r=1,focalPoint:c=D(t)}="number"==typeof n?{x:n,y:n}:n,l=m(t),{options:{limits:u,zoom:d}}=l;k(t,l);const f=1!==i,p=1!==r,h=s(d,c,t);e.each(h||t.scales,(function(t){t.isHorizontal()&&f?S(t,i,c,u):!t.isHorizontal()&&p&&S(t,r,c,u)})),t.update(o),e.callback(d.onZoom,[{chart:t,trigger:a}])}function Z(t,n,o,a="none",i="api"){const c=m(t),{options:{limits:s,zoom:l}}=c,{mode:u="xy"}=l;k(t,c);const d=r(u,"x",t),f=r(u,"y",t);e.each(t.scales,(function(t){t.isHorizontal()&&d?P(t,n.x,o.x,s):!t.isHorizontal()&&f&&P(t,n.y,o.y,s)})),t.update(a),e.callback(l.onZoom,[{chart:t,trigger:i}])}function j(t){const n=m(t);let o=1,a=1;return e.each(t.scales,(function(t){const i=function(t,n){const o=t.originalScaleLimits[n];if(!o)return;const{min:a,max:i}=o;return e.valueOrDefault(i.options,i.scale)-e.valueOrDefault(a.options,a.scale)}(n,t.id);if(i){const n=Math.round(i/(t.max-t.min)*100)/100;o=Math.min(o,n),a=Math.max(a,n)}})),o<1?o:a}function L(t,n,o,a){const{panDelta:i}=a,r=i[t.id]||0;e.sign(r)===e.sign(n)&&(n+=r);const c=z[t.type]||z.default;e.callback(c,[t,n,o])?i[t.id]=0:i[t.id]=n}function O(t,n,o,a="none"){const{x:i=0,y:r=0}="number"==typeof n?{x:n,y:n}:n,c=m(t),{options:{pan:s,limits:l}}=c,{onPan:u}=s||{};k(t,c);const d=0!==i,f=0!==r;e.each(o||t.scales,(function(t){t.isHorizontal()&&d?L(t,i,l,c):!t.isHorizontal()&&f&&L(t,r,l,c)})),t.update(a),e.callback(u,[{chart:t}])}function R(t){const n=m(t);k(t,n);const e={};for(const o of Object.keys(t.scales)){const{min:t,max:a}=n.originalScaleLimits[o]||{min:{},max:{}};e[o]={min:t.scale,max:a.scale}}return e}function E(t){const n=m(t);return n.panning||n.dragging}const F=(t,n,e)=>Math.min(e,Math.max(n,t));function N(t,n){const{handlers:e}=m(t),o=e[n];o&&o.target&&(o.target.removeEventListener(n,o),delete e[n])}function A(t,n,e,o){const{handlers:a,options:i}=m(t),r=a[e];if(r&&r.target===n)return;N(t,e),a[e]=n=>o(t,n,i),a[e].target=n;const c="wheel"!==e&&void 0;n.addEventListener(e,a[e],{passive:c})}function H(t,n){const e=m(t);e.dragStart&&(e.dragging=!0,e.dragEnd=n,t.update("none"))}function T(t,n){const e=m(t);e.dragStart&&"Escape"===n.key&&(N(t,"keydown"),e.dragging=!1,e.dragStart=e.dragEnd=null,t.update("none"))}function Y(t,n){if(t.target!==n.canvas){const e=n.canvas.getBoundingClientRect();return{x:t.clientX-e.left,y:t.clientY-e.top}}return e.getRelativePosition(t,n)}function q(t,n,o){const{onZoomStart:a,onZoomRejected:i}=o;if(a){const o=Y(n,t);if(!1===e.callback(a,[{chart:t,event:n,point:o}]))return e.callback(i,[{chart:t,event:n}]),!1}}function V(t,n){if(t.legend){const o=e.getRelativePosition(n,t);if(e._isPointInArea(o,t.legend))return}const r=m(t),{pan:c,zoom:s={}}=r.options;if(0!==n.button||a(o(c),n)||i(o(s.drag),n))return e.callback(s.onZoomRejected,[{chart:t,event:n}]);!1!==q(t,n,s)&&(r.dragStart=n,A(t,t.canvas.ownerDocument,"mousemove",H),A(t,window.document,"keydown",T))}function X(t,n,e,{min:o,max:a,prop:i}){t[o]=F(Math.min(e.begin[i],e.end[i]),n[o],n[a]),t[a]=F(Math.max(e.begin[i],e.end[i]),n[o],n[a])}function B(t,n,e){const o={begin:Y(n.dragStart,t),end:Y(n.dragEnd,t)};if(e){!function({begin:t,end:n},e){let o=n.x-t.x,a=n.y-t.y;const i=Math.abs(o/a);i>e?o=Math.sign(o)*Math.abs(a*e):i=0?2-1/(1-s):1+s;C(t,{x:l,y:l,focalPoint:{x:n.clientX-c.left,y:n.clientY-c.top}},"zoom","wheel"),e.callback(a,[{chart:t}])}function U(t,n,o,a){o&&(m(t).handlers[n]=function(t,n){let e;return function(){return clearTimeout(e),e=setTimeout(t,n),n}}((()=>e.callback(o,[{chart:t}])),a))}function _(t,n){return function(r,c){const{pan:s,zoom:l={}}=n.options;if(!s||!s.enabled)return!1;const m=c&&c.srcEvent;return!m||(!(!n.panning&&"mouse"===c.pointerType&&(i(o(s),m)||a(o(l.drag),m)))||(e.callback(s.onPanRejected,[{chart:t,event:c}]),!1))}}function G(t,n,e){if(n.scale){const{center:o,pointers:a}=e,i=1/n.scale*e.scale,c=e.target.getBoundingClientRect(),s=function(t,n){const e=Math.abs(t.clientX-n.clientX),o=Math.abs(t.clientY-n.clientY),a=e/o;let i,r;return a>.3&&a<1.7?i=r=!0:e>o?i=!0:r=!0,{x:i,y:r}}(a[0],a[1]),l=n.options.zoom.mode;C(t,{x:s.x&&r(l,"x",t)?i:1,y:s.y&&r(l,"y",t)?i:1,focalPoint:{x:o.x-c.left,y:o.y-c.top}},"zoom","pinch"),n.scale=e.scale}}function J(t,n,e){const o=n.delta;o&&(n.panning=!0,O(t,{x:e.deltaX-o.x,y:e.deltaY-o.y},n.panScales),n.delta={x:e.deltaX,y:e.deltaY})}const Q=new WeakMap;function $(t,o){const a=m(t),i=t.canvas,{pan:r,zoom:c}=o,l=new n.Manager(i);c&&c.pinch.enabled&&(l.add(new n.Pinch),l.on("pinchstart",(n=>function(t,n,o){if(n.options.zoom.pinch.enabled){const a=e.getRelativePosition(o,t);!1===e.callback(n.options.zoom.onZoomStart,[{chart:t,event:o,point:a}])?(n.scale=null,e.callback(n.options.zoom.onZoomRejected,[{chart:t,event:o}])):n.scale=1}}(t,a,n))),l.on("pinch",(n=>G(t,a,n))),l.on("pinchend",(n=>function(t,n,o){n.scale&&(G(t,n,o),n.scale=null,e.callback(n.options.zoom.onZoomComplete,[{chart:t}]))}(t,a,n)))),r&&r.enabled&&(l.add(new n.Pan({threshold:r.threshold,enable:_(t,a)})),l.on("panstart",(n=>function(t,n,o){const{enabled:a,onPanStart:i,onPanRejected:r}=n.options.pan;if(!a)return;const c=o.target.getBoundingClientRect(),l={x:o.center.x-c.left,y:o.center.y-c.top};if(!1===e.callback(i,[{chart:t,event:o,point:l}]))return e.callback(r,[{chart:t,event:o}]);n.panScales=s(n.options.pan,l,t),n.delta={x:0,y:0},J(t,n,o)}(t,a,n))),l.on("panmove",(n=>J(t,a,n))),l.on("panend",(()=>function(t,n){n.delta=null,n.panning&&(n.panning=!1,n.filterNextClick=!0,e.callback(n.options.pan.onPanComplete,[{chart:t}]))}(t,a)))),Q.set(t,l)}function tt(t){const n=Q.get(t);n&&(n.remove("pinchstart"),n.remove("pinch"),n.remove("pinchend"),n.remove("panstart"),n.remove("pan"),n.remove("panend"),n.destroy(),Q.delete(t))}function nt(t,n,e){const o=e.zoom.drag,{dragStart:a,dragEnd:i}=m(t);if(o.drawTime!==n||!i)return;const{left:r,top:c,width:s,height:l}=K(t,e.zoom.mode,{dragStart:a,dragEnd:i},o.maintainAspectRatio),u=t.ctx;u.save(),u.beginPath(),u.fillStyle=o.backgroundColor||"rgba(225,225,225,0.3)",u.fillRect(r,c,s,l),o.borderWidth>0&&(u.lineWidth=o.borderWidth,u.strokeStyle=o.borderColor||"rgba(225,225,225)",u.strokeRect(r,c,s,l)),u.restore()}var et={id:"zoom",version:"2.2.0",defaults:{pan:{enabled:!1,mode:"xy",threshold:10,modifierKey:null},zoom:{wheel:{enabled:!1,speed:.1,modifierKey:null},drag:{enabled:!1,drawTime:"beforeDatasetsDraw",modifierKey:null},pinch:{enabled:!1},mode:"xy"}},start:function(t,o,a){m(t).options=a,Object.prototype.hasOwnProperty.call(a.zoom,"enabled")&&console.warn("The option `zoom.enabled` is no longer supported. Please use `zoom.wheel.enabled`, `zoom.drag.enabled`, or `zoom.pinch.enabled`."),(Object.prototype.hasOwnProperty.call(a.zoom,"overScaleMode")||Object.prototype.hasOwnProperty.call(a.pan,"overScaleMode"))&&console.warn("The option `overScaleMode` is deprecated. Please use `scaleMode` instead (and update `mode` as desired)."),n&&$(t,a),t.pan=(n,e,o)=>O(t,n,e,o),t.zoom=(n,e)=>C(t,n,e),t.zoomRect=(n,e,o)=>Z(t,n,e,o),t.zoomScale=(n,o,a)=>function(t,n,o,a="none",i="api"){const r=m(t);k(t,r),h(t.scales[n],o,void 0,!0),t.update(a),e.callback(r.options.zoom?.onZoom,[{chart:t,trigger:i}])}(t,n,o,a),t.resetZoom=n=>function(t,n="default"){const o=m(t),a=k(t,o);e.each(t.scales,(function(t){const n=t.options;a[t.id]?(n.min=a[t.id].min.options,n.max=a[t.id].max.options):(delete n.min,delete n.max),delete o.updatedScaleLimits[t.id]})),t.update(n),e.callback(o.options.zoom.onZoomComplete,[{chart:t}])}(t,n),t.getZoomLevel=()=>j(t),t.getInitialScaleBounds=()=>R(t),t.getZoomedScaleBounds=()=>function(t){const n=m(t),e={};for(const o of Object.keys(t.scales))e[o]=n.updatedScaleLimits[o];return e}(t),t.isZoomedOrPanned=()=>function(t){const n=R(t);for(const e of Object.keys(t.scales)){const{min:o,max:a}=n[e];if(void 0!==o&&t.scales[e].min!==o)return!0;if(void 0!==a&&t.scales[e].max!==a)return!0}return!1}(t),t.isZoomingOrPanning=()=>E(t)},beforeEvent(t,{event:n}){if(E(t))return!1;if("click"===n.type||"mouseup"===n.type){const n=m(t);if(n.filterNextClick)return n.filterNextClick=!1,!1}},beforeUpdate:function(t,n,e){const o=m(t),a=o.options;o.options=e,function(t,n){const{pan:e,zoom:o}=t,{pan:a,zoom:i}=n;return o?.zoom?.pinch?.enabled!==i?.zoom?.pinch?.enabled||e?.enabled!==a?.enabled||e?.threshold!==a?.threshold}(a,e)&&(tt(t),$(t,e)),function(t,n){const e=t.canvas,{wheel:o,drag:a,onZoomComplete:i}=n.zoom;o.enabled?(A(t,e,"wheel",I),U(t,"onZoomComplete",i,250)):N(t,"wheel"),a.enabled?(A(t,e,"mousedown",V),A(t,e.ownerDocument,"mouseup",W)):(N(t,"mousedown"),N(t,"mousemove"),N(t,"mouseup"),N(t,"keydown"))}(t,e)},beforeDatasetsDraw(t,n,e){nt(t,"beforeDatasetsDraw",e)},afterDatasetsDraw(t,n,e){nt(t,"afterDatasetsDraw",e)},beforeDraw(t,n,e){nt(t,"beforeDraw",e)},afterDraw(t,n,e){nt(t,"afterDraw",e)},stop:function(t){!function(t){N(t,"mousedown"),N(t,"mousemove"),N(t,"mouseup"),N(t,"wheel"),N(t,"click"),N(t,"keydown")}(t),n&&tt(t),function(t){l.delete(t)}(t)},panFunctions:z,zoomFunctions:v,zoomRectFunctions:w};return t.Chart.register(et),et})); diff --git a/src/opnsense/www/js/chartjs-scale-timestack.min.js b/src/opnsense/www/js/chartjs-scale-timestack.min.js new file mode 100644 index 000000000..4999b108b --- /dev/null +++ b/src/opnsense/www/js/chartjs-scale-timestack.min.js @@ -0,0 +1 @@ +var _timestack=(()=>{var Z=Object.create;var v=Object.defineProperty;var tt=Object.getOwnPropertyDescriptor;var et=Object.getOwnPropertyNames;var it=Object.getPrototypeOf,nt=Object.prototype.hasOwnProperty;var Y=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),ot=(r,e)=>{for(var t in e)v(r,t,{get:e[t],enumerable:!0})},N=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of et(e))!nt.call(r,n)&&n!==t&&v(r,n,{get:()=>e[n],enumerable:!(i=tt(e,n))||i.enumerable});return r};var R=(r,e,t)=>(t=r!=null?Z(it(r)):{},N(e||!r||!r.__esModule?v(t,"default",{value:r,enumerable:!0}):t,r)),st=r=>N(v({},"__esModule",{value:!0}),r);var z=Y((lt,P)=>{P.exports=Chart});var E=Y((_t,$)=>{$.exports=luxon});var rt={};ot(rt,{DEF_TICK_GENERATORS:()=>G,DEF_TOOLTIP_FORMAT:()=>q,DaysTickGenerator:()=>D,HM:()=>h,HMS:()=>S,MD:()=>u,MDAY:()=>F,MON:()=>b,TickGenerator:()=>j,TimestackScale:()=>x,YEAR:()=>y,YM:()=>M,YMD:()=>f,YearsTickGenerator:()=>g});var K=R(z());var V=R(z()),U=R(E());var A=R(E());var C=R(E()),B=new Map,H=new Map;function w(r,e,t){let i=[{},0];for(let n of e){if(!n.isValid)throw"invalid datetime";let o=n.toLocaleString(t),s=r.measureText(o).width;s>i[1]&&(i=[n,s,o])}return i[0]}function at(r,e,t,i){let n=`${e.month}/${e.weekday}`,o=B.get(r);if(o)return o[n];let s={year:2024,month:12,day:22,hour:23,minute:59,second:59},m=C.DateTime.fromObject(s,i);function*l(I){for(let k=1;k<=12;k++)yield I.set({month:k})}let c=w(t,l(m),{month:"short"}),a=w(t,l(m),{month:"long"});function*d(I){for(let k=22;k<29;k++)yield I.set({day:k})}let p=w(t,d(c),{weekday:"short"}),T=w(t,d(c),{weekday:"long"}),O=w(t,d(c),{weekday:"narrow"}),Q=w(t,d(a),{weekday:"short"}),W=w(t,d(a),{weekday:"long"}),X=w(t,d(a),{weekday:"narrow"}),J=Object.fromEntries(Object.entries({"undefined/undefined":m,"undefined/short":p,"undefined/long":T,"undefined/narrow":O,"numeric/undefined":m,"numeric/short":p,"numeric/long":T,"numeric/narrow":O,"2-digit/undefined":m,"2-digit/short":p,"2-digit/long":T,"2-digit/narrow":O,"short/undefined":c,"short/short":p,"short/long":T,"short/narrow":O,"long/undefined":a,"long/short":Q,"long/long":W,"long/narrow":X}).map(([I,k])=>[I,k.toObject()]));return B.set(r,J),J[n]}function L(r,e,t){let i=JSON.stringify(r),n=`${t.locale||C.Settings.defaultLocale}/${e.font}`,o=`${n}/${i}`,s=H.get(o);if(s)return s;let m=at(n,r,e,t),c=C.DateTime.fromObject(m,t).toLocaleString(r),a=e.measureText(c).width;return H.set(o,a),a}var j=class{*seq(e){}constructor(e,t){this.top=e,this.bottom=t}estimate(e,t,i,n=!0){let o=this.top,s=this.bottom,m=L(o.fmt,t,i),l=o.maj_fmt?L(o.maj_fmt,t,i):0,c={nticks:e/o.size,label_width:Math.max(m,l)},a;if(s){let d=L(s.short_fmt,t,i),p=n&&s.long_fmt?L(s.long_fmt,t,i):0;a={nticks:e/s.size,label_width:Math.max(d,p)}}return{top:c,bottom:a}}format(e,t,i,n){let o=e.toLocaleString(t&&this.top.maj_fmt?this.top.maj_fmt:this.top.fmt);if(!i||!this.bottom)return o;let s=e.toLocaleString(n&&this.bottom.long_fmt?this.bottom.long_fmt:this.bottom.short_fmt);return[o,s]}create(e,t,i){let n=[];for(let{dt:o,is_major:s,with_bottom:m}of this.seq(e))if(!(o=t)break;n.push({value:o.toMillis(),major:s,label:this.format(o,s,m,i(o))})}return n}create_floating(e,t,i){let n=this.bottom,o=n?i(e)&&n.long_fmt?n.long_fmt:n.short_fmt:void 0,s=o?e.toLocaleString(o):"";return{value:e.toMillis(),label:["",t==="left"?"\u2026"+s:s+"\u2026"]}}patch_formats(e){var i,n;function t(o){for(let s of Object.entries(e)){let m=s[0],l=s[1];m==="hour12"&&o.hour&&(o.hour12=l),o[m]&&(o[m]=s[1])}}t(this.top.fmt),this.top.maj_fmt&&t(this.top.maj_fmt),(i=this.bottom)!=null&&i.short_fmt&&t(this.bottom.short_fmt),(n=this.bottom)!=null&&n.long_fmt&&t(this.bottom.long_fmt)}},_=class extends j{constructor(e,t){let{fmt:i,maj_fmt:n,align:o,maj_unit:s,...m}=e;super({fmt:i,maj_fmt:n,size:A.Duration.fromDurationLike(m).toMillis()},t&&{...t,size:A.Duration.fromObject({[t.unit]:1}).toMillis()}),this.step=m,this.bottom_unit=t==null?void 0:t.unit,this.align=o,this.maj_unit=s}*seq(e){let t=e.startOf(this.align);for(;;){let i=this.bottom_unit?t.startOf(this.bottom_unit).equals(t):!1,n=this.maj_unit?t.startOf(this.maj_unit).equals(t):!1;yield{dt:t,is_major:n,with_bottom:i},t=t.plus(this.step)}}},D=class extends j{constructor(e,t){super({...e,size:e.step*86400*1e3},t&&{...t,size:30*86400*1e3}),this.days=e.days,this.maj_days=e.maj_days??[1],this.bottom_days=t?t.days??[1]:[]}*seq(e){let t=e.startOf("month");for(;;){for(let i of this.days){let n=t.set({day:i}),o=this.maj_days.includes(i),s=this.bottom_days.includes(i);yield{dt:n,is_major:o,with_bottom:s}}t=t.plus({month:1})}}},g=class extends j{constructor(e,t){super({...t,size:e*365*86400*1e3}),this.by_years=e}*seq(e){let t=(e.year/this.by_years|0)*this.by_years,i=e.startOf("year").set({year:t});for(;;)yield{dt:i,is_major:!1,with_bottom:!1},i=i.plus({year:this.by_years})}};var S={hour:"numeric",minute:"numeric",second:"numeric"},h={hour:"numeric",minute:"numeric"},F={day:"numeric"},b={month:"short"},y={year:"numeric"},f={year:"numeric",month:"short",day:"numeric"},M={year:"numeric",month:"short"},u={month:"short",day:"numeric"},G=[new _({second:1,align:"second",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:5,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:10,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:30,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:1,align:"minute",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:5,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:10,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:15,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:30,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:1,align:"hour",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:3,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:6,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:12,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({day:1,align:"day",maj_unit:"month",fmt:F},{unit:"month",short_fmt:b,long_fmt:M}),new D({days:[1,5,10,15,20,25],step:5,fmt:F},{short_fmt:b,long_fmt:M}),new D({days:[1,10,20],step:10,fmt:F},{short_fmt:b,long_fmt:M}),new D({days:[1,15],step:15,fmt:F},{short_fmt:b,long_fmt:M}),new _({month:1,align:"month",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new _({month:3,align:"year",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new _({month:6,align:"year",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new g(1,{fmt:y}),new g(5,{fmt:y}),new g(10,{fmt:y}),new g(25,{fmt:y}),new g(50,{fmt:y}),new g(100,{fmt:y}),new g(1e3,{fmt:y})];var q={year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"numeric",second:"numeric"},x=class extends V.Scale{constructor(e){var n,o,s,m,l;super(e);let t=(s=(o=(n=e.chart)==null?void 0:n.config.options)==null?void 0:o.scales)==null?void 0:s[e.id],i=(m=t==null?void 0:t.timestack)!=null&&m.make_tick_generators?(l=t==null?void 0:t.timestack)==null?void 0:l.make_tick_generators():G;if(this._gens=i,t!=null&&t.timestack.format_style)for(let c of i)c.patch_formats(t==null?void 0:t.timestack.format_style)}init(e){this._dt_opts=e.timestack.datetime??{},super.init(e)}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!1);e=isFinite(e)?e:this._dt_now().startOf("day").toMillis(),t=isFinite(t)?t:this._dt_now().endOf("day").toMillis()+1,this.min=Math.min(e,t-1),this.max=Math.max(e+1,t)}_dt_from_ts(e){return U.DateTime.fromMillis(e,this._dt_opts)}_dt_from_object(e){return U.DateTime.fromObject(e,this._dt_opts)}_dt_now(){return U.DateTime.local(this._dt_opts)}_choose_gen(e){let t=this._gens,i=this.options.timestack.max_density,n=this.options.timestack.density,o=this.options.ticks.maxTicksLimit??1/0,s=[];for(let l of t){let{top:c,bottom:a}=l.estimate(e,this.ctx,this._dt_opts),d=Math.max(c.nticks,(a==null?void 0:a.nticks)??0),p=c.nticks*c.label_width/this.width,T=a?a.nticks*a.label_width/this.width:0,O=Math.max(p,T);O<=i&&d<=o&&s.push([l,Math.abs(O-n)])}return s.length?s.reduce((l,c)=>c[1]t:!0}_need_floating_right_tick(e){let t=this.options.timestack.right_floating_tick_thres;return t===!1?!1:e.length?(this.max-e[e.length-1].value)/(this.max-this.min)>t:!0}_build_ticks(){let{min:e,max:t}=this,i=this._choose_gen(t-e);if(!i)return console.warn("Failed to choose the tick generator"),[];let n=this._dt_from_ts(e),o=this._dt_from_ts(t),s=this._dt_now(),m=a=>!a.hasSame(s,"year"),l=i.create(n,o,m);if(!i.bottom)return l;let c=l.filter(a=>Array.isArray(a.label)&&a.label.length>1);if(this._need_floating_left_tick(c)){let a;if(a=i.create_floating(n,"left",m),c.length){let d=c[0],p=this.ctx.measureText(a.label[1]).width,T=(d.value-this.min)*this.width/(this.max-this.min);p*2>T&&(a=void 0)}a&&l.unshift(a)}if(this._need_floating_right_tick(c)){let a;if(a=i.create_floating(o,"right",m),c.length){let d=c[c.length-1],p=this.ctx.measureText(a.label[1]).width,T=(this.max-d.value)*this.width/(this.max-this.min);p*2>T&&(a=void 0)}a&&l.push(a)}return l}buildTicks(){let e;try{e=this._resolveTickFontOptions(0).string}catch{console.warn("failed to resolve the font")}this.ctx.save(),e&&(this.ctx.font=e);let t=this._build_ticks();return this.ctx.restore(),t}getLabelForValue(e){return this._dt_from_ts(e).toLocaleString(this.options.timestack.tooltip_format)}generateTickLabels(e){}getPixelForValue(e){let t=e===null?NaN:(e-this.min)/(this.max-this.min);return this.getPixelForDecimal(t)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return this.min+t*(this.max-this.min)}};x.id="timestack",x.defaults={timestack:{tooltip_format:q,density:.5,max_density:.75,left_floating_tick_thres:.33,right_floating_tick_thres:!1},ticks:{source:"",maxRotation:0,autoSkip:!1}};K.Chart.register(x);return st(rt);})(); diff --git a/src/opnsense/www/js/luxon.min.js b/src/opnsense/www/js/luxon.min.js new file mode 100644 index 000000000..231f9d89e --- /dev/null +++ b/src/opnsense/www/js/luxon.min.js @@ -0,0 +1 @@ +var luxon=function(e){"use strict";function L(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[n++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var t=function(e){function t(){return e.apply(this,arguments)||this}return o(t,e),t}(q(Error)),Y=function(t){function e(e){return t.call(this,"Invalid DateTime: "+e.toMessage())||this}return o(e,t),e}(t),P=function(t){function e(e){return t.call(this,"Invalid Interval: "+e.toMessage())||this}return o(e,t),e}(t),H=function(t){function e(e){return t.call(this,"Invalid Duration: "+e.toMessage())||this}return o(e,t),e}(t),w=function(e){function t(){return e.apply(this,arguments)||this}return o(t,e),t}(t),J=function(t){function e(e){return t.call(this,"Invalid unit "+e)||this}return o(e,t),e}(t),u=function(e){function t(){return e.apply(this,arguments)||this}return o(t,e),t}(t),n=function(e){function t(){return e.call(this,"Zone is an abstract class")||this}return o(t,e),t}(t),t="numeric",r="short",a="long",G={year:t,month:t,day:t},$={year:t,month:r,day:t},B={year:t,month:r,day:t,weekday:r},Q={year:t,month:a,day:t},K={year:t,month:a,day:t,weekday:a},X={hour:t,minute:t},ee={hour:t,minute:t,second:t},te={hour:t,minute:t,second:t,timeZoneName:r},ne={hour:t,minute:t,second:t,timeZoneName:a},re={hour:t,minute:t,hourCycle:"h23"},ie={hour:t,minute:t,second:t,hourCycle:"h23"},oe={hour:t,minute:t,second:t,hourCycle:"h23",timeZoneName:r},ae={hour:t,minute:t,second:t,hourCycle:"h23",timeZoneName:a},ue={year:t,month:t,day:t,hour:t,minute:t},se={year:t,month:t,day:t,hour:t,minute:t,second:t},le={year:t,month:r,day:t,hour:t,minute:t},ce={year:t,month:r,day:t,hour:t,minute:t,second:t},fe={year:t,month:r,day:t,weekday:r,hour:t,minute:t},de={year:t,month:a,day:t,hour:t,minute:t,timeZoneName:r},he={year:t,month:a,day:t,hour:t,minute:t,second:t,timeZoneName:r},me={year:t,month:a,day:t,weekday:a,hour:t,minute:t,timeZoneName:a},ye={year:t,month:a,day:t,weekday:a,hour:t,minute:t,second:t,timeZoneName:a},s=function(){function e(){}var t=e.prototype;return t.offsetName=function(e,t){throw new n},t.formatOffset=function(e,t){throw new n},t.offset=function(e){throw new n},t.equals=function(e){throw new n},i(e,[{key:"type",get:function(){throw new n}},{key:"name",get:function(){throw new n}},{key:"ianaName",get:function(){return this.name}},{key:"isUniversal",get:function(){throw new n}},{key:"isValid",get:function(){throw new n}}]),e}(),ve=null,ge=function(e){function t(){return e.apply(this,arguments)||this}o(t,e);var n=t.prototype;return n.offsetName=function(e,t){return gt(e,t.format,t.locale)},n.formatOffset=function(e,t){return bt(this.offset(e),t)},n.offset=function(e){return-new Date(e).getTimezoneOffset()},n.equals=function(e){return"system"===e.type},i(t,[{key:"type",get:function(){return"system"}},{key:"name",get:function(){return(new Intl.DateTimeFormat).resolvedOptions().timeZone}},{key:"isUniversal",get:function(){return!1}},{key:"isValid",get:function(){return!0}}],[{key:"instance",get:function(){return ve=null===ve?new t:ve}}]),t}(s),pe={};var ke={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};var we={},f=function(n){function r(e){var t=n.call(this)||this;return t.zoneName=e,t.valid=r.isValidZone(e),t}o(r,n),r.create=function(e){return we[e]||(we[e]=new r(e)),we[e]},r.resetCache=function(){we={},pe={}},r.isValidSpecifier=function(e){return this.isValidZone(e)},r.isValidZone=function(e){if(!e)return!1;try{return new Intl.DateTimeFormat("en-US",{timeZone:e}).format(),!0}catch(e){return!1}};var e=r.prototype;return e.offsetName=function(e,t){return gt(e,t.format,t.locale,this.name)},e.formatOffset=function(e,t){return bt(this.offset(e),t)},e.offset=function(e){var t,n,r,i,o,a,u,s,e=new Date(e);return isNaN(e)?NaN:(i=this.name,pe[i]||(pe[i]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:i,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),a=(i=(i=pe[i]).formatToParts?function(e,t){for(var n=e.formatToParts(t),r=[],i=0;iyt(i,t,n)?(r=i+1,a=1):r=i,l({weekYear:r,weekNumber:a,weekday:o},St(e))}function Ke(e,t,n){void 0===n&&(n=1);var r,i=e.weekYear,o=e.weekNumber,a=e.weekday,n=Be(Je(i,1,t=void 0===t?4:t),n),u=M(i),o=7*o+a-n-7+t,a=(o<1?o+=M(r=i-1):uO.twoDigitCutoffYear?1900+e:2e3+e}function gt(e,t,n,r){void 0===r&&(r=null);var e=new Date(e),i={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"},r=(r&&(i.timeZone=r),l({timeZoneName:t},i)),t=new Intl.DateTimeFormat(n,r).formatToParts(e).find(function(e){return"timezonename"===e.type.toLowerCase()});return t?t.value:null}function pt(e,t){e=parseInt(e,10),Number.isNaN(e)&&(e=0),t=parseInt(t,10)||0;return 60*e+(e<0||Object.is(e,-0)?-t:t)}function kt(e){var t=Number(e);if("boolean"==typeof e||""===e||Number.isNaN(t))throw new u("Invalid unit value "+e);return t}function wt(e,t){var n,r,i={};for(n in e)h(e,n)&&null!=(r=e[n])&&(i[t(n)]=kt(r));return i}function bt(e,t){var n=Math.trunc(Math.abs(e/60)),r=Math.trunc(Math.abs(e%60)),i=0<=e?"+":"-";switch(t){case"short":return i+m(n,2)+":"+m(r,2);case"narrow":return i+n+(0e},t.isBefore=function(e){return!!this.isValid&&this.e<=e},t.contains=function(e){return!!this.isValid&&this.s<=e&&this.e>e},t.set=function(e){var e=void 0===e?{}:e,t=e.start,e=e.end;return this.isValid?l.fromDateTimes(t||this.s,e||this.e):this},t.splitAt=function(){var t=this;if(!this.isValid)return[];for(var e=arguments.length,n=new Array(e),r=0;r+this.e?this.e:s;o.push(l.fromDateTimes(a,s)),a=s,u+=1}return o},t.splitBy=function(e){var t=E.fromDurationLike(e);if(!this.isValid||!t.isValid||0===t.as("milliseconds"))return[];for(var n=this.s,r=1,i=[];n+this.e?this.e:o;i.push(l.fromDateTimes(n,o)),n=o,r+=1}return i},t.divideEqually=function(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]},t.overlaps=function(e){return this.e>e.s&&this.s=e.e},t.equals=function(e){return!(!this.isValid||!e.isValid)&&this.s.equals(e.s)&&this.e.equals(e.e)},t.intersection=function(e){var t;return this.isValid?(t=(this.s>e.s?this:e).s,(e=(this.ee.e?this:e).e,l.fromDateTimes(t,e)):this},l.merge=function(e){var e=e.sort(function(e,t){return e.s-t.s}).reduce(function(e,t){var n=e[0],e=e[1];return e?e.overlaps(t)||e.abutsStart(t)?[n,e.union(t)]:[n.concat([e]),t]:[n,t]},[[],null]),t=e[0],e=e[1];return e&&t.push(e),t},l.xor=function(e){for(var t,n=null,r=0,i=[],e=e.map(function(e){return[{time:e.s,type:"s"},{time:e.e,type:"e"}]}),o=R((t=Array.prototype).concat.apply(t,e).sort(function(e,t){return e.time-t.time}));!(a=o()).done;)var a=a.value,n=1===(r+="s"===a.type?1:-1)?a.time:(n&&+n!=+a.time&&i.push(l.fromDateTimes(n,a.time)),null);return l.merge(i)},t.difference=function(){for(var t=this,e=arguments.length,n=new Array(e),r=0;rthis.valueOf())?this:e,r?e:this,t,n),r?e.negate():e):E.invalid("created by diffing an invalid DateTime")},t.diffNow=function(e,t){return void 0===e&&(e="milliseconds"),void 0===t&&(t={}),this.diff(k.now(),e,t)},t.until=function(e){return this.isValid?Mn.fromDateTimes(this,e):this},t.hasSame=function(e,t,n){var r;return!!this.isValid&&(r=e.valueOf(),(e=this.setZone(e.zone,{keepLocalTime:!0})).startOf(t,n)<=r)&&r<=e.endOf(t,n)},t.equals=function(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)},t.toRelative=function(e){var t,n,r,i;return this.isValid?(t=(e=void 0===e?{}:e).base||k.fromObject({},{zone:this.zone}),n=e.padding?thisthis.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset)}},{key:"isInLeapYear",get:function(){return ft(this.year)}},{key:"daysInMonth",get:function(){return dt(this.year,this.month)}},{key:"daysInYear",get:function(){return this.isValid?M(this.year):NaN}},{key:"weeksInWeekYear",get:function(){return this.isValid?yt(this.weekYear):NaN}},{key:"weeksInLocalWeekYear",get:function(){return this.isValid?yt(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}}],[{key:"DATE_SHORT",get:function(){return G}},{key:"DATE_MED",get:function(){return $}},{key:"DATE_MED_WITH_WEEKDAY",get:function(){return B}},{key:"DATE_FULL",get:function(){return Q}},{key:"DATE_HUGE",get:function(){return K}},{key:"TIME_SIMPLE",get:function(){return X}},{key:"TIME_WITH_SECONDS",get:function(){return ee}},{key:"TIME_WITH_SHORT_OFFSET",get:function(){return te}},{key:"TIME_WITH_LONG_OFFSET",get:function(){return ne}},{key:"TIME_24_SIMPLE",get:function(){return re}},{key:"TIME_24_WITH_SECONDS",get:function(){return ie}},{key:"TIME_24_WITH_SHORT_OFFSET",get:function(){return oe}},{key:"TIME_24_WITH_LONG_OFFSET",get:function(){return ae}},{key:"DATETIME_SHORT",get:function(){return ue}},{key:"DATETIME_SHORT_WITH_SECONDS",get:function(){return se}},{key:"DATETIME_MED",get:function(){return le}},{key:"DATETIME_MED_WITH_SECONDS",get:function(){return ce}},{key:"DATETIME_MED_WITH_WEEKDAY",get:function(){return fe}},{key:"DATETIME_FULL",get:function(){return de}},{key:"DATETIME_FULL_WITH_SECONDS",get:function(){return he}},{key:"DATETIME_HUGE",get:function(){return me}},{key:"DATETIME_HUGE_WITH_SECONDS",get:function(){return ye}}]),k}(Symbol.for("nodejs.util.inspect.custom"));function yr(e){if(W.isDateTime(e))return e;if(e&&e.valueOf&&y(e.valueOf()))return W.fromJSDate(e);if(e&&"object"==typeof e)return W.fromObject(e);throw new u("Unknown datetime argument: "+e+", of type "+typeof e)}return e.DateTime=W,e.Duration=E,e.FixedOffsetZone=d,e.IANAZone=f,e.Info=In,e.Interval=Mn,e.InvalidZone=Le,e.Settings=O,e.SystemZone=ge,e.VERSION="3.4.4",e.Zone=s,Object.defineProperty(e,"__esModule",{value:!0}),e}({}); \ No newline at end of file diff --git a/src/opnsense/www/js/opnsense_health.js b/src/opnsense/www/js/opnsense_health.js new file mode 100644 index 000000000..56e998881 --- /dev/null +++ b/src/opnsense/www/js/opnsense_health.js @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2025 Deciso B.V. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + +class HealthGraph { + constructor(canvasId, chartConfig = {}, datasetOptions = {}) { + this.ctx = $(`#${canvasId}`)[0].getContext('2d'); + this.chartConfig = { ...this._defaultChartOptions(), ...chartConfig }; + this.datasetOptions = { ...this._defaultDatasetOptions(), ...datasetOptions }; + this.chart = null; + this.rrdList = null; + + this.currentSystem = null; + this.currentDetailLevel = 0; + } + + async initialize() { + this.rrdList = await this._fetchRRDList(); + if (Object.keys(this.rrdList.data).length === 0) { + throw new Error('No RRD data available'); + } + + const firstKey = Object.keys(this.rrdList.data)[0]; + const firstValue = this.rrdList.data[firstKey]?.[0] || ""; + this.currentSystem = `${firstValue}-${firstKey}`; + + this.chart = new Chart(this.ctx, this.chartConfig); + } + + getRRDList() { + return this.rrdList; + } + + async update(system = null, detailLevel = null) { + if (system === null) + system = this.currentSystem; + else + this.currentSystem = system; + + + if (detailLevel === null) + detailLevel = this.currentDetailLevel; + else + this.currentDetailLevel = detailLevel; + + const data = await this._fetchData(); + const formatted = this._formatData(data.set); + + const stepSize = data.set.step_size; + + this.chart.data.datasets = formatted; + this.chart.options.scales.y.title.text = data['y-axis_label']; + this.chart.options.plugins.title.text = data['title']; + this.chart.options.plugins.zoom.limits.x.minRange = this._getMinRange(stepSize * 1000); + this.chart.update(); + this.chart.resetZoom(); + } + + resetZoom() { + this.chart.resetZoom(); + } + + exportData() { + window.open(`/api/diagnostics/systemhealth/exportAsCSV/${this.currentSystem}/${this.currentDetailLevel}`); + } + + async _fetchRRDList() { + const list = await fetch(`/api/diagnostics/systemhealth/getRRDlist`).then(response => response.json()); + return list; + } + + async _fetchData() { + const data = await fetch(`/api/diagnostics/systemhealth/getSystemHealth/${this.currentSystem}/0/${this.currentDetailLevel}`) + .then(response => response.json()); + return data; + } + + _getMinRange(stepSize) { + const ONE_MINUTE = 60 * 1000; + const FIVE_MINUTES = 5 * ONE_MINUTE; + const ONE_HOUR = 60 * ONE_MINUTE; + const ONE_DAY = 24 * ONE_HOUR; + + if (stepSize <= ONE_MINUTE) { + return 5 * ONE_MINUTE; + } else if (stepSize <= FIVE_MINUTES) { + return 30 * ONE_MINUTE; + } else if (stepSize <= ONE_HOUR) { + return 6 * ONE_HOUR; + } else if (stepSize <= ONE_DAY) { + return 7 * ONE_DAY; + } else { + return 30 * ONE_DAY; + } + } + + _formatData(data) { + let datasets = []; + for (const item of data.data) { + const dataset = { + ...this.datasetOptions, + label: item.key, + data: [] + }; + for (const [x, y] of item.values) { + // timestack formatter expects array of x,y objects + dataset.data.push({ x: x, y: y }); + } + datasets.push(dataset); + } + return datasets; + } + + _defaultChartOptions() { + const verticalHoverLine = { + id: 'verticalHoverLine', + beforeDatasetsDraw(chart, args, pluginOptions) { + if (chart.getDatasetMeta(0).data.length == 0) { + // data may not have loaded yet + return; + } + + const { ctx, chartArea: { top, bottom } } = chart; + ctx.save(); + + chart.getDatasetMeta(0).data.forEach((dataPoint, index) => { + if (dataPoint.active === true) { + ctx.beginPath(); + ctx.strokeStyle = 'gray'; // XXX theming concern + ctx.moveTo(dataPoint.x, top); + ctx.lineTo(dataPoint.x, bottom); + ctx.stroke(); + } + }); + } + }; + + const config = { + type: 'line', + data: {}, + options: { + normalized: true, + responsive: true, + maintainAspectRatio: false, + parsing: false, + animation: { + duration: 500, + }, + scales: { + x: { + type: 'timestack' + }, + y: { + type: 'linear', + position: 'left', + title: { + display: true, + padding: 8 + } + }, + }, + plugins: { + zoom: { + limits: { + x: { min: 'original', max: 'original', minRange: 1800 * 1000 }, + }, + pan: { + enabled: true, + mode: 'x', + modifierKey: 'ctrl', + }, + zoom: { + wheel: { + speed: 0.3, + enabled: true, + }, + drag: { + enabled: true, + }, + pinch: { + enabled: true + }, + mode: 'x', + } + }, + title: { + display: true, + position: 'top', + }, + tooltip: { + caretPadding: 15, + }, + }, + transitions: { + zoom: { + animation: { + duration: 500 + } + } + }, + interaction: { + // complementary to the vertical line on hover plugin + mode: 'index', + intersect: false + } + }, + plugins: [verticalHoverLine] + }; + + return config; + } + + _defaultDatasetOptions() { + return { + spanGaps: true, + pointRadius: 0, + pointHoverRadius: 7, + stepped: true, + pointHoverBackgroundColor: (ctx) => ctx.element.options.borderColor, + } + } + +}