mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-13 08:09:41 +00:00
Dashboard: boilerplate for new widgets (#7328)
This commit is contained in:
parent
16a6dcbd4c
commit
419fec650f
13
plist
13
plist
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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"/>
|
||||
|
||||
60
src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt
Normal file
60
src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt
Normal 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>
|
||||
249
src/opnsense/www/css/dashboard.css
Normal file
249
src/opnsense/www/css/dashboard.css
Normal 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);
|
||||
}
|
||||
1
src/opnsense/www/css/gridstack-extra.min.css
vendored
Normal file
1
src/opnsense/www/css/gridstack-extra.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/opnsense/www/css/gridstack.min.css
vendored
Normal file
1
src/opnsense/www/css/gridstack.min.css
vendored
Normal 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%}
|
||||
8
src/opnsense/www/js/chartjs-plugin-matrix.min.js
vendored
Normal file
8
src/opnsense/www/js/chartjs-plugin-matrix.min.js
vendored
Normal 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
|
||||
1
src/opnsense/www/js/gridstack-all.min.js
vendored
Normal file
1
src/opnsense/www/js/gridstack-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
362
src/opnsense/www/js/opnsense_widget_manager.js
Normal file
362
src/opnsense/www/js/opnsense_widget_manager.js
Normal 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;
|
||||
}
|
||||
}
|
||||
1176
src/opnsense/www/js/smoothie.js
Normal file
1176
src/opnsense/www/js/smoothie.js
Normal file
File diff suppressed because it is too large
Load Diff
194
src/opnsense/www/js/widgets/BaseTableWidget.js
Normal file
194
src/opnsense/www/js/widgets/BaseTableWidget.js
Normal 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;
|
||||
}
|
||||
}
|
||||
42
src/opnsense/www/js/widgets/BaseWidget.js
Normal file
42
src/opnsense/www/js/widgets/BaseWidget.js
Normal 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));
|
||||
}
|
||||
}
|
||||
140
src/opnsense/www/js/widgets/Interfaces.js
Normal file
140
src/opnsense/www/js/widgets/Interfaces.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user