Dashboard: boilerplate for new widgets (#7328)

This commit is contained in:
Stephan de Wit 2024-03-20 11:44:07 +01:00 committed by GitHub
parent 16a6dcbd4c
commit 419fec650f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 2449 additions and 3 deletions

13
plist
View File

@ -232,11 +232,13 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/VoucherController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/forms/dialogZone.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/BackupController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/FirmwareController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/MenuController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/ServiceController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/BackupController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/DashboardController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/FirmwareController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/HaltController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/IndexController.php
@ -771,6 +773,7 @@
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/index.volt
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/vouchers.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/backup_history.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/firmware.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/halt.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/license.volt
@ -1318,6 +1321,7 @@
/usr/local/opnsense/www/css/bootstrap-datepicker3.min.css
/usr/local/opnsense/www/css/bootstrap-select.css
/usr/local/opnsense/www/css/chart.css
/usr/local/opnsense/www/css/dashboard.css
/usr/local/opnsense/www/css/dns-overview.css
/usr/local/opnsense/www/css/flags/1x1/ad.svg
/usr/local/opnsense/www/css/flags/1x1/ae.svg
@ -1838,6 +1842,8 @@
/usr/local/opnsense/www/css/flags/flag-icon.css
/usr/local/opnsense/www/css/flags/flag-icon.scss
/usr/local/opnsense/www/css/font-awesome.min.css
/usr/local/opnsense/www/css/gridstack-extra.min.css
/usr/local/opnsense/www/css/gridstack.min.css
/usr/local/opnsense/www/css/jqtree.css
/usr/local/opnsense/www/css/jquery.bootgrid.css
/usr/local/opnsense/www/css/nv.d3.css
@ -1871,10 +1877,12 @@
/usr/local/opnsense/www/js/chartjs-adapter-moment.min.js
/usr/local/opnsense/www/js/chartjs-plugin-colorschemes.js
/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-streaming.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
/usr/local/opnsense/www/js/jquery-3.5.1.min.js
/usr/local/opnsense/www/js/jquery-sortable.js
/usr/local/opnsense/www/js/jquery.bootgrid.LICENSE
@ -1890,15 +1898,20 @@
/usr/local/opnsense/www/js/opnsense_status.js
/usr/local/opnsense/www/js/opnsense_theme.js
/usr/local/opnsense/www/js/opnsense_ui.js
/usr/local/opnsense/www/js/opnsense_widget_manager.js
/usr/local/opnsense/www/js/pick-a-color-1.2.3.LICENSE
/usr/local/opnsense/www/js/pick-a-color-1.2.3.js
/usr/local/opnsense/www/js/pick-a-color-1.2.3.min.js
/usr/local/opnsense/www/js/polyfills.js
/usr/local/opnsense/www/js/qrcode.js
/usr/local/opnsense/www/js/smoothie.js
/usr/local/opnsense/www/js/theme.js
/usr/local/opnsense/www/js/tinycolor-1.4.1.min.js
/usr/local/opnsense/www/js/tokenize2.js
/usr/local/opnsense/www/js/tree.jquery.min.js
/usr/local/opnsense/www/js/widgets/BaseTableWidget.js
/usr/local/opnsense/www/js/widgets/BaseWidget.js
/usr/local/opnsense/www/js/widgets/Interfaces.js
/usr/local/opnsense/www/themes/opnsense/LICENSE
/usr/local/opnsense/www/themes/opnsense/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot
/usr/local/opnsense/www/themes/opnsense/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.otf

View File

@ -0,0 +1,157 @@
<?php
/*
* Copyright (C) 2024 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.
*/
namespace OPNsense\Core\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\ACL;
use OPNsense\Core\Config;
class DashboardController extends ApiControllerBase
{
private function canAccessEndpoints($fname)
{
if (!file_exists($fname)) {
return false;
}
$handle = fopen($fname, "r");
if ($handle) {
$lines = [];
while (($line = fgets($handle)) !== false) {
if (strpos($line, "// endpoint:") === 0) {
$endpoint = explode(':', trim($line))[1] ?? null;
if (!empty($endpoint)) {
$endpoint = strstr($endpoint, ' ', true) ?: $endpoint;
$lines[] = $endpoint;
}
continue;
}
break;
}
fclose($handle);
$acl = new ACL();
foreach ($lines as $line) {
if (!$acl->isPageAccessible($this->getUserName(), $line)) {
return false;
}
}
}
return true;
}
public function getDashboardAction()
{
$this->sessionClose();
$result = [];
$dashboard = null;
$config = Config::getInstance()->object();
foreach ($config->system->user as $node) {
if ($this->getUserName() === (string)$node->name) {
$dashboard = (string)$node->dashboard;
}
}
$widgetModules = array_filter(glob('/usr/local/opnsense/www/js/widgets/*.js'),
function($element) {
$base = basename($element);
if (str_contains($base, '.js') && !str_contains($base, 'Base')) {
return $this->canAccessEndpoints($element);
}
return false;
}
);
$widgetModules = array_map(function($element) {return basename($element);}, $widgetModules);
$result['modules'] = [];
foreach ($widgetModules as $module) {
$result['modules'][] = [
'id' => strtolower(basename($module, '.js')),
'module' => basename($module)
];
}
$result['dashboard'] = !empty($dashboard) ? base64_decode($dashboard) : null;
return json_encode($result);
}
public function saveWidgetsAction()
{
$result = ['result' => 'failed'];
if ($this->request->isPost() && !empty($this->request->getRawBody())) {
$dashboard = $this->request->getRawBody();
if (strlen($dashboard) > (1024 * 1024)) {
// prevent saving large blobs of data
return json_encode($result);
}
$encoded = base64_encode($dashboard);
$config = Config::getInstance()->object();
$name = $this->getUserName();
foreach ($config->system->user as $node) {
if ($name === (string)$node->name) {
$node->dashboard = $encoded;
Config::getInstance()->save();
$result = ['result' => 'saved'];
break;
}
}
}
return json_encode($result);
}
public function restoreDefaultsAction()
{
$result = ['result' => 'failed'];
$config = Config::getInstance()->object();
$name = $this->getUserName();
foreach ($config->system->user as $node) {
if ($name === (string)$node->name) {
$node->dashboard = null;
Config::getInstance()->save();
$result = ['result' => 'saved'];
break;
}
}
return json_encode($result);
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* Copyright (C) 2023 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.
*/
namespace OPNsense\Core;
class DashboardController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/Core/dashboard');
}
}

View File

@ -53,6 +53,9 @@
<pattern>index.php*</pattern>
<pattern>widgets/widgets/*.widget.php*</pattern>
<pattern>widgets/api/get.php*</pattern>
<pattern>ui/core/dashboard</pattern>
<pattern>ui/js/widgets/*</pattern>
<pattern>api/core/dashboard/*</pattern>
<pattern>api/diagnostics/dns/reverse_lookup*</pattern>
<pattern>api/routes/gateway/status*</pattern>
<pattern>api/diagnostics/interface/getInterfaceNames</pattern>

View File

@ -5,11 +5,13 @@
<Root url="/" visibility="hidden"/>
<RootArgs url="/?*" visibility="hidden"/>
</Dashboard>
<License order="1" url="/ui/core/license" cssClass="fa fa-balance-scale fa-fw"/>
<Password order="2" url="/system_usermanager_passwordmg.php" cssClass="fa fa-key fa-fw">
<Dashboard_new order="1" VisibleName="Dashboard [new]" url="/ui/core/dashboard" cssClass="fa fa-dashboard fa-fw">
</Dashboard_new>
<License order="2" url="/ui/core/license" cssClass="fa fa-balance-scale fa-fw"/>
<Password order="3" url="/system_usermanager_passwordmg.php" cssClass="fa fa-key fa-fw">
<Edit url="/system_usermanager_passwordmg.php*" visibility="hidden"/>
</Password>
<Logout order="3" url="/index.php?logout" cssClass="fa fa-sign-out fa-fw"/>
<Logout order="4" url="/index.php?logout" cssClass="fa fa-sign-out fa-fw"/>
</Lobby>
<Reporting order="15" cssClass="fa fa-area-chart">
<Settings order="10" url="/reporting_settings.php" cssClass="fa fa-cog fa-fw"/>

View File

@ -0,0 +1,60 @@
{#
# Copyright (c) 2024 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.
#}
{% set theme_name = ui_theme|default('opnsense') %}
<!-- required for gridstack calculations -->
<link href="{{ cache_safe('/ui/css/gridstack.min.css') }}" rel="stylesheet">
<!-- required for any amount of columns < 12 -->
<link href="{{ cache_safe('/ui/css/gridstack-extra.min.css') }}" rel="stylesheet">
<!-- gridstack core -->
<script src="{{ cache_safe('/ui/js/gridstack-all.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/opnsense_widget_manager.js') }}"></script>
<link rel="stylesheet" type="text/css" href="{{ cache_safe(theme_file_or_default('/css/dashboard.css', theme_name)) }}" rel="stylesheet" />
<script src="{{ cache_safe('/ui/js/chart.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/chartjs-plugin-colorschemes.js') }}"></script>
<script>
$( document ).ready(function() {
let widgetManager = new WidgetManager({
float: false,
column: 4,
margin: 10,
alwaysShowResizeHandle: false,
sizeToContent: true,
resizable: {
handles: 'e, w'
}
}, {
'save': "{{ lang._('Save') }}",
'restore': "{{ lang._('Restore default layout') }}"
});
widgetManager.initialize();
});
</script>
<div class="grid-stack"></div>

View File

@ -0,0 +1,249 @@
.fa-circle-o-notch {
font-size: 3em;
}
.grid-stack-item-content {
text-align: center;
border-style: solid;
border-color: rgba(217, 79, 0, 0.15);
border-width: 2px;
background-color: #fbfbfb;
border-radius: 0.5em 0.5em 0.5em 0.5em;
}
.widget-content {
position: relative;
width: 100%;
height: 100%;
padding: 1px;
}
.widget-header {
margin-top: 0.5em;
margin-left: 1em;
margin-right: 1em;
display: flex;
justify-content: space-between;
align-items: center;
}
.fa-stack.small {
font-size: 0.5em;
}
/* Align fa icons to middle to adjust for icon stack */
i {
vertical-align: middle;
}
.close-handle {
vertical-align: middle;
text-align: right;
cursor: pointer;
}
.close-handle > i {
font-size: 0.7em;
}
.panel-divider {
width: 100%;
text-align: center;
height: 8px;
margin-bottom: 10px;
}
table {
table-layout: fixed;
}
td {
word-break: break-all;
}
.line {
display: inline-block;
height: 1px;
width: 70%;
background: #d94f00;
margin: 5px;
}
.canvas-container {
width: 100%;
}
.smoothie-chart-tooltip {
z-index: 1; /* necessary to force to foreground */
background: rgba(50, 50, 50, 0.9);
padding-left: 15px;
padding-right: 15px;
color: white;
font-size: 13px;
border-radius: 0.5em 0.5em 0.5em 0.5em;
pointer-events: none;
}
.flex-container {
display: flex;
flex-wrap: nowrap;
white-space: nowrap;
}
.gateway-info {
margin: 5px;
padding: 5px;
font-size: 13px;
}
.gateway-detail-container {
display: none;
margin: 5px;
}
.interface-info {
display: flex;
flex-wrap: wrap;
align-items: center;
height: 100%;
}
.nowrap {
flex-wrap: nowrap;
}
.gateway-graph {
display: none;
}
.flex-container > .gateway-graph {
font-size: 13px;
}
.vertical-center-row {
height: 100%;
display: inline;
}
.interfaces-info {
margin: 5px;
font-size: 13px;
}
.interface-descr {
margin-left: 10px;
font-size: 15px;
cursor: pointer;
text-decoration: underline;
}
.interfaces-detail-container {
margin: 5px;
display: none;
}
.d-flex {
display: flex;
}
.d-flex > .justify-content-start {
justify-content: start;
}
.d-flex > .justify-content-end {
justify-content: end;
}
#chartjs-toolip {
z-index: 20;
}
/* Gauge */
#gauge-container {
width: 80%;
margin: 5px auto;
background-color: #ccc;
height: 10px;
position: relative;
border-radius: 5px;
overflow: hidden;
}
#gauge-fill {
height: 100%;
width: 0;
background-color: green;
border-radius: 5px;
transition: width 0.5s, background-color 0.5s;
}
/* Custom flex table */
div {
box-sizing: border-box;
}
.flextable-container {
display: block;
margin: 2em auto;
width: 95%;
max-width: 1200px; /* XXX */
}
.flextable-header {
display: flex;
flex-flow: row wrap;
transition: 0.5s;
padding: 0.5em 0.5em;
border-top: solid 1px rgba(217, 79, 0, 0.15);
}
.flextable-row {
display: flex;
flex-flow: row wrap;
transition: 0.5s;
padding: 0.5em 0.5em;
border-top: solid 1px rgba(217, 79, 0, 0.15);
align-items: center
}
.flextable-header .flex-row {
font-weight: bold;
}
.flextable-row:hover {
background: #F5F5F5;
transition: 500ms;
}
.flex-row {
text-align: left;
word-break: break-word;
}
.column {
display: flex;
flex-flow: column wrap;
width: 50%;
padding: 0;
}
.column .flex-row {
display: flex;
flex-flow: row wrap;
width: 100%;
padding: 0;
border: 0;
border-top: rgba(217, 79, 0, 0.15);
}
.column .flex-row:hover {
background: #F5F5F5;
transition: 500ms;
}
.flex-cell {
width: 100%;
text-align: left;
}
.column .flex-row:not(:last-child) {
border-bottom: solid 1px rgba(217, 79, 0, 0.15);
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.grid-stack{position:relative}.grid-stack-rtl{direction:ltr}.grid-stack-rtl>.grid-stack-item{direction:rtl}.grid-stack-placeholder>.placeholder-content{background-color:rgba(0,0,0,.1);margin:0;position:absolute;width:auto;z-index:0!important}.grid-stack>.grid-stack-item{position:absolute;padding:0}.grid-stack>.grid-stack-item>.grid-stack-item-content{margin:0;position:absolute;width:auto;overflow-x:hidden;overflow-y:auto}.grid-stack>.grid-stack-item.size-to-content:not(.size-to-content-max)>.grid-stack-item-content{overflow-y:hidden}.grid-stack-item>.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.grid-stack-item.ui-resizable-autohide>.ui-resizable-handle,.grid-stack-item.ui-resizable-disabled>.ui-resizable-handle{display:none}.grid-stack-item>.ui-resizable-ne,.grid-stack-item>.ui-resizable-nw,.grid-stack-item>.ui-resizable-se,.grid-stack-item>.ui-resizable-sw{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="%23666" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 20 20"><path d="m10 3 2 2H8l2-2v14l-2-2h4l-2 2"/></svg>');background-repeat:no-repeat;background-position:center}.grid-stack-item>.ui-resizable-ne{transform:translate(0,10px) rotate(45deg)}.grid-stack-item>.ui-resizable-sw{transform:rotate(45deg)}.grid-stack-item>.ui-resizable-nw{transform:translate(0,10px) rotate(-45deg)}.grid-stack-item>.ui-resizable-se{transform:rotate(-45deg)}.grid-stack-item>.ui-resizable-nw{cursor:nw-resize;width:20px;height:20px;top:0}.grid-stack-item>.ui-resizable-n{cursor:n-resize;height:10px;top:0;left:25px;right:25px}.grid-stack-item>.ui-resizable-ne{cursor:ne-resize;width:20px;height:20px;top:0}.grid-stack-item>.ui-resizable-e{cursor:e-resize;width:10px;top:15px;bottom:15px}.grid-stack-item>.ui-resizable-se{cursor:se-resize;width:20px;height:20px}.grid-stack-item>.ui-resizable-s{cursor:s-resize;height:10px;left:25px;bottom:0;right:25px}.grid-stack-item>.ui-resizable-sw{cursor:sw-resize;width:20px;height:20px}.grid-stack-item>.ui-resizable-w{cursor:w-resize;width:10px;top:15px;bottom:15px}.grid-stack-item.ui-draggable-dragging>.ui-resizable-handle{display:none!important}.grid-stack-item.ui-draggable-dragging{will-change:left,top;cursor:move}.grid-stack-item.ui-resizable-resizing{will-change:width,height}.ui-draggable-dragging,.ui-resizable-resizing{z-index:10000}.ui-draggable-dragging>.grid-stack-item-content,.ui-resizable-resizing>.grid-stack-item-content{box-shadow:1px 4px 6px rgba(0,0,0,.2);opacity:.8}.grid-stack-animate,.grid-stack-animate .grid-stack-item{transition:left .3s,top .3s,height .3s,width .3s}.grid-stack-animate .grid-stack-item.grid-stack-placeholder,.grid-stack-animate .grid-stack-item.ui-draggable-dragging,.grid-stack-animate .grid-stack-item.ui-resizable-resizing{transition:left 0s,top 0s,height 0s,width 0s}.grid-stack>.grid-stack-item[gs-y="0"]{top:0}.grid-stack>.grid-stack-item[gs-x="0"]{left:0}.gs-12>.grid-stack-item{width:8.333%}.gs-12>.grid-stack-item[gs-x="1"]{left:8.333%}.gs-12>.grid-stack-item[gs-w="2"]{width:16.667%}.gs-12>.grid-stack-item[gs-x="2"]{left:16.667%}.gs-12>.grid-stack-item[gs-w="3"]{width:25%}.gs-12>.grid-stack-item[gs-x="3"]{left:25%}.gs-12>.grid-stack-item[gs-w="4"]{width:33.333%}.gs-12>.grid-stack-item[gs-x="4"]{left:33.333%}.gs-12>.grid-stack-item[gs-w="5"]{width:41.667%}.gs-12>.grid-stack-item[gs-x="5"]{left:41.667%}.gs-12>.grid-stack-item[gs-w="6"]{width:50%}.gs-12>.grid-stack-item[gs-x="6"]{left:50%}.gs-12>.grid-stack-item[gs-w="7"]{width:58.333%}.gs-12>.grid-stack-item[gs-x="7"]{left:58.333%}.gs-12>.grid-stack-item[gs-w="8"]{width:66.667%}.gs-12>.grid-stack-item[gs-x="8"]{left:66.667%}.gs-12>.grid-stack-item[gs-w="9"]{width:75%}.gs-12>.grid-stack-item[gs-x="9"]{left:75%}.gs-12>.grid-stack-item[gs-w="10"]{width:83.333%}.gs-12>.grid-stack-item[gs-x="10"]{left:83.333%}.gs-12>.grid-stack-item[gs-w="11"]{width:91.667%}.gs-12>.grid-stack-item[gs-x="11"]{left:91.667%}.gs-12>.grid-stack-item[gs-w="12"]{width:100%}.gs-1>.grid-stack-item{width:100%}

View File

@ -0,0 +1,8 @@
/*!
* chartjs-chart-matrix v2.0.1
* https://chartjs-chart-matrix.pages.dev/
* (c) 2023 Jukka Kurkela
* Released under the MIT license
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["exports","chart.js","chart.js/helpers"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self)["chartjs-chart-matrix"]={},t.Chart,t.Chart.helpers)}(this,(function(t,e,i){"use strict";class r extends e.DatasetController{static id="matrix";static version="2.0.1";static defaults={dataElementType:"matrix",animations:{numbers:{type:"number",properties:["x","y","width","height"]}}};static overrides={interaction:{mode:"nearest",intersect:!0},scales:{x:{type:"linear",offset:!0},y:{type:"linear",reverse:!0}}};initialize(){this.enableOptionSharing=!0,super.initialize()}update(t){const e=this._cachedMeta;this.updateElements(e.data,0,e.data.length,t)}updateElements(t,e,i,r){const s=this,a="reset"===r,{xScale:h,yScale:l}=s._cachedMeta,d=s.resolveDataElementOptions(e,r),c=s.getSharedOptions(r,t[e],d);for(let d=e;d<e+i;d++){const e=!a&&s.getParsed(d),i=a?h.getBasePixel():h.getPixelForValue(e.x),c=a?l.getBasePixel():l.getPixelForValue(e.y),u=s.resolveDataElementOptions(d,r),{width:g,height:p,anchorX:f,anchorY:x}=u,y={x:n(f,i,g),y:o(x,c,p),width:g,height:p,options:u};s.updateElement(t[d],d,y,r)}s.updateSharedOptions(c,r)}draw(){const t=this,e=t.getMeta().data||[];let i,r;for(i=0,r=e.length;i<r;++i)e[i].draw(t._ctx)}}function n(t,e,i){return"left"===t||"start"===t?e:"right"===t||"end"===t?e-i:e-i/2}function o(t,e,i){return"top"===t||"start"===t?e:"bottom"===t||"end"===t?e-i:e-i/2}function s(t,e){const{x:i,y:r,width:n,height:o}=t.getProps(["x","y","width","height"],e);return{left:i,top:r,right:i+n,bottom:r+o}}function a(t,e,i){return Math.max(Math.min(t,i),e)}function h(t){const e=s(t),r=e.right-e.left,n=e.bottom-e.top,o=function(t,e,r){const n=t.options.borderWidth;let o,s,h,l;return i.isObject(n)?(o=+n.top||0,s=+n.right||0,h=+n.bottom||0,l=+n.left||0):o=s=h=l=+n||0,{t:a(o,0,r),r:a(s,0,e),b:a(h,0,r),l:a(l,0,e)}}(t,r/2,n/2);return{outer:{x:e.left,y:e.top,w:r,h:n},inner:{x:e.left+o.l,y:e.top+o.t,w:r-o.l-o.r,h:n-o.t-o.b}}}function l(t,e,i,r){const n=null===e,o=null===i,a=!(!t||n&&o)&&s(t,r);return a&&(n||e>=a.left&&e<=a.right)&&(o||i>=a.top&&i<=a.bottom)}class d extends e.Element{static id="matrix";static defaults={backgroundColor:void 0,borderColor:void 0,borderWidth:void 0,borderRadius:0,anchorX:"center",anchorY:"center",width:20,height:20};constructor(t){super(),this.options=void 0,this.width=void 0,this.height=void 0,t&&Object.assign(this,t)}draw(t){const e=this.options,{inner:r,outer:n}=h(this),o=i.toTRBLCorners(e.borderRadius);t.save(),n.w!==r.w||n.h!==r.h?(t.beginPath(),i.addRoundedRectPath(t,{x:n.x,y:n.y,w:n.w,h:n.h,radius:o}),i.addRoundedRectPath(t,{x:r.x,y:r.y,w:r.w,h:r.h,radius:o}),t.fillStyle=e.backgroundColor,t.fill(),t.fillStyle=e.borderColor,t.fill("evenodd")):(t.beginPath(),i.addRoundedRectPath(t,{x:r.x,y:r.y,w:r.w,h:r.h,radius:o}),t.fillStyle=e.backgroundColor,t.fill()),t.restore()}inRange(t,e,i){return l(this,t,e,i)}inXRange(t,e){return l(this,t,null,e)}inYRange(t,e){return l(this,null,t,e)}getCenterPoint(t){const{x:e,y:i,width:r,height:n}=this.getProps(["x","y","width","height"],t);return{x:e+r/2,y:i+n/2}}tooltipPosition(){return this.getCenterPoint()}getRange(t){return"x"===t?this.width/2:this.height/2}}e.Chart.register(r,d),t.MatrixController=r,t.MatrixElement=d}));
//# sourceMappingURL=chartjs-chart-matrix.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,362 @@
/**
* Copyright (C) 2024 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 ResizeObserverWrapper {
_lastWidths = {};
_lastHeights = {};
_debounce(f, delay = 50, ensure = true) {
// debounce to prevent a flood of calls in a short time
let lastCall = Number.NEGATIVE_INFINITY;
let wait;
let handle;
return (...args) => {
wait = lastCall + delay - Date.now();
clearTimeout(handle);
if (wait <= 0 || ensure) {
handle = setTimeout(() => {
f(...args);
lastCall = Date.now();
}, wait);
}
};
}
observe(elements, onSizeChanged, onInitialize) {
const observer = new ResizeObserver(this._debounce((entries) => {
if (entries != undefined && entries.length > 0) {
for (const entry of entries) {
const width = entry.contentRect.width;
const height = entry.contentRect.height;
let id = entry.target.id;
if (id.length === 0) {
// element has just rendered
onInitialize(entry.target);
// we're observing multiple elements of the same class, assign a unique id
entry.target.id = Math.random().toString(36).substring(7);
this._lastWidths[id] = width;
this._lastHeights[id] = height;
} else {
if (width !== this._lastWidths[id] || height !== this._lastHeights[id]) {
this._lastWidths[id] = width;
this._lastHeights[id] = height;
onSizeChanged(entry.target, width, height);
}
}
}
}
}));
elements.forEach((element) => {
observer.observe(element);
});
}
}
class WidgetManager {
constructor(gridStackOptions = {}, gettext = {}) {
this.gridStackOptions = gridStackOptions;
this.gettext = gettext;
this.loadedModules = {}; // id -> widget module
this.widgetConfigurations = {}; // id -> per-widget configuration
this.widgetClasses = {}; // id -> instantiated widget module
this.widgetHTMLElements = {}; // id -> Element types
this.widgetTickRoutines = {}; // id -> tick routines
this.grid = null; // gridstack instance
this.moduleDiff = []; // list of module ids that are allowed, but not currently rendered
this.renderDefaultDashboard = true;
}
async initialize() {
try {
// import allowed modules and current persisted configuration
await this._loadWidgets();
// prepare widget markup
this._initializeWidgets();
// render grid and append widget markup
this._initializeGridStack();
// load all dynamic content and start tick routines
await this._loadDynamicContent();
} catch (error) {
console.error('Failed initializing Widgets', error);
}
}
async _loadWidgets() {
const response = await $.ajax('/api/core/dashboard/getDashboard', {
type: 'GET',
dataType: 'json',
contentType: 'application/json'
}).then(async (data) => {
if ('dashboard' in data && data.dashboard != null) {
this.renderDefaultDashboard = false;
let configuration = JSON.parse(data.dashboard);
configuration.forEach(item => {
this.widgetConfigurations[item.id] = item;
});
}
const promises = data.modules.map(async (item) => {
const mod = await import('/ui/js/widgets/' + item.module);
this.loadedModules[item.id] = mod.default;
});
// Load all modules simultaneously - this shouldn't take long
await Promise.all(promises).catch((error) => {
console.error('Failed to load widgets', error);
null;
});
});
}
_initializeWidgets() {
if ($.isEmptyObject(this.loadedModules)) {
throw new Error('No widgets loaded');
}
if (!this.renderDefaultDashboard) {
// restore
for (const [id, configuration] of Object.entries(this.widgetConfigurations)) {
if (id in this.loadedModules) {
this._createGridStackWidget(id, this.loadedModules[id], configuration);
}
}
} else {
// default
for (const [identifier, widgetClass] of Object.entries(this.loadedModules)) {
this._createGridStackWidget(identifier, widgetClass);
}
}
this.moduleDiff = Object.keys(this.loadedModules).filter(x => !Object.keys(this.widgetConfigurations).includes(x));
}
_createGridStackWidget(id, widgetClass, config = {}) {
// instantiate widget
const widget = new widgetClass(config);
// make id accessible to the widget, useful for traceability (e.g. data-widget-id attribute in the DOM)
widget.setId(id);
this.widgetClasses[id] = widget;
// setup generic panels
let content = widget.getMarkup();
let $panel = this._makeWidget(id, widget.title, content);
if (id in this.widgetConfigurations) {
this.widgetConfigurations[id].content = $panel.prop('outerHTML');
} else {
// let each widget override settings
const options = widget.getGridOptions();
let gridElement = {
content: $panel.prop('outerHTML'),
id: id,
...options
};
// lock the system information widget
if (id == 'systeminformation') {
gridElement = {
x: 0, y: 0,
id: id,
...gridElement,
};
}
this.widgetConfigurations[id] = gridElement;
}
}
_onWidgetClose(id) {
clearInterval(this.widgetTickRoutines[id]);
this.widgetClasses[id].onWidgetClose();
this.grid.removeWidget(this.widgetHTMLElements[id]);
this.moduleDiff.push(id);
// TODO propagate updated diff to widget selection
}
// runs only once
_initializeGridStack() {
this.grid = GridStack.init(this.gridStackOptions);
// before we render the grid, register the added event so we can store the Element type objects
this.grid.on('added', (event, items) => {
// store Elements for later use, such as update() and resizeToContent()
items.forEach((item) => {
this.widgetHTMLElements[item.id] = item.el;
});
});
for (const event of ['disable', 'dragstop', 'dropped', 'removed', 'resizestop']) {
this.grid.on(event, (event, items) => {
$('#save-grid').show();
});
}
// render to the DOM
this.grid.load(Object.values(this.widgetConfigurations));
// click handler for widget removal.
$('.close-handle').click((event) => {
let widgetId = $(event.currentTarget).data('widget-id');
this._onWidgetClose(widgetId);
});
// force the cell height of each widget to the lowest value. The grid will adjust the height
// according to the content of the widget.
this.grid.cellHeight(1);
// Serialization options
let $btn_group = $('.btn-group-container');
$btn_group.append($(`<button class="btn btn-primary" id="save-grid">${this.gettext.save}</button>`));
$btn_group.append($(`<button class="btn btn-secondary" id="restore-defaults">${this.gettext.restore}</button>`));
$('#save-grid').hide();
$('#save-grid').click(() => {
//this.grid.cellHeight('auto', false);
let items = this.grid.save(false);
//this.grid.cellHeight(1, false);
$.ajax({
type: "POST",
url: "/api/core/dashboard/saveWidgets",
dataType: "text",
contentType: 'text/plain',
data: JSON.stringify(items),
complete: function(data, status) {
let response = JSON.parse(data.responseText);
if (response['result'] == 'failed') {
console.error('Failed to save widgets', data);
}
}
});
});
$('#restore-defaults').click(() => {
ajaxGet("/api/core/dashboard/restoreDefaults", null, (response, status) => {
if (response['result'] == 'failed') {
console.error('Failed to restore default widgets');
} else {
window.location.reload();
}
});
});
}
/* Executes all widget post-render callbacks asynchronously and in "parallel".
* No widget should wait on other widgets, and therefore the
* individual widget tick() callbacks are not bound to a master timer,
* this has the benefit of making it configurable per widget.
*/
async _loadDynamicContent() {
// map to an array of context-bound _onMarkupRendered functions
let fns = Object.values(this.widgetClasses).map((widget) => {
return this._onMarkupRendered.bind(this, widget);
});
// convert each _onMarkupRendered(widget) to a promise
let promises = fns.map(func => new Promise(resolve => resolve(func())));
// fire away
await Promise.all(promises).catch((error) => {
console.error('Failed to load dynamic content', error);
null;
});
// handle dynamic resize of widgets - do this after the dynamic content has been loaded
new ResizeObserverWrapper().observe(
document.querySelectorAll('.widget'),
(elem, width, height) => {
for (const subclass of elem.className.split(" ")) {
let id = subclass.split('-')[1];
if (id in this.widgetClasses) {
if (this.widgetClasses[id].onWidgetResize(elem, width, height)) {
this._updateGrid(elem.parentElement.parentElement);
}
}
}
},
(elem) => {
this._updateGrid(elem.parentElement.parentElement);
}
);
}
// Executed for each widget; starts the widget-specific tick routine.
async _onMarkupRendered(widget) {
// first: load the widget dynamic content, make sure to bind the widget context to the callback
let onMarkupRendered = widget.onMarkupRendered.bind(widget);
// show a spinner while the widget is loading
let $selector = $(`.widget-${widget.id} > .widget-content > .panel-divider`);
$selector.after($(`<div class="spinner-${widget.id}"><i class="fa fa-circle-o-notch fa-spin"></i></div>`));
await onMarkupRendered();
$(`.spinner-${widget.id}`).remove();
// second: start the widget-specific tick routine
let onWidgetTick = widget.onWidgetTick.bind(widget);
await onWidgetTick();
const interval = setInterval(async () => {
await onWidgetTick();
this._updateGrid(this.widgetHTMLElements[widget.id]);
}, widget.tickTimeout);
// store the reference to the tick routine so we can clear it later on widget removal
this.widgetTickRoutines[widget.id] = interval;
}
// Recalculate widget/grid dimensions
_updateGrid(elem = null) {
if (elem !== null) {
this.grid.resizeToContent(elem);
} else {
for (const item of this.grid.getGridItems()) {
this.grid.resizeToContent(item);
}
}
}
// Generic widget panels
_makeWidget(identifier, title, content) {
let $panel = $(`<div class="widget widget-${identifier}"></div>`);
let $content = $(`<div class="widget-content"></div>`);
let $header = $(`
<div class="widget-header">
<div></div>
<div>${title}</div>
<div class="close-handle" data-widget-id="${identifier}">
<i class="fa fa-times fa-xs"></i>
</div>
</div>
`);
$content.append($header);
let $divider = $(`<div class="panel-divider"><div class="line"></div></div></div>`);
$content.append($divider);
$content.append(content);
$panel.append($content);
return $panel;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,194 @@
import BaseWidget from "./BaseWidget.js";
export default class BaseTableWidget extends BaseWidget {
constructor() {
super();
this.options = null;
this.data = [];
this.curSize = null;
this.sizeStates = {
0: {
'.flextable-row': {'padding': ''},
'.header .flex-row': {'border-bottom': 'solid 1px'},
'.flex-row': {'width': '100%'},
'.column': {'width': '100%'},
'.flex-cell': {'width': '100%'},
},
500: {
'.flextable-row': {'padding': '0.5em 0.5em'},
'.header .flex-row': {'border-bottom': ''},
'.flex-row': {'width': this._calculateColumnWidth.bind(this)},
'.column': {'width': ''},
'.flex-cell': {'width': ''},
}
}
this.widths = Object.keys(this.sizeStates).sort();
this.flextableId = Math.random().toString(36).substring(7);
this.$flextable = null;
this.$headerContainer = null;
this.headers = new Set();
}
_calculateColumnWidth() {
if (this.options !== null && this.data !== null) {
switch (this.options.headerPosition) {
case 'none':
return `calc(100% / ${this.data[0].length})`;
case 'top':
return `calc(100% / ${Object.keys(this.data[0]).length})`;
case 'left':
return `calc(100% / 2)`;
}
}
return '';
}
setTableOptions(options = {}) {
/**
* headerPosition: top, left or none.
* top: headers are on top of the table. Data layout: [{header1: value1}, {header1: value2}, ...]
* left: headers are on the left of the table (key-value). Data layout: [{header1: value1, header2: value2}, ...]
* none: no headers. Data layout: [[value1, value2], ...]
*/
this.options = {
headerPosition: 'top',
...options // merge and override defaults
}
}
updateTable(data = [], clear = true) {
let $table = $(`#${this.flextableId}`);
if (clear) {
$table.children('.flextable-row').remove();
}
for (const row of data) {
this.data.push(row);
let rowType = Array.isArray(row) && row !== null ? 'flat' : 'nested';
if (rowType === 'flat' && this.options.headerPosition !== 'none') {
console.error('Flat data is not supported with headers');
return null;
}
if (rowType === 'nested' && this.options.headerPosition === 'none') {
console.error('Nested data requires headers');
return null;
}
if (rowType === 'flat') {
let $row = $(`<div class="flextable-row"></div>`)
for (const item of row) {
$row.append($(`
<div class="flex-row" role="cell">${item}</div>
`));
}
$table.append($row);
} else {
if (this.options.headerPosition === 'top') {
let $flextableRow = $(`<div class="flextable-row"></div>`);
for (const [h, c] of Object.entries(row)) {
if (!this.headers.has(h)) {
this.$headerContainer.append($(`
<div class="flex-row">${h}</div>
`));
this.headers.add(h);
}
if (Array.isArray(c)) {
let $column = $('<div class="column"></div>');
for (const item of c) {
$column.append($(`
<div class="flex-row">
<div class="flex-cell">${item}</div>
</div>
`));
}
$flextableRow.append($column);
} else {
$flextableRow.append($(`
<div class="flex-row">${c}</div>
`));
}
}
$table.append($flextableRow);
} else if (this.options.headerPosition === 'left') {
for (const [h, c] of Object.entries(row)) {
if (Array.isArray(c)) {
// nested column
let $row = $('<div class="flextable-row" role="rowgroup"></div>');
$row.append($(`
<div class="flex-row rowspan first" role="cell"><b>${h}</b></div>
`));
let $column = $('<div class="column"></div>');
for (const item of c) {
$column.append($(`
<div class="flex-row">
<div class="flex-cell" role="cell">${item}</div>
</div>
`));
}
$table.append($row.append($column));
} else {
$table.append($(`
<div class="flextable-row" role="rowgroup">
<div class="flex-row first" role="cell"><b>${h}</b></div>
<div class="flex-row" role="cell">${c}</div>
</div>
`));
}
}
}
}
}
}
_constructTable() {
if (this.options === null) {
console.error('No table options set');
return null;
}
this.$flextable = $(`<div class="flextable-container" id="${this.flextableId}" role="table"></div>`)
if (this.options.headerPosition === 'top') {
this.$headerContainer = $(`<div class="flextable-header"></div>`);
this.$flextable.append(this.$headerContainer);
}
}
getMarkup() {
this._constructTable();
return $(this.$flextable);
}
async onMarkupRendered() {
}
onWidgetResize(elem, width, height) {
let lowIndex = 0;
for (let i = 0; i < this.widths.length; i++) {
if (this.widths[i] <= width) {
lowIndex = i;
} else {
break;
}
}
const lowIndexWidth = this.widths[lowIndex];
if (lowIndexWidth !== this.curSize) {
for (const [selector, styles] of Object.entries(this.sizeStates[lowIndexWidth])) {
$(elem).find(selector).css(styles);
}
this.curSize = lowIndexWidth;
return true;
}
return false;
}
}

View File

@ -0,0 +1,42 @@
export default class BaseWidget {
constructor(config) {
this.config = config;
this.title = "";
this.id = null;
this.tickTimeout = 5000; // Default tick timeout
}
setId(id) {
this.id = id;
}
getGridOptions() {
// per-widget gridstack options override
return {};
}
getMarkup() {
return $("");
}
async onMarkupRendered() {
return null;
}
onWidgetResize(elem, width, height) {
return false;
}
async onWidgetTick() {
return null;
}
onWidgetClose() {
return null;
}
/* For testing purposes */
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

View File

@ -0,0 +1,140 @@
// endpoint:/api/interfaces/overview/*
/**
* Copyright (C) 2024 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.
*/
import BaseTableWidget from "./BaseTableWidget.js";
export default class Interfaces extends BaseTableWidget {
constructor() {
super();
this.title = "Interfaces";
}
getGridOptions() {
return {
// trigger overflow-y:scroll after 650px height
sizeToContent: 650
}
}
getMarkup() {
let options = {
headerPosition: 'none'
}
this.setTableOptions(options);
return super.getMarkup();
}
async onMarkupRendered() {
ajaxGet('/api/interfaces/overview/interfacesInfo', {}, (data, status) => {
let rows = [];
data.rows.map((intf_data) => {
if (!intf_data.hasOwnProperty('config') || intf_data.enabled == false) {
return;
}
if (intf_data.config.hasOwnProperty('virtual') && intf_data.config.virtual == '1') {
return;
}
let row = [];
let symbol = '';
switch (intf_data.link_type) {
case 'ppp':
symbol = 'fa fa-mobile';
break;
case 'wireless':
symbol = 'fa fa-signal';
break;
default:
symbol = 'fa fa-exchange';
break;
}
row.push($(`
<div class="interface-info if-name">
<i class="fa fa-plug text-${intf_data.status === 'up' ? 'success' : 'danger'}" title="" data-toggle="tooltip" data-original-title="${intf_data.status}"></i>
<b class="interface-descr" onclick="location.href='/interfaces.php?if=${intf_data.identifier}'">
${intf_data.description}
</b>
</div>
`).prop('outerHTML'));
let media = (!'media' in intf_data ? intf_data.cell_mode : intf_data.media) ?? '';
row.push($(`
<div class="interface-info-detail">
<div>${media}</div>
</div>
`).prop('outerHTML'));
let ipv4 = '';
let ipv6 = '';
if ('ipv4' in intf_data && intf_data.ipv4.length > 0) {
ipv4 = intf_data.ipv4[0].ipaddr;
}
if ('ipv6' in intf_data && intf_data.ipv6.length > 0) {
ipv6 = intf_data.ipv6[0].ipaddr;
}
row.push($(`
<div class="interface-info">
${ipv4}
<div style="flex-basis: 100%; height: 0;"></div>
${ipv6}
</div>
`).prop('outerHTML'));
rows.push(row);
});
super.updateTable(rows);
$('[data-toggle="tooltip"]').tooltip();
return super.onMarkupRendered();
});
}
onWidgetResize(elem, width, height) {
if (width > 500) {
$('.interface-info-detail').parent().show();
$('.interface-info').css('justify-content', 'initial');
$('.interface-info').css('text-align', 'left');
} else {
$('.interface-info-detail').parent().hide();
$('.interface-info').css('justify-content', 'center');
$('.interface-info').css('text-align', 'center');
}
return super.onWidgetResize(elem, width, height);
}
}